From c7cdec27e06aab4ae7eb9681ca3af4e7ba668933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 18 Sep 2025 10:57:17 +0800 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit a514bf40090d951f1300d7397b55dd6add230a01 Author: 浅念 Date: Thu Sep 18 09:55:10 2025 +0800 Squashed commit of the following: commit 9e260ddcd85c2e5431328f9b6ac65ba96975d4fd Author: 浅念 Date: Thu Sep 18 09:49:33 2025 +0800 修改 kpi commit 6feb3ace75df218d063448a777ec56aa77a5f473 Author: 浅念 Date: Thu Sep 18 09:42:44 2025 +0800 kpi 统计 commit ec257a246d0548b9e63c72f7a5871f0f4e9aee19 Author: 浅念 Date: Wed Sep 17 17:04:35 2025 +0800 修改 弹框表格 commit fa764852ab88e624640585e1cf18e552629a69aa Author: 浅念 Date: Wed Sep 17 15:54:35 2025 +0800 添加 账户简称 commit 65f7f4a123a1b2cbd76fd17636240312ff3579a0 Author: 浅念 Date: Wed Sep 17 15:10:28 2025 +0800 品牌加热门 修改 仪表盘接口 commit 922cfec81d771e09b0a915ad83de0f59b722ef11 Author: 浅念 Date: Tue Sep 16 15:57:17 2025 +0800 调整备货单 crm 端页面 commit 5c8df36f127b56bdec379316445bf684972e620c Author: 浅念 Date: Mon Sep 15 17:14:38 2025 +0800 调整备货单 及各页面名称 commit 5503a14408fb24e6c04c8aa301d26b0ce64ae8d2 Author: 浅念 Date: Fri Sep 12 18:10:03 2025 +0800 账户添加公司 commit 3439b45d9ab833c35cbcfaa7e64b3e2d4fccb93b Author: 浅念 Date: Fri Sep 12 17:53:26 2025 +0800 修改终版备货单 commit 045d13eb7b9982058150351e5046ab56b9ac7269 Author: 浅念 Date: Fri Sep 12 17:49:07 2025 +0800 修改备货单 等页面 commit da690dfa06b5b01216c6a5c2d53a725dd2fd865b Author: 浅念 Date: Thu Sep 11 17:44:37 2025 +0800 修改 备货单 详情 commit 853e9591b3420658e074d0fcbc287ee843d5ef72 Author: 浅念 Date: Wed Sep 10 18:11:39 2025 +0800 修改 备货单页面 commit 9052bd1027956b2860972fb372bbc5a5d60e054a Author: 浅念 Date: Tue Sep 9 20:53:30 2025 +0800 备货单 审核功能 commit 079d9babb8c0ef61b4df75d265c0a61f877d0c3d Merge: ebd7479 91d810b Author: 浅念 Date: Tue Sep 9 14:51:43 2025 +0800 Merge branch 'feature/crm_pc' of http://219.157.255.213:25311/xinyouhe/art-design into feature/crm_vehicle commit ebd7479573f3dc64acb7e86da8f87ea9ff5c137f Merge: 64e53b6 459d306 Author: 浅念 Date: Tue Sep 9 13:57:28 2025 +0800 Merge branch 'feature/crm_pc' of http://219.157.255.213:25311/xinyouhe/art-design into feature/crm_vehicle commit 64e53b6ee9855f2221d1f9eacd5a56b94354df3d Author: 浅念 Date: Mon Sep 8 19:09:55 2025 +0800 备货单 加 审批流程 commit 9bd7c89cb425141f4645d4f7bd2ef4ad2a001f3b Author: 浅念 Date: Thu Sep 4 15:09:17 2025 +0800 车辆信息,备货单 等页面 commit 91d810b33a05e8f91e9811f1a6fe1b0508a52af8 Author: 浅念 Date: Tue Sep 9 14:51:23 2025 +0800 添加公司搜索 commit 459d306504f497f69cefdcd31774ba012bb7a315 Author: 浅念 Date: Tue Sep 9 13:56:43 2025 +0800 用户添加 公司搜索 commit 8a0d4fc516f6a32102e37bc868ff93a73a4c86de Author: 浅念 Date: Thu Sep 4 14:38:09 2025 +0800 自定义跟进时间 处理 commit 58f7d56f402d67b7f7c0a14f06ab850cbc4f0e7d Author: 浅念 Date: Wed Sep 3 15:13:10 2025 +0800 工单去掉 移动端不显示限制 commit a91e1d782099a513261ed3a17b14afa3ecf13545 Author: 浅念 Date: Tue Sep 2 14:13:15 2025 +0800 将编号 后移 commit a9f8c5c4dcf026a4513290f4c09af25d5a85d708 Author: 浅念 Date: Wed Aug 27 09:20:56 2025 +0800 客户重复通知 commit aefd43f79de8995c107b6acb492748f4b79690df Author: 浅念 Date: Tue Aug 26 09:55:42 2025 +0800 修改 通知详情样式 和 即时通讯 commit 1bd9d163ecb6f276be05e1f9f97b5b7576bbe8c5 Author: 浅念 Date: Mon Aug 25 15:31:14 2025 +0800 通知 列表富文本内容显示 commit be6093e57d06d943d233002f2349b321673c6bb0 Author: 浅念 Date: Mon Aug 25 13:34:13 2025 +0800 crm 客户添加自定义列 跟进添加图片粘贴上传 commit d83246ba8c3a0c482386f6e715966da6ea542f92 Author: 浅念 Date: Fri Aug 22 18:33:58 2025 +0800 显示编号 commit 329acaf68d0c11ad0f314a4b24b061a543618e69 Author: 浅念 Date: Fri Aug 22 18:27:26 2025 +0800 通知板块 及 功能调整 commit b3199440073246655d7aa116eaeb06926bc6b0a7 Author: 浅念 Date: Thu Aug 21 11:12:12 2025 +0800 调整联系方式字段为电话 列表中多个联系方式纵向排列显示 会增加公司信息的自定义输入框 排序搜索 等功能 commit 70d26f08295850f87476e08244e4b0309be983fa Author: 浅念 Date: Tue Aug 19 16:18:15 2025 +0800 1.0.3 的相关功能 commit 0e84bedb670e5697dc5018d35f5c202dd58072d1 Author: 浅念 Date: Fri Aug 15 16:31:55 2025 +0800 v1.0.2 更新内容 commit f5deb7b8f78c2fc949211df173164fb2cc3f7fc4 Author: 浅念 Date: Wed Aug 13 19:27:49 2025 +0800 添加菜单 权限 等页面 commit e797cb24def2c32a1d9355789eb970fc99fde566 Author: 浅念 Date: Mon Aug 11 19:24:24 2025 +0800 添加工单附件 及样式 commit 619311dfbed9f6228500862dda116058a8a2bb31 Author: chenlan Date: Fri Aug 8 22:13:25 2025 +0800 添加crm 归属客户 commit b402e522bf0ac644a2d1d5759017eb909b24ada3 Author: 浅念 Date: Thu Aug 7 10:51:17 2025 +0800 询单权限校验 commit f950f665a9309c24f9e29759e0b29d4fd8e1b722 Author: 浅念 Date: Thu Aug 7 10:10:45 2025 +0800 添加 询单图片预览 commit 8deecc8197fda8f9242804a4b8da4c6165dfa7f8 Author: 浅念 Date: Wed Aug 6 18:36:42 2025 +0800 修改用户页面 询单 commit 3e24d1905f92753104b1b8d2dcb83316e865b078 Author: 浅念 Date: Tue Aug 5 18:25:32 2025 +0800 添加用户页面 客户页面接口 等 commit 7542d80e38a6f6b3f8d2e5538abbefc3662939b4 Author: 浅念 Date: Mon Aug 4 18:05:07 2025 +0800 修改 仪表盘页面样式 commit e5548f95e4a665282ab6166aa78ff397badc39f5 Author: 浅念 Date: Mon Aug 4 14:50:55 2025 +0800 修改询单字段 仪表盘页面 及添加管理端客户页面 commit d30ea5b7bf623e4785e207a0a9a48fb770a40213 Author: 浅念 Date: Mon Aug 4 14:48:58 2025 +0800 添加echarts 插件 commit 0e4160c5caf49e490b2b4cbf293b7dd82dbea044 Author: 浅念 Date: Fri Aug 1 18:27:22 2025 +0800 仪表盘接口 commit bd23288acd1043fa96581cf929cc8b6bcbd96ce1 Author: 浅念 Date: Fri Aug 1 18:26:46 2025 +0800 修改 仪表盘 commit 6bfb940881bd0c2bea1762b0f8d292e779ad6e55 Author: 浅念 Date: Fri Aug 1 10:01:35 2025 +0800 客户列表 添加公海详情展示 commit b50f650565d7dbc535a0fca737e6c8c60152a4e1 Author: 浅念 Date: Thu Jul 31 17:26:36 2025 +0800 修改 客户标签样式 个人信息添加邮箱 修复关闭全部页面404 commit ea1dbb7af46cac7f4f88232da6dda145aea5ff00 Author: 浅念 Date: Wed Jul 30 18:50:31 2025 +0800 修改公海和询单页面 commit 1bfbb95bd9ded050d4b2b7efa6819859c1f8f538 Author: 浅念 Date: Wed Jul 30 17:23:09 2025 +0800 修改样式和添加用户数据显示 commit a9f5f76b353b35af1fe20dc9e0e7b0847d4ebb95 Author: 浅念 Date: Wed Jul 30 15:06:03 2025 +0800 设置默认页面 commit 3ea9d3d5df25f3658a2c3ef6d8e5644b44a56f0c Author: 浅念 Date: Wed Jul 30 11:46:31 2025 +0800 修改飞书登录 公司id 系统id 获取不到问题 客户及公海添加用户信息显示 commit 0cef82279f322fd6562afc145c3e98d71557edda Author: 浅念 Date: Tue Jul 29 16:42:36 2025 +0800 修改工单 客户等页面 commit 695ebbcc71c47587d2befa845f85dabfc6c217b3 Author: 浅念 Date: Fri Jul 25 17:50:04 2025 +0800 修改默认值 commit ffc194a1c1e35a209b6bc8c1532c095ada17bef0 Author: 浅念 Date: Fri Jul 25 17:05:33 2025 +0800 飞书授权添加系统码 及登录进入页面路由 commit 48b95e252f85e3e18fde5036b820b911f10d4ae7 Author: 浅念 Date: Fri Jul 25 15:48:49 2025 +0800 回收站添加占位 commit 382011d592cd2b82e0f305223726a6eea41c3a5a Author: 浅念 Date: Fri Jul 25 15:11:50 2025 +0800 修改登录页样式公海数据 个人信息默认图 commit 0f790269db159e29de890d000007516ebe5e6306 Author: 浅念 Date: Fri Jul 25 09:16:55 2025 +0800 修改字段名称 commit f18cfdb69e786b12d94c43a40bedaf5ccc6f53a8 Author: 浅念 Date: Thu Jul 24 17:28:51 2025 +0800 调整代码格式 commit 858147fbbdec0583737ac080f1b94772ff6b2814 Author: 浅念 Date: Thu Jul 24 10:17:22 2025 +0800 修改token刷新 及修改询单详情展示 commit c333b907e682a3935fc87754eff246b43f4a3518 Author: 浅念 Date: Wed Jul 23 18:22:56 2025 +0800 对接询单接口 commit 7979a31e361d18f3d2db078dc802f037033f6bbd Author: 浅念 Date: Wed Jul 23 14:40:02 2025 +0800 修改代码格式 commit 4a393878658bd2e167b51da2433e906f8734d816 Author: 浅念 Date: Wed Jul 23 14:07:13 2025 +0800 设置客户列表权限 commit fc4de02582e64dc03c826542489b3943dcab2a98 Author: 浅念 Date: Tue Jul 22 17:08:13 2025 +0800 feat(router): 调整菜单 commit 2ffe0c97539ff47920438d8ade96b714458410e1 Author: 浅念 Date: Tue Jul 22 17:04:16 2025 +0800 feat(crmApi.ts): 调整结构 commit c3fb2c48d38629d26e83fd7733190a6d80893e4e Author: 浅念 Date: Tue Jul 22 17:00:25 2025 +0800 feat(router): 路由调整 commit c5b0a8265af5180515862aefcf6018bd7a664876 Author: 浅念 Date: Tue Jul 22 16:54:06 2025 +0800 chore: 添加询单页面及调整菜单 commit ca06c9bc469989b9b682bc1a40d530b45aae9025 Author: 浅念 Date: Tue Jul 22 16:35:43 2025 +0800 询单页面及菜单 commit 3b6de4c7fa2736a3e6fb71188e0f67fe145edf5c Merge: d3bb4ec 85e8b9b Author: 浅念 Date: Tue Jul 22 09:22:20 2025 +0800 Merge branch 'master' into feature/crm_pc commit d3bb4ec6e5406d3a0e6d8185900cb4220981025b Merge: c7d8908 d7ab6f1 Author: 浅念 Date: Mon Jul 21 18:22:55 2025 +0800 Merge branch 'master' into feature/crm_pc # Conflicts: # .gitignore # src/api/crmApi.ts # src/api/usersApi.ts # src/store/modules/user.ts # src/utils/http/index.ts # src/views/auth/login/index.vue # src/views/crmpc/customer/Show.vue # src/views/crmpc/customer/index.vue commit c7d890809c539a7592979904337ce6826bdfa714 Author: 浅念 Date: Mon Jul 21 18:12:16 2025 +0800 添加系统码 commit 31cd18fade9aa8191d52c0e10a9823c22867dcd4 Author: 浅念 Date: Mon Jul 21 18:02:48 2025 +0800 Merge branch 'master' into feature/crm_pc # Conflicts: # .gitignore commit 59c9c97c0830c285ff0bcd4f51e0d9de916057ee Author: 浅念 Date: Mon Jul 21 17:56:52 2025 +0800 Merge branch 'master' into feature/crm_pc # Conflicts: # .gitignore commit d4b577c42eb4af584f4d190f292488a55f306c03 Author: 浅念 Date: Mon Jul 21 13:03:56 2025 +0800 feat(crm): 添加相关图片 commit e4967760b742aa42aed88bf6b440272e20080630 Merge: 0c67666 85102fc Author: 浅念 Date: Mon Jul 21 11:25:49 2025 +0800 Finish crm_pc commit 85102fcf32a6e9f635c6de749646eb6c247ca633 Author: 浅念 Date: Mon Jul 21 11:24:06 2025 +0800 crm 接口对接 commit 0c676667932a5aa59416032d3dc856c95ec2cbff Author: 浅念 Date: Wed Jul 16 17:18:14 2025 +0800 添加上传头部信息 commit a41c75f70386728197dc92ae778ddd6232ee44c5 Author: 浅念 Date: Tue Jul 15 09:40:53 2025 +0800 修改页面 commit 22ae0e86c82a6c969cf8a35364c0a0935b74bbc8 Author: 浅念 Date: Mon Jul 14 17:59:02 2025 +0800 修改页面 commit 812e3c4b0c0a5abac3bb540e3907b1b25469f1aa Author: 浅念 Date: Mon Jul 14 10:09:10 2025 +0800 修改菜单问题 commit 2ad479b006b9767e3eedafe56affe364ac02ee16 Author: 浅念 Date: Fri Jul 11 19:45:57 2025 +0800 修改样式 commit 5802e157bf65d353d71be3dd7ff0579bcc971163 Author: 浅念 Date: Fri Jul 11 19:43:34 2025 +0800 修改工单 commit 713d43f7b44bf2cc8543ce7f77716a94e798e648 Author: 浅念 Date: Fri Jul 11 18:37:16 2025 +0800 添加登录 commit 719911918f3f273b578a6ebd63a7771fe9a2bdd3 Author: 浅念 Date: Fri Jul 4 17:16:38 2025 +0800 项目创建 commit ce47b39fa51dd09cfbc7433a1e10521767e3a5ac Author: 浅念 Date: Wed Jul 2 09:37:51 2025 +0800 删除 # Conflicts: # src/App.vue # src/api/adminApi.ts # src/api/crmApi.ts # src/api/menuApi.ts # src/api/systemApi.ts # src/api/usersApi.ts # src/components/core/layouts/art-header-bar/index.vue # src/components/core/layouts/art-page-content/index.vue # src/router/routes/asyncRoutes.ts # src/router/routesAlias.ts # src/views/admin/customer/customeraudit.vue # src/views/admin/customer/highseas.vue # src/views/admin/customer/index.vue # src/views/admin/inquiry_system/index.vue # src/views/admin/system/company.vue # src/views/admin/system/feishu_app.vue # src/views/admin/system/menu/index.vue # src/views/admin/system/role/index.vue # src/views/admin/system/systems.vue # src/views/admin/user/index.vue # src/views/admin/user/user_info.vue # src/views/auth/login/index.vue # src/views/crmpc/customer/Add.vue # src/views/crmpc/customer/Edit.vue # src/views/crmpc/customer/Followadd.vue # src/views/crmpc/customer/Show.vue # src/views/crmpc/customer/index.vue # src/views/crmpc/highseas/index.vue # src/views/crmpc/recycle/index.vue # src/views/crmpc/team/team_customer.vue # src/views/crmpc/team/user_customer.vue # src/views/dashboard/console/index.vue # src/views/system/user-center/index.vue # src/views/work/myOrder/index.vue # src/views/work/order/index.vue --- .env.development | 6 +- .env.production | 4 +- src/App.vue | 160 +- src/api/admin/kpi.ts | 109 + src/api/adminApi.ts | 119 +- src/api/commonApi.ts | 135 ++ src/api/crmApi.ts | 96 +- src/api/crmpc/vehicleApi.ts | 98 + src/api/financeApi.ts | 74 + src/api/menuApi.ts | 2 +- src/api/systemApi.ts | 93 + src/api/usersApi.ts | 15 + src/api/vehicleApi.ts | 196 ++ src/assets/img/common/logox.png | Bin 0 -> 26170 bytes src/assets/img/crm/contract.png | Bin 0 -> 1508 bytes src/assets/styles/el-ui.scss | 9 + .../core/layouts/art-header-bar/index.vue | 103 +- .../core/layouts/art-notification/index.vue | 267 ++- .../core/layouts/art-page-content/index.vue | 6 +- src/components/core/tables/ArtTableHeader.vue | 2 +- src/components/custom/OrderDetailContent.vue | 419 ++++ src/composables/useOrderDetailDialog.ts | 73 + src/composables/usePdfPreview.ts | 140 ++ src/composables/useStockingForm.ts | 968 +++++++++ src/composables/useUploadHeaders.ts | 8 +- src/config/index.ts | 2 +- src/router/guards/beforeEach.ts | 80 +- src/router/routes/asyncRoutes.ts | 108 + src/router/routes/staticRoutes.ts | 6 + src/router/routesAlias.ts | 8 +- src/router/utils/registerRoutes.ts | 16 +- src/store/modules/user copy.ts | 212 ++ src/typings/api.d.ts | 2 + src/utils/storage/storage.ts | 4 +- src/utils/sys/wsClient.ts | 273 +++ src/views/admin/approval/approval_add.vue | 1279 ++++++++++++ src/views/admin/approval/approval_edit.vue | 1373 ++++++++++++ src/views/admin/approval/approval_list.vue | 283 +++ src/views/admin/approval/stocking_auth.vue | 404 ++++ .../admin/approval/stocking_auth_detil.vue | 541 +++++ src/views/admin/customer/customeraudit.vue | 164 +- src/views/admin/customer/highseas.vue | 24 +- src/views/admin/customer/index.vue | 61 +- src/views/admin/customer/recycle.vue | 574 +++++ src/views/admin/finance/account.vue | 389 ++++ src/views/admin/finance/currency.vue | 389 ++++ src/views/admin/inquiry_system/index.vue | 1 + src/views/admin/kpi/kpi_annual.vue | 339 +++ src/views/admin/kpi/kpi_monthly.vue | 331 +++ src/views/admin/kpi/kpi_statistics.vue | 1103 ++++++++++ src/views/admin/stock/final_list_admin.vue | 425 ++++ src/views/admin/stock/final_show_admin.vue | 678 ++++++ src/views/admin/stock/show_admin.vue | 670 ++++++ src/views/admin/stock/stocking_list_admin.vue | 469 +++++ src/views/admin/system/company.vue | 14 +- src/views/admin/system/dictionary/index.vue | 538 +++++ src/views/admin/system/feishu_app.vue | 14 +- src/views/admin/system/menu/index.vue | 38 +- src/views/admin/system/notice/index.vue | 309 +++ src/views/admin/system/role/index.vue | 265 ++- src/views/admin/system/systems.vue | 14 +- src/views/admin/user/index.vue | 328 ++- src/views/admin/user/user_info.vue | 103 +- src/views/admin/vehicle/brand.vue | 654 ++++++ src/views/admin/vehicle/color.vue | 356 ++++ src/views/admin/vehicle/modification.vue | 356 ++++ src/views/admin/vehicle/port.vue | 353 ++++ src/views/admin/vehicle/vehicle_list.vue | 1426 +++++++++++++ src/views/auth/login/index.vue | 4 +- src/views/auth/signature/mobile.vue | 362 ++++ src/views/common/notice/index.vue | 276 +++ src/views/crmpc/approval/auth_detil.vue | 554 +++++ src/views/crmpc/approval/stocking_auth.vue | 384 ++++ src/views/crmpc/customer/Add.vue | 42 +- src/views/crmpc/customer/Edit.vue | 5 +- src/views/crmpc/customer/Followadd.vue | 174 +- src/views/crmpc/customer/Show.vue | 3 +- src/views/crmpc/customer/index copy.vue | 735 +++++++ src/views/crmpc/customer/index.vue | 443 +++- src/views/crmpc/highseas/index.vue | 16 +- src/views/crmpc/home/index.vue | 36 +- src/views/crmpc/recycle/index.vue | 1 + src/views/crmpc/stocking/Add.vue | 1111 ++++++++++ src/views/crmpc/stocking/final_add.vue | 1108 ++++++++++ src/views/crmpc/stocking/final_edit.vue | 1204 +++++++++++ src/views/crmpc/stocking/final_list.vue | 471 +++++ src/views/crmpc/stocking/final_show.vue | 701 +++++++ src/views/crmpc/stocking/show.vue | 688 ++++++ src/views/crmpc/stocking/stocking_edit.vue | 1110 ++++++++++ src/views/crmpc/stocking/stocking_list.vue | 553 +++++ src/views/crmpc/team/team_customer.vue | 40 +- src/views/crmpc/team/user_customer.vue | 3 + src/views/crmpc/vehicle/color.vue | 356 ++++ src/views/crmpc/vehicle/modification.vue | 361 ++++ src/views/crmpc/vehicle/vehicle_list.vue | 1426 +++++++++++++ src/views/dashboard/console/index.vue | 138 +- src/views/system/user-center/index.vue | 256 ++- src/views/work/myOrder/index.vue | 606 +++--- src/views/work/order/index.vue | 1852 +++++++++-------- src/views/work/type/index.vue | 363 ++-- 100 files changed, 31463 insertions(+), 1997 deletions(-) create mode 100644 src/api/admin/kpi.ts create mode 100644 src/api/commonApi.ts create mode 100644 src/api/crmpc/vehicleApi.ts create mode 100644 src/api/financeApi.ts create mode 100644 src/api/vehicleApi.ts create mode 100644 src/assets/img/common/logox.png create mode 100644 src/assets/img/crm/contract.png create mode 100644 src/components/custom/OrderDetailContent.vue create mode 100644 src/composables/useOrderDetailDialog.ts create mode 100644 src/composables/usePdfPreview.ts create mode 100644 src/composables/useStockingForm.ts create mode 100644 src/store/modules/user copy.ts create mode 100644 src/utils/sys/wsClient.ts create mode 100644 src/views/admin/approval/approval_add.vue create mode 100644 src/views/admin/approval/approval_edit.vue create mode 100644 src/views/admin/approval/approval_list.vue create mode 100644 src/views/admin/approval/stocking_auth.vue create mode 100644 src/views/admin/approval/stocking_auth_detil.vue create mode 100644 src/views/admin/customer/recycle.vue create mode 100644 src/views/admin/finance/account.vue create mode 100644 src/views/admin/finance/currency.vue create mode 100644 src/views/admin/kpi/kpi_annual.vue create mode 100644 src/views/admin/kpi/kpi_monthly.vue create mode 100644 src/views/admin/kpi/kpi_statistics.vue create mode 100644 src/views/admin/stock/final_list_admin.vue create mode 100644 src/views/admin/stock/final_show_admin.vue create mode 100644 src/views/admin/stock/show_admin.vue create mode 100644 src/views/admin/stock/stocking_list_admin.vue create mode 100644 src/views/admin/system/dictionary/index.vue create mode 100644 src/views/admin/system/notice/index.vue create mode 100644 src/views/admin/vehicle/brand.vue create mode 100644 src/views/admin/vehicle/color.vue create mode 100644 src/views/admin/vehicle/modification.vue create mode 100644 src/views/admin/vehicle/port.vue create mode 100644 src/views/admin/vehicle/vehicle_list.vue create mode 100644 src/views/auth/signature/mobile.vue create mode 100644 src/views/common/notice/index.vue create mode 100644 src/views/crmpc/approval/auth_detil.vue create mode 100644 src/views/crmpc/approval/stocking_auth.vue create mode 100644 src/views/crmpc/customer/index copy.vue create mode 100644 src/views/crmpc/stocking/Add.vue create mode 100644 src/views/crmpc/stocking/final_add.vue create mode 100644 src/views/crmpc/stocking/final_edit.vue create mode 100644 src/views/crmpc/stocking/final_list.vue create mode 100644 src/views/crmpc/stocking/final_show.vue create mode 100644 src/views/crmpc/stocking/show.vue create mode 100644 src/views/crmpc/stocking/stocking_edit.vue create mode 100644 src/views/crmpc/stocking/stocking_list.vue create mode 100644 src/views/crmpc/vehicle/color.vue create mode 100644 src/views/crmpc/vehicle/modification.vue create mode 100644 src/views/crmpc/vehicle/vehicle_list.vue diff --git a/.env.development b/.env.development index 45ed3e5..c188e1a 100644 --- a/.env.development +++ b/.env.development @@ -5,11 +5,13 @@ VITE_BASE_URL = / # API 地址前缀 # VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default -# VITE_API_URL = http://219.157.255.213:25702 +# VITE_API_URL = http://219.157.255.213:25703 # VITE_API_URL = https://mzl.dev.hnxinyouhe.com # VITE_API_URL = https://fdesk.hnxinyouhe.com VITE_API_URL = https://x-server.hnxinyouhe.com - +# VITE_API_URL_WSS = wss://mzl.dev.hnxinyouhe.com +# VITE_API_URL_WSS = wss://fdesk.hnxinyouhe.com +VITE_API_URL_WSS = wss://x-server.hnxinyouhe.com # Delete console VITE_DROP_CONSOLE = false \ No newline at end of file diff --git a/.env.production b/.env.production index 9b6c051..1ffe966 100644 --- a/.env.production +++ b/.env.production @@ -5,7 +5,9 @@ VITE_BASE_URL = / # API 地址前缀 # VITE_API_URL = https://fdesk.hnxinyouhe.com +# VITE_API_URL = https://mzl.dev.hnxinyouhe.com VITE_API_URL = https://x-server.hnxinyouhe.com - +# VITE_API_URL_WSS = wss://fdesk.hnxinyouhe.com +VITE_API_URL_WSS = wss://x-server.hnxinyouhe.com # Delete console VITE_DROP_CONSOLE = true \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index e698776..eacffaf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,9 +12,12 @@ import { UserService } from './api/usersApi' import { setThemeTransitionClass } from './utils/theme/animation' import { checkStorageCompatibility } from './utils/storage' + import { mittBus } from '@/utils/sys' + import appLogo from '@/assets/img/common/logox.png' + const router = useRouter() const userStore = useUserStore() - const { language } = storeToRefs(userStore) + const { language, accessToken } = storeToRefs(userStore) function setCompanyFromUrl() { const userStore = useUserStore() const params = new URLSearchParams(window.location.search) @@ -32,20 +35,143 @@ zh: zh, en: en } + const isSupported = ref(false) + const permissionStatus = ref('default') + // 检查浏览器支持情况 + const checkSupport = () => { + if ('Notification' in window) { + isSupported.value = true + console.log('浏览器支持原生Notification API') + } else { + isSupported.value = false + console.log('⚠️ 浏览器不支持原生Notification API') + } + } + // 请求通知权限 + const requestPermission = async () => { + if (!('Notification' in window)) { + console.log('浏览器不支持原生Notification API') + return + } + try { + const permission = await Notification.requestPermission() + permissionStatus.value = permission + if (permission === 'granted') { + console.log('用户已授予通知权限') + } else if (permission === 'denied') { + console.log('用户拒绝了通知权限') + } else { + console.log('用户暂时未授予通知权限') + } + } catch (error) { + console.log(`请求权限时出错:`) + } + } + + // 显示基本通知(支持自定义标题与内容) + const showBasicNotification = ( + title: string = '基本通知', + body: string = '这是一个使用浏览器原生API的系统通知' + ) => { + if (!('Notification' in window)) { + ElMessage({ + message: '浏览器不支持原生Notification API', + type: 'warning' + }) + return + } + if (Notification.permission !== 'granted') { + ElMessage({ + message: '请先授予通知权限', + type: 'warning' + }) + requestPermission() + return + } + try { + const iconUrl = appLogo || '/favicon.ico' + const notification = new Notification(title, { + body, + requireInteraction: true, + icon: iconUrl + }) + notification.onclick = () => { + notification.close() + } + } catch (error) {} + } onBeforeMount(() => { setThemeTransitionClass(true) setCompanyFromUrl() }) + // 页面刷新时重置WebSocket重连次数,同时清理事件监听 + onBeforeUnmount(() => { + import('@/utils/sys/wsClient').then(({ resetWebSocketReconnect }) => { + resetWebSocketReconnect() + }) + mittBus.off('ws:message') + }) + + const handleWsMessageNotification = (data: any) => { + if (data && data.event === 'notify.receive') { + const notice = data?.data?.notice_data + const title = notice?.message?.title || '新通知' + const rawContent = notice?.message?.content || '' + const body = typeof rawContent === 'string' ? rawContent.replace(/<[^>]+>/g, '') : '' + showBasicNotification(title, body) + } + } + onMounted(() => { // 检查存储兼容性 checkStorageCompatibility() + checkSupport() + requestPermission() // 提升暗黑主题下页面刷新视觉体验 setThemeTransitionClass(false) // 系统升级 systemUpgrade() // 获取用户信息 getUserInfo() + // 初始化 WebSocket + // showEventNotification() + if (accessToken.value) { + import('@/utils/sys/wsClient').then( + ({ initWebSocket, resetWebSocketReconnect, getWebSocketStatus }) => { + // 页面刷新时重置重连次数 + resetWebSocketReconnect() + initWebSocket(accessToken.value) + // 添加调试功能,可以在控制台查看WebSocket状态 + // @ts-ignore + window.getWebSocketStatus = getWebSocketStatus + // console.log('WebSocket状态查看: window.getWebSocketStatus()') + } + ) + } + + // 监听ws消息,用于系统级通知 + mittBus.on('ws:message', handleWsMessageNotification) + }) + + // 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新) + watch(accessToken, async (token) => { + if (token) { + const { initWebSocket, getWebSocketStatus } = await import('@/utils/sys/wsClient') + const status = getWebSocketStatus?.() + // 避免重复初始化:已连接或正在连接时不再初始化 + if ( + !status?.readyState || + (status.readyState !== WebSocket.OPEN && status.readyState !== WebSocket.CONNECTING) + ) { + initWebSocket(token) + } + } else { + // 退出登录时,关闭并重置 + const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient') + closeWebSocket() + resetWebSocketReconnect() + } }) // 获取用户信息 @@ -53,6 +179,35 @@ if (userStore.isLogin) { try { const data = await UserService.getUserInfo() + const { systemsid } = useUserStore() + // 排除移动端电子签名页,不做签名校验 + const currentPath = router.currentRoute.value?.path || '' + const isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile') + if (!isOnMobileSignature) { + if (data.user.sign_img == null || data.user.sign_img == '') { + ElMessageBox.alert('请前往个人中心完成签名操作!', '提醒', { + confirmButtonText: '确定', + callback: () => { + if (systemsid == '1') { + router.push('/admin/user/user_info') + } else { + router.push('/system/user-center') + } + } + }) + } else if (data.user.email == null || data.user.email == '') { + ElMessageBox.alert('邮箱没有填写!请前往个人中心补充邮箱!', '提醒', { + confirmButtonText: '确定', + callback: () => { + if (systemsid == '1') { + router.push('/admin/user/user_info') + } else { + router.push('/system/user-center') + } + } + }) + } + } userStore.setUserInfo(data) } catch (error) { console.error('获取用户信息失败', error) @@ -116,4 +271,7 @@ top: 50%; transform: translateY(-50%); } + .item-row-wrap { + margin-bottom: 12px; + } diff --git a/src/api/admin/kpi.ts b/src/api/admin/kpi.ts new file mode 100644 index 0000000..58edea6 --- /dev/null +++ b/src/api/admin/kpi.ts @@ -0,0 +1,109 @@ +import request from '@/utils/http' + +export class kpiService { + // 月度-业绩 列表 + static getmonthlylist(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/targets/monthly/list', + params + }) + } + // 月度-业绩 添加 + static addmonthly(params: any) { + return request.post({ + url: '/admin/targets/monthly/store', + params + }) + } + // 月度-业绩 删除 + static deletemonthly(params: any) { + return request.post({ + url: '/admin/targets/monthly/destroy', + params + }) + } + // 月度-业绩 修改 + static updatemonthly(params: any) { + return request.post({ + url: '/admin/targets/monthly/update', + params + }) + } + // 月度-业绩 详情 + static detailmonthly(params: any) { + return request.get({ + url: '/admin/targets/monthly/show', + params + }) + } + //年度-业绩 列表 + static getyearlylist(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/targets/annual/list', + params + }) + } + // 年度-业绩 添加 + static addyearly(params: any) { + return request.post({ + url: '/admin/targets/annual/store', + params + }) + } + // 年度-业绩 删除 + static deleteyearly(params: any) { + return request.post({ + url: '/admin/targets/annual/destroy', + params + }) + } + // 年度-业绩 详情 + static detailyearly(params: any) { + return request.get({ + url: '/admin/targets/annual/show', + params + }) + } + // 年度-业绩 修改 + static updateyearly(params: any) { + return request.post({ + url: '/admin/targets/annual/update', + params + }) + } + //年度-公司目标完成率 + static statisticstotal(params: any) { + return request.get({ + url: '/admin/targets/statistics/total', + params + }) + } + //年度-个人销售数量排名 + static statisticsranking(params: any) { + return request.get({ + url: '/admin/targets/statistics/ranking', + params + }) + } + //年度-业务KPI分月完成数量表 + static statisticsmonthly(params: any) { + return request.get({ + url: '/admin/targets/statistics/user/month', + params + }) + } + //年度-个人年度目标完成度(百分比) + static statisticspercentage(params: any) { + return request.get({ + url: '/admin/targets/statistics/user/progressh', + params + }) + } + //年度-个人累积完成数量(辆) + static statisticscumulative(params: any) { + return request.get({ + url: '/admin/targets/statistics/user/achieved', + params + }) + } +} diff --git a/src/api/adminApi.ts b/src/api/adminApi.ts index d33b476..320bfdf 100644 --- a/src/api/adminApi.ts +++ b/src/api/adminApi.ts @@ -4,37 +4,43 @@ export class manageService { //仪表盘 总 static statisticswhole() { return request.get({ - url: '/admin/inquiry/statistics/whole' + // url: '/admin/inquiry/statistics/whole' + url: 'admin/crm/statistics/whole' }) } //各渠道统计 static channelcount() { return request.get({ - url: '/admin/inquiry/statistics/channel/count' + // url: '/admin/inquiry/statistics/channel/count' + url: '/admin/crm/statistics/channel/count' }) } //各国家统计 static countrycount() { return request.get({ - url: '/admin/inquiry/statistics/country/count' + // url: '/admin/inquiry/statistics/country/count' + url: '/admin/crm/statistics/country/count' }) } //跟单人统计 static trackcount() { return request.get({ - url: '/admin/inquiry/statistics/track/count' + // url: '/admin/inquiry/statistics/track/count' + url: '/admin/crm/statistics/track/count' }) } //每日录单统计 static daycount() { return request.get({ - url: '/admin/inquiry/statistics/day/count' + // url: '/admin/inquiry/statistics/day/count' + url: '/admin/crm/statistics/day/count' }) } //每日录单统计 static inquirycount(params: any) { return request.get({ - url: '/admin/inquiry/statistics/inquiry/count', + // url: '/admin/inquiry/statistics/inquiry/count', + url: '/admin/crm/statistics/customer/count', params }) } @@ -198,4 +204,105 @@ export class manageService { // showErrorMessage: false // 不显示错误消息 }) } + //回收站 恢复 + static customerRestore(params: any) { + return request.get({ + url: '/admin/crm/customer/restore', + params + }) + } + //备货单审核列表 + static getstocklist(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/approval/list', + params + }) + } + //备货单审核 + static stockapproval(params: any) { + return request.post({ + url: '/admin/approval/store', + params + // showErrorMessage: false // 不显示错误消息 + }) + } + // 审核更新 + static stockapprovalupdate(params: any) { + return request.post({ + url: '/admin/approval/update', + params + // showErrorMessage: false // 不显示错误消息 + }) + } + //审核详情 + static stockapprovaldetail(params: any) { + return request.get({ + url: '/admin/approval/show', + params + }) + } + //审核删除 + static stockapprovaldelete(params: any) { + return request.get({ + url: '/admin/approval/destroy', + params + }) + } + //审批记录 + static stockapprovalrecord(params: any) { + return request.get({ + url: '/admin/approval/records/list', + params + }) + } + //审批通过/拒绝 + static stockapprovalpass(params: any) { + return request.post({ + url: '/admin/approval/records/action', + params + }) + } + //审批记录 + static stockapprovallogs(params: any) { + return request.get({ + url: '/admin/approval/records/logs', + params + }) + } + //备货单 + static getStockList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/stock/order/list', + params + }) + } + //添加备货单 + static addStock(params: any) { + return request.post({ + url: '/admin/stock/order/store', + params + // showErrorMessage: false // 不显示错误消息 + }) + } + //详情备货单 + static getStockDetail(params: any) { + return request.get({ + url: '/admin/stock/order/show', + params + }) + } + //备货单查看 + static checkStock(params: any) { + return request.get({ + url: '/admin/stock/order/detail', + params + }) + } + //打印 + static printStock(params: any) { + return request.get({ + url: '/admin/stock/order/print', + params + }) + } } diff --git a/src/api/commonApi.ts b/src/api/commonApi.ts new file mode 100644 index 0000000..2c812fe --- /dev/null +++ b/src/api/commonApi.ts @@ -0,0 +1,135 @@ +import request from '@/utils/http' + +export class commonService { + //通知列表 + static getNoticeList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/common/notice/list', + params + }) + } + //通知详情 + static getNoticeDetail(params: any) { + return request.get({ + url: '/common/notice/show', + params + }) + } + //通知已读 + static readNotice(params: any) { + return request.post({ + url: '/common/notice/read', + params + }) + } + //通知删除 + static delNotice(params: any) { + return request.post({ + url: '/common/notice/destroy', + params + }) + } + //通知未读列表 + static getUnReadNoticeList() { + return request.get({ + url: '/common/notice/unread' + }) + } + //货币下拉列表 + static getCurrencySelect() { + return request.get({ + url: '/common/preset/currency' + }) + } + + //国家下拉列表 + static getCountrySelect() { + return request.get({ + url: '/crm/customer/getCountry' + }) + } + //品牌下拉列表 + static getBrandSelect() { + return request.get({ + url: '/common/preset/brand' + }) + } + //车系下拉列表 + static getSeriesSelect(params: any) { + return request.get({ + url: '/common/preset/brand/series', + params + }) + } + //港口下拉列表 + static getPortSelect(params: any) { + return request.get({ + url: '/common/preset/port', + params + }) + } + //颜色下拉列表 + static getColorSelect(params: any) { + return request.get({ + url: '/common/preset/color', + params + }) + } + //改装下拉列表 + static getModificationSelect() { + return request.get({ + url: '/common/preset/modification' + }) + } + //字典下拉框 + static getDictSelect(params: any) { + return request.get({ + url: '/admin/dict/scope', + params + }) + } + //字典项下拉框 + static getDictItemSelect(params: any) { + return request.get({ + url: '/common/preset/dictItem', + params + }) + } + //字典分类下拉框 + static getDictTypeSelect() { + return request.get({ + url: '/admin/dict/group' + }) + } + //账户下拉框 + static getAccountSelect() { + return request.get({ + url: '/common/preset/account' + }) + } + //名下客户下拉框 + static getCustomerSelect() { + return request.get({ + url: '/crm/customer/preset' + }) + } + //角色下拉框 + static getRoleSelect() { + return request.get({ + url: '/common/preset/roles' + }) + } + //用户下拉框 + static getUserSelect() { + return request.get({ + url: '/common/preset/users' + }) + } + //签名 编辑 + static signatureEdit(params: any) { + return request.post({ + url: '/admin/user/update/sign', + params + }) + } +} diff --git a/src/api/crmApi.ts b/src/api/crmApi.ts index 3727eed..3843245 100644 --- a/src/api/crmApi.ts +++ b/src/api/crmApi.ts @@ -55,7 +55,7 @@ export class CrmService { } //国家下拉 static getCountry() { - return request.get({ + return request.get({ url: '/crm/customer/getCountry' }) } @@ -191,4 +191,98 @@ export class CrmService { // showErrorMessage: false // 不显示错误消息 }) } + //客户删除 + static crmCustomerDelete(params: any) { + return request.get({ + url: '/crm/customer/delete', + params + // showErrorMessage: false // 不显示错误消息 + }) + } + //通知客户重复 + static repeatCustomer(params: any) { + return request.post({ + url: '/crm/customer/distinctNotice', + params + // showErrorMessage: false // 不显示错误消息 + }) + } + //备货单 + static getStockList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/crm/stock/order/list', + params + }) + } + //添加备货单 + static addStock(params: any) { + return request.post({ + url: '/crm/stock/order/store', + params + // showErrorMessage: false // 不显示错误消息 + }) + } + //详情备货单 + static getStockDetail(params: any) { + return request.get({ + url: '/crm/stock/order/show', + params + }) + } + //编辑备货单 + static editStock(params: any) { + return request.post({ + url: '/crm/stock/order/update', + params + }) + } + //备货单查看 + static checkStock(params: any) { + return request.get({ + url: '/crm/stock/order/detail', + params + }) + } + //提交备货单 + static submitStock(params: any) { + return request.post({ + url: '/crm/stock/order/approval', + params + }) + } + //打印 + static printStock(params: any) { + return request.get({ + url: '/crm/stock/order/print', + params + }) + } + //审批记录 + static stockapprovalrecord(params: any) { + return request.get({ + url: '/crm/approval/records/list', + params + }) + } + //审批通过/拒绝 + static stockapprovalpass(params: any) { + return request.post({ + url: '/crm/approval/records/action', + params + }) + } + //审批记录 + static stockapprovallogs(params: any) { + return request.get({ + url: '/crm/approval/records/logs', + params + }) + } + //终极备货单 提交 + static stockultimateapproval(params: any) { + return request.post({ + url: '/crm/stock/order/ultimate/approval', + params + }) + } } diff --git a/src/api/crmpc/vehicleApi.ts b/src/api/crmpc/vehicleApi.ts new file mode 100644 index 0000000..dc314b8 --- /dev/null +++ b/src/api/crmpc/vehicleApi.ts @@ -0,0 +1,98 @@ +import request from '@/utils/http' + +export class vehicleService { + // 颜色管理 + static getColorList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/crm/color/list', + params + }) + } + static colorAdd(params: any) { + return request.post({ + url: '/crm/color/store', + params + }) + } + static colorDetail(params: any) { + return request.get({ + url: '/crm/color/show', + params + }) + } + static colorEdit(params: any) { + return request.post({ + url: '/crm/color/update', + params + }) + } + static colorDelete(params: any) { + return request.post({ + url: '/crm/color/destroy', + params + }) + } + + // 车辆列表 + static getVehicleList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/crm/vehicle/sku/list', + params + }) + } + static vehicleAdd(params: any) { + return request.post({ + url: '/crm/vehicle/sku/store', + params + }) + } + static vehicleDetail(params: any) { + return request.get({ + url: '/crm/vehicle/sku/show', + params + }) + } + static vehicleEdit(params: any) { + return request.post({ + url: '/crm/vehicle/sku/update', + params + }) + } + static vehicleDelete(params: any) { + return request.post({ + url: '/crm/vehicle/sku/destroy', + params + }) + } + // 改装管理 + static getModificationList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/crm/modification/list', + params + }) + } + static modificationAdd(params: any) { + return request.post({ + url: '/crm/modification/store', + params + }) + } + static modificationDetail(params: any) { + return request.get({ + url: '/crm/modification/show', + params + }) + } + static modificationEdit(params: any) { + return request.post({ + url: '/crm/modification/update', + params + }) + } + static modificationDelete(params: any) { + return request.post({ + url: '/crm/modification/destroy', + params + }) + } +} diff --git a/src/api/financeApi.ts b/src/api/financeApi.ts new file mode 100644 index 0000000..6229c2b --- /dev/null +++ b/src/api/financeApi.ts @@ -0,0 +1,74 @@ +import request from '@/utils/http' + +export class financeService { + // 货币列表 + static getCurrencyList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/currency/list', + params + }) + } + // 货币添加 + static currencyAdd(params: any) { + return request.post({ + url: '/admin/currency/store', + params + }) + } + // 货币详情 + static currencyDetail(params: any) { + return request.get({ + url: '/admin/currency/show', + params + }) + } + // 货币编辑 + static currencyEdit(params: any) { + return request.post({ + url: '/admin/currency/update', + params + }) + } + // 货币删除 + static currencyDelete(params: any) { + return request.post({ + url: '/admin/currency/destroy', + params + }) + } + // 收款账户列表 + static getAccountList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/account/list', + params + }) + } + // 收款账户添加 + static accountAdd(params: any) { + return request.post({ + url: '/admin/account/store', + params + }) + } + // 收款账户详情 + static accountDetail(params: any) { + return request.get({ + url: '/admin/account/show', + params + }) + } + // 收款账户编辑 + static accountEdit(params: any) { + return request.post({ + url: '/admin/account/update', + params + }) + } + // 收款账户删除 + static accountDelete(params: any) { + return request.post({ + url: '/admin/account/destroy', + params + }) + } +} diff --git a/src/api/menuApi.ts b/src/api/menuApi.ts index ab614f8..03889fc 100644 --- a/src/api/menuApi.ts +++ b/src/api/menuApi.ts @@ -23,7 +23,7 @@ export const menuService = { ElMessage.error('暂无系统菜单权限') return { menuList: [] } } - console.log('menuData', menuData) + // console.log('menuData', menuData) // 处理菜单数据 const menuList = menuData.map((route: any) => menuDataToRouter(route)) // 验证处理后的菜单列表 diff --git a/src/api/systemApi.ts b/src/api/systemApi.ts index 5284590..458e369 100644 --- a/src/api/systemApi.ts +++ b/src/api/systemApi.ts @@ -217,4 +217,97 @@ export class systemService { params }) } + //通知列表 + static getNoticeList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/notice/list', + params + }) + } + //通知详情 + static getNoticeDetail(params: any) { + return request.get({ + url: '/admin/notice/show', + params + }) + } + //通知删除 + static delNotice(params: any) { + return request.post({ + url: '/admin/notice/destroy', + params + }) + } + //通知推送 + static sendNotice(params: any) { + return request.post({ + url: '/admin/notice/push', + params + }) + } + // 字典类型列表 + static getDictTypeList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/dict/list', + params + }) + } + // 字典类型新增/详情/编辑/删除 + static dictTypeAdd(params: any) { + return request.post({ + url: '/admin/dict/store', + params + }) + } + static dictTypeDetail(params: any) { + return request.get({ + url: '/admin/dict/show', + params + }) + } + static dictTypeEdit(params: any) { + return request.post({ + url: '/admin/dict/update', + params + }) + } + static dictTypeDelete(params: any) { + return request.post({ + url: '/admin/dict/destroy', + params + }) + } + + // 字典项列表 + static getDictItemList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/dict/item/list', + params + }) + } + // 字典项新增/详情/编辑/删除 + static dictItemAdd(params: any) { + return request.post({ + url: '/admin/dict/item/store', + params + }) + } + static dictItemDetail(params: any) { + return request.get({ + url: '/admin/dict/item/show', + params + }) + } + static dictItemEdit(params: any) { + return request.post({ + url: '/admin/dict/item/update', + params + }) + } + static dictItemDelete(params: any) { + return request.post({ + url: '/admin/dict/item/destroy', + params + }) + } } diff --git a/src/api/usersApi.ts b/src/api/usersApi.ts index 04929a1..62b9c20 100644 --- a/src/api/usersApi.ts +++ b/src/api/usersApi.ts @@ -85,4 +85,19 @@ export class UserService { params }) } + //离职分配 + static leaveUser(params: any) { + return request.get({ + url: '/admin/crm/customer/allocation', + params + }) + } + //客户删除 + static crmCustomerDelete(params: any) { + return request.get({ + url: '/admin/crm/customer/delete', + params + // showErrorMessage: false // 不显示错误消息 + }) + } } diff --git a/src/api/vehicleApi.ts b/src/api/vehicleApi.ts new file mode 100644 index 0000000..9853219 --- /dev/null +++ b/src/api/vehicleApi.ts @@ -0,0 +1,196 @@ +import request from '@/utils/http' + +export class vehicleService { + // 车辆品牌列表 + static getBrandList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/brand/list', + params + }) + } + // 车辆品牌新增/详情/编辑/删除 + static brandAdd(params: any) { + return request.post({ + url: '/admin/brand/store', + params + }) + } + static brandDetail(params: any) { + return request.get({ + url: '/admin/brand/show', + params + }) + } + static brandEdit(params: any) { + return request.post({ + url: '/admin/brand/update', + params + }) + } + static brandDelete(params: any) { + return request.post({ + url: '/admin/brand/destroy', + params + }) + } + + // 车辆系列列表 + static getSeriesList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/brand/series/list', + params + }) + } + // 车辆系列新增/详情/编辑/删除 + static seriesAdd(params: any) { + return request.post({ + url: '/admin/brand/series/store', + params + }) + } + static seriesDetail(params: any) { + return request.get({ + url: '/admin/brand/series/show', + params + }) + } + static seriesEdit(params: any) { + return request.post({ + url: '/admin/brand/series/update', + params + }) + } + static seriesDelete(params: any) { + return request.post({ + url: '/admin/brand/series/destroy', + params + }) + } + + // 颜色管理 + static getColorList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/color/list', + params + }) + } + static colorAdd(params: any) { + return request.post({ + url: '/admin/color/store', + params + }) + } + static colorDetail(params: any) { + return request.get({ + url: '/admin/color/show', + params + }) + } + static colorEdit(params: any) { + return request.post({ + url: '/admin/color/update', + params + }) + } + static colorDelete(params: any) { + return request.post({ + url: '/admin/color/destroy', + params + }) + } + + // 改装管理 + static getModificationList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/modification/list', + params + }) + } + static modificationAdd(params: any) { + return request.post({ + url: '/admin/modification/store', + params + }) + } + static modificationDetail(params: any) { + return request.get({ + url: '/admin/modification/show', + params + }) + } + static modificationEdit(params: any) { + return request.post({ + url: '/admin/modification/update', + params + }) + } + static modificationDelete(params: any) { + return request.post({ + url: '/admin/modification/destroy', + params + }) + } + + // 港口管理 + static getPortList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/port/list', + params + }) + } + static portAdd(params: any) { + return request.post({ + url: '/admin/port/store', + params + }) + } + static portDetail(params: any) { + return request.get({ + url: '/admin/port/show', + params + }) + } + static portEdit(params: any) { + return request.post({ + url: '/admin/port/update', + params + }) + } + static portDelete(params: any) { + return request.post({ + url: '/admin/port/destroy', + params + }) + } + // 车辆列表 + static getVehicleList(params: Api.Common.PaginatingParams & any) { + return request.get({ + url: '/admin/vehicle/sku/list', + params + }) + } + static vehicleAdd(params: any) { + return request.post({ + url: '/admin/vehicle/sku/store', + params + }) + } + static vehicleDetail(params: any) { + return request.get({ + url: '/admin/vehicle/sku/show', + params + }) + } + static vehicleEdit(params: any) { + return request.post({ + url: '/admin/vehicle/sku/update', + params + }) + } + static vehicleDelete(params: any) { + return request.post({ + url: '/admin/vehicle/sku/destroy', + params + }) + } +} diff --git a/src/assets/img/common/logox.png b/src/assets/img/common/logox.png new file mode 100644 index 0000000000000000000000000000000000000000..8bba14d85313c4985102f8f7d8720363169e29f4 GIT binary patch literal 26170 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�zOTDVTCae2fIkUHzhIgX_*!2>zDBJGjnj{F$(2rg6NO?SzIW` zdnT?}tiG}4$&Y+;Ic5m0xKq07p4x_*j&{Am#FI|2L4uz(48+;zmRO&T?jp52?_d*! zy!koP%*@OS%SvV2F~T(?{Hg+Hl8KY%o|9r;spR~?LmYtv0xUO;PF}s0jO&mt$$?!p z#Z2d>*S7C%geZ{^_!#HWEqp>F#=MgqY1q!Q?`@sj$ZV8c#n4c4e2|(#dIPP#^>U z2~g&A8qwEs=o#oI=e zL7VFoLiE$6k0t6bZ9E&u6{#8Z>6$?j?04w9V{>VgWm3_VDrA)5F&k;d3}Y6q&UV-4 zYA>kb`RZgHJURiiXQ81aF`#-oLQZ3IX6=D;GmOa`&$VCOvR3 zuy9!B4jPWfZ~CP<(%TrSc%nD7CraJJJWh;PUxxHS%acOgNn79ds{Z6yQFE;h<8nV6 zFMD0s{>D;&`}@Sg)fXqV>@GYOK1^4vN?SWv)<^pXW1Hz0k2p-(XGH3gL_ z7Nmf{{GUf++-~C#x0J}m`&Btslqj}ekRVPjl<*>z#D| z;o)I~-E>~r)z#HX#xX-2^B>CRE6Ub0$}Xeq5#^unHqN(|QMT~4X5o=w1XQFg{$rDQ zzD^t+9R( zzsD~ub?lZ^m5)dm3dpaGT~eJiAmH;#O20*ulas$^)nFl?lz_XC5d4t z7^tbsP%D?}ZE$yY#|^7DufG7{ej%YS{eJ(GPD8GB57M618E-W)gluZHny$IrIR3__ zSfJiVB>WO-mFkmV8g0sb7vUNP4O^(w(nGU|kB-9(34$QJP$9u6a3|DtTC4@bpl5cW zl95;o7V+WDi9KTQD@1E+YjY$RgVjRF3G{_YFpH=Ki->S7GFEHh?CdP0(b8=+8Y3GF z7>jTrRQ4gW$r8|r!Lg&q$H(T<(o%;kGZx{(pRoXYaO8qj@R(s8`wm$E*A|cF@JOf7StVl+&dlarH<2=@)3 zp_wjVp3Re^xYnR8tF5%?U_6dl6KlaRiSX6PR}p$KCWG+!ccBYECxcEjH#avyo};K* ztp*dYjf-#<&?em%FML$H&KCAZRKA{^YS)6J==7!rR;1 ztsn?uU0fzg(fwr|-0gOGD2mENX-qK|@YU578Da}#hx2Gy8#bpYyk|Ri7HWoVQlo>i z_`{mKfA^gTsF^w$26sYRr~RaJmjt8uN$0L$ma;g@Bc3CNeW@nIIInYg`q9M1#GIOM z&_OHD*-rXz(>N{Mlwf0czL(1aJ~%jt1M#>Gb5<>630A`b#%`M&gncT?uQKbgbC);N zh6Qp6Em)rLtz%wQI^;A2+)B(?bP_-5+@=2p2%X^m{{Bx0bNAulfp4>2&g=Dh0XuRa z;5l9w0e0#vIgHCXw*>*WGVxj{h?QpkE&hS}TOo7JnJh_zrC-}gBrEPiB; zSPK@B~{r*?| zu3D{bkx&>84bW-yeA`~FR@)(^8hA#bdVH5KBuXV{B5WN`P3TBY-e^HRv`82VmZO?0 z7pccEMlU7;YHSO;xYn{WCp|pF`gqW@9id`&H#RnQ()k!+-Wjy0#A{2gpzJw6L@2K; z+B`w-`p=@CTb;)yNcfU*Ovs~@_*5bzEO#`qVo>nP~Tl --> + +
+
+ + +
+
-
+
{{ isFullscreen ? '' : '' }}
- -
-
+
{{ isDark ? '' : '' }}
@@ -247,6 +253,10 @@ const showNotice = ref(false) const notice = ref(null) const userMenuPopover = ref() + // 控制铃铛是否晃动 + const isBellShaking = ref(false) + // 轮询定时器句柄,防止重复创建 + let notificationPollTimer: number | undefined const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT) const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU) @@ -268,10 +278,20 @@ onMounted(() => { initLanguage() document.addEventListener('click', bodyCloseNotice) + // 监听通知数据变化,控制铃铛晃动 + watchNotificationData() }) onUnmounted(() => { document.removeEventListener('click', bodyCloseNotice) + // 清理事件监听器 + mittBus.off('notification-data-changed') + mittBus.off('ws:message') + // 清理通知轮询 + if (notificationPollTimer) { + clearInterval(notificationPollTimer) + notificationPollTimer = undefined + } }) const { isFullscreen, toggle: toggleFullscreen } = useFullscreen() @@ -372,8 +392,6 @@ if (showSettingGuide.value) { settingStore.hideSettingGuide() } - // 打开设置引导 - // settingStore.openSettingGuide() } const openSearchDialog = () => { @@ -411,9 +429,72 @@ userMenuPopover.value.hide() }, 100) } + + // 监听通知数据变化,控制铃铛晃动 + const watchNotificationData = () => { + // 定期检查通知数据 + // const checkNotificationData = async () => { + // try { + // const { commonService } = await import('@/api/commonApi') + // const res = await commonService.getUnReadNoticeList() + // const noticeCount = res?.list?.length || 0 + // isBellShaking.value = noticeCount > 0 + // } catch (error) { + // console.error('获取通知数据失败:', error) + // isBellShaking.value = false + // } + // } + // // 立即检查一次 + // checkNotificationData() + // // 每30秒检查一次通知数据,先清理旧定时器,避免重复 + // if (notificationPollTimer) { + // clearInterval(notificationPollTimer) + // notificationPollTimer = undefined + // } + // notificationPollTimer = window.setInterval(checkNotificationData, 30000) + // 监听通知数据变化事件 + mittBus.on('notification-data-changed', (count: unknown) => { + isBellShaking.value = typeof count === 'number' ? count > 0 : false + }) + // 监听WebSocket消息,处理实时通知 + mittBus.on('ws:message', (data: any) => { + if ( + data && + data.event && + (data.event.includes('notice') || + data.event.includes('notification') || + data.event.includes('message')) + ) { + isBellShaking.value = true + } + }) + } diff --git a/src/components/core/layouts/art-notification/index.vue b/src/components/core/layouts/art-notification/index.vue index 25ad045..41773c6 100644 --- a/src/components/core/layouts/art-notification/index.vue +++ b/src/components/core/layouts/art-notification/index.vue @@ -9,43 +9,46 @@ @click.stop="" >
- {{ $t('notice.title') }} - {{ $t('notice.btnRead') }} + + 消息 + {{ $t('notice.btnRead') }}
  • - {{ item.name }} ({{ item.num }}) + {{ item.name }} ({{ noticeList.length }})
-
    -
  • +
      + +
    • -

      {{ item.title }}

      -

      {{ item.time }}

      +

      {{ item.message.title }}

      +

      {{ item.message.created_at }}

    -
      + -
        + -
        +

        {{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}

        -
        +
        @@ -86,6 +89,29 @@
        + + + +
        +
        +
        +
        发送人
        +
        {{ noticeDetail.sender_name || '-' }}
        +
        +
        +
        通知标题
        +
        {{ noticeDetail.title || '-' }}
        +
        +
        +
        +
        通知内容
        +
        +
        +
        +
        +
        +
        +
        diff --git a/src/components/core/layouts/art-page-content/index.vue b/src/components/core/layouts/art-page-content/index.vue index f51e576..1ecf8c2 100644 --- a/src/components/core/layouts/art-page-content/index.vue +++ b/src/components/core/layouts/art-page-content/index.vue @@ -6,7 +6,7 @@ @@ -16,8 +16,9 @@ + - + @@ -37,6 +38,7 @@ const { pageTransition, containerWidth, refresh } = storeToRefs(useSettingStore()) const { keepAliveExclude } = storeToRefs(useWorktabStore()) + const { containerMinHeight } = useCommon() const containerStyle = computed(() => { diff --git a/src/components/core/tables/ArtTableHeader.vue b/src/components/core/tables/ArtTableHeader.vue index 74e235d..c977247 100644 --- a/src/components/core/tables/ArtTableHeader.vue +++ b/src/components/core/tables/ArtTableHeader.vue @@ -4,6 +4,7 @@
        +
        @@ -68,7 +69,6 @@ }}
        -
diff --git a/src/components/custom/OrderDetailContent.vue b/src/components/custom/OrderDetailContent.vue new file mode 100644 index 0000000..7ef4121 --- /dev/null +++ b/src/components/custom/OrderDetailContent.vue @@ -0,0 +1,419 @@ + + + + + diff --git a/src/composables/useOrderDetailDialog.ts b/src/composables/useOrderDetailDialog.ts new file mode 100644 index 0000000..1773613 --- /dev/null +++ b/src/composables/useOrderDetailDialog.ts @@ -0,0 +1,73 @@ +import { ref, reactive } from 'vue' + +export interface OrderItem { + model?: string + quantity?: number + exteriorColor?: string + interiorColor?: string + modification?: string + unitPrice?: number + totalPrice?: number + officialPrice?: number + discount?: string +} + +export interface PaymentItem { + country?: string + date?: string + currency?: string + account?: string + remitter?: string + amount?: number +} + +export interface OrderData { + orderNo?: string + salesperson?: string + piNo?: string + date?: string + clearanceCountry?: string + customerSource?: string + customerName?: string + customerCountry?: string + items?: OrderItem[] + contractRate?: string + totalCif?: number + declarant?: string + etaShipDate?: string + shippingMethod?: string + domesticPort?: string + tradeTerms?: string + latestArrival?: string + payments?: PaymentItem[] +} + +export function useOrderDetailDialog() { + const visible = ref(false) + const title = ref('订单详情') + const orderData = ref({}) + + const show = (data: OrderData, dialogTitle?: string) => { + orderData.value = data + title.value = dialogTitle || '订单详情' + visible.value = true + } + + const hide = () => { + visible.value = false + } + + const print = () => { + // 可以在这里添加打印逻辑 + window.print() + } + + return { + visible, + title, + orderData, + show, + hide, + print + } +} diff --git a/src/composables/usePdfPreview.ts b/src/composables/usePdfPreview.ts new file mode 100644 index 0000000..61f151f --- /dev/null +++ b/src/composables/usePdfPreview.ts @@ -0,0 +1,140 @@ +import { ref, reactive } from 'vue' + +export const usePdfPreview = () => { + const state = reactive({ + visible: false, + url: '', + name: '', + blobUrl: '', + images: [] as string[] + }) + const frameRef = ref(null) + + // 动态加载 pdf.js(优先本地依赖,失败回退 CDN) + const loadPdfJs = async (): Promise => { + // 统一使用 CDN,避免本地依赖缺失导致打包时报错 + const ensure = (src: string) => + new Promise((resolve, reject) => { + const s = document.createElement('script') + s.src = src + s.onload = () => resolve() + s.onerror = () => reject(new Error('load script failed')) + document.head.appendChild(s) + }) + await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js') + await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js') + return (window as any).pdfjsLib + } + + const open = (url: string, name = '') => { + if (!url) return + state.url = url + state.name = name + state.visible = true + state.images = [] + } + + const openWithResponse = (res: any) => { + const url = res?.data?.file_url_full || res?.file_url_full + const name = res?.data?.file_url || res?.file_url || '' + open(url, name) + } + + const inferFilenameFromUrl = (url: string): string => { + try { + const u = new URL(url) + const parts = u.pathname.split('/') + return parts[parts.length - 1] || 'document.pdf' + } catch (e) { + const parts = (url || '').split('?')[0].split('/') + return parts[parts.length - 1] || 'document.pdf' + } + } + + const download = async () => { + if (!state.url) return + const filename = state.name || inferFilenameFromUrl(state.url) + try { + const resp = await fetch(state.url, { credentials: 'omit' }) + const blob = await resp.blob() + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + } catch { + window.open(state.url, '_blank') + } + } + + // 预览时也用 blob,提升兼容性 + const preparePreview = async () => { + if (!state.url) return + try { + const resp = await fetch(state.url, { credentials: 'omit' }) + const blob = await resp.blob() + state.blobUrl && window.URL.revokeObjectURL(state.blobUrl) + state.blobUrl = window.URL.createObjectURL(blob) + } catch { + state.blobUrl = '' + } + } + + // 生成图片以便无需浏览器 PDF 插件即可展示 + const prepareImages = async () => { + if (!state.url) return + state.images = [] + try { + const resp = await fetch(state.url, { credentials: 'omit' }) + const arrayBuffer = await resp.arrayBuffer() + const pdfjsLib = await loadPdfJs() + const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise + const images: string[] = [] + for (let p = 1; p <= pdf.numPages; p++) { + const page = await pdf.getPage(p) + const viewport = page.getViewport({ scale: 1.5 }) + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d') + if (!context) continue + canvas.width = viewport.width + canvas.height = viewport.height + await page.render({ canvasContext: context, viewport }).promise + images.push(canvas.toDataURL('image/png')) + } + state.images = images + } catch { + state.images = [] + } + } + + const print = () => { + const frame = frameRef.value + if (frame && frame.contentWindow) { + frame.contentWindow.focus() + frame.contentWindow.print() + return + } + if (state.url) { + const win = window.open(state.url, '_blank') + if (win) { + win.addEventListener('load', () => win.print()) + } + } + } + + return { + pdf: state, + frameRef, + open, + openWithResponse, + download, + print, + preparePreview, + prepareImages + } +} + + diff --git a/src/composables/useStockingForm.ts b/src/composables/useStockingForm.ts new file mode 100644 index 0000000..15ffb52 --- /dev/null +++ b/src/composables/useStockingForm.ts @@ -0,0 +1,968 @@ +import { ref, reactive, watch, onMounted } from 'vue' +import { ElMessage } from 'element-plus' +import { CrmService } from '@/api/crmApi' +import { commonService } from '@/api/commonApi' +import { vehicleService } from '@/api/vehicleApi' +import { useUploadHeaders } from '@/composables/useUploadHeaders' + +// 类型定义 +export interface BaseForm { + pi_no: string + biz_date: string + clearance_country_id: string + clearance_country_snapshot: string + customer_source_id: string + customer_source_snapshot: string + customer_id: string + customer_snapshot: string + customer_country_id: string + customer_country_snapshot: string + declarant_name: string + eta_ship_date: string + shipping_method_id: string + shipping_method_snapshot: string + domestic_port_id: string + domestic_port_snapshot: string + trade_term_id: string + trade_term_snapshot: string + clearance_port_id: string + clearance_port_snapshot: string + latest_arrival_date: string + pick_up_date: string + collect_all_date: string + contract_exchange_rate: string + settlement_currency: string + remark: string + total_cif: number | undefined + total_cif_currency: string + total_cif_currency_snapshot: string + is_final: string + object_id: string | number + contract: string + contract_url?: string +} + +export interface ItemRow { + sku_id: string + vin?: string + _echo_vin?: boolean + mileage?: number + listing_price?: number | undefined + listing_price_currency: string + listing_price_currency_snapshot: string + reduction_price?: number | undefined + reduction_price_currency: string + reduction_price_currency_snapshot: string + model_snapshot: string + color_interior_code: string + color_interior_snapshot: string + color_exterior_code: string + color_exterior_snapshot: string + unit_price: number | undefined + handling_fee: string + fob_transport_fee: number | undefined + port_misc_fee: string + ocean_freight: string + ocean_freight_currency: string + ocean_freight_currency_snapshot: string + retrofit_fee: string + total_price: number | undefined + official_price: number | undefined + discount_price: number | undefined + fob_fee?: number | undefined + fob_fee_currency: string + fob_fee_currency_snapshot: string + helper_fee?: number | undefined + helper_fee_currency: string + helper_fee_currency_snapshot: string + domestic_freight?: number | undefined + domestic_freight_currency: string + domestic_freight_currency_snapshot: string + port_fee?: number | undefined + port_fee_currency: string + port_fee_currency_snapshot: string + unit_price_currency: string + unit_price_currency_snapshot: string + total_price_currency: string + total_price_currency_snapshot: string + guide_price_currency: string + guide_price_currency_snapshot: string + discount?: number | undefined + discount_currency: string + discount_currency_snapshot: string + contract_rate?: number | undefined + profit: number | undefined + profit_type_snapshot?: string + modify_ids: string[] + modify_list: Array<{ option_id: string; option_name_snapshot: string }> + modify_fee_currency: string + modify_fee_currency_snapshot: string +} + +export interface PaymentItem { + remit_country_id: string + remit_country_name_snapshot: string + remit_date: string + currency_code: string + currency_name_snapshot: string + account_id: string + account_name_snapshot: string + remitter_name: string + amount: number | undefined +} + +export interface DialogState { + visible: boolean + search: { keyword: string } + loading: boolean + tableData: any[] + pagination: { currentPage: number; pageSize: number; total: number } + targetIndex?: number +} + +export function useStockingForm(isFinal = false, stockingId?: string | number) { + const { uploadHeaders } = useUploadHeaders() + const actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=stock_orders` + + // 选项数据 + const channelOptions = ref() + const countryOptions = ref() + const modificationOptions = ref() + const currencyOptions = ref() + const accountOptions = ref() + const customerOptions = ref() + const portOptions = ref() + const portOptionsall = ref() + const wcolorOptions = ref>([]) + const ncolorOptions = ref>([]) + const transportOptions = ref>([]) + const trade_termsOptions = ref>([]) + + // 表单数据 + const baseForm = reactive({ + pi_no: '', + biz_date: '', + clearance_country_id: '', + clearance_country_snapshot: '', + customer_source_id: '', + customer_source_snapshot: '', + customer_id: '', + customer_snapshot: '', + customer_country_id: '', + customer_country_snapshot: '', + declarant_name: '', + eta_ship_date: '', + shipping_method_id: '', + shipping_method_snapshot: '', + domestic_port_id: '', + domestic_port_snapshot: '', + trade_term_id: '', + trade_term_snapshot: '', + clearance_port_id: '', + clearance_port_snapshot: '', + latest_arrival_date: '', + contract_exchange_rate: '', + settlement_currency: 'CNY', + remark: '', + total_cif: undefined, + total_cif_currency: 'CNY', + total_cif_currency_snapshot: '', + is_final: isFinal ? '1' : '0', + object_id: stockingId || '0', + contract: '', + contract_url: '', + pick_up_date: '', + collect_all_date: '' + }) + + // 创建新的订单项 + const newItem = (): ItemRow => ({ + sku_id: '', + model_snapshot: '', + _echo_vin: false, + mileage: 0, + listing_price: undefined, + listing_price_currency: 'CNY', + listing_price_currency_snapshot: '人民币', + reduction_price: undefined, + reduction_price_currency: 'CNY', + reduction_price_currency_snapshot: '人民币', + color_interior_code: '', + color_interior_snapshot: '', + color_exterior_code: '', + color_exterior_snapshot: '', + unit_price: undefined, + handling_fee: '', + fob_transport_fee: undefined, + port_misc_fee: '', + ocean_freight: '', + ocean_freight_currency: 'CNY', + ocean_freight_currency_snapshot: '人民币', + retrofit_fee: '', + total_price: undefined, + official_price: undefined, + discount_price: undefined, + fob_fee: undefined, + fob_fee_currency: 'CNY', + fob_fee_currency_snapshot: '人民币', + helper_fee: undefined, + helper_fee_currency: 'CNY', + helper_fee_currency_snapshot: '人民币', + domestic_freight: undefined, + domestic_freight_currency: 'CNY', + domestic_freight_currency_snapshot: '人民币', + port_fee: undefined, + port_fee_currency: 'CNY', + port_fee_currency_snapshot: '人民币', + unit_price_currency: 'CNY', + unit_price_currency_snapshot: '人民币', + total_price_currency: 'CNY', + total_price_currency_snapshot: '人民币', + guide_price_currency: 'CNY', + guide_price_currency_snapshot: '人民币', + discount: undefined, + discount_currency: 'CNY', + discount_currency_snapshot: '人民币', + contract_rate: 7.16, + profit: undefined, + modify_ids: [], + modify_list: [], + modify_fee_currency: 'CNY', + modify_fee_currency_snapshot: '人民币' + }) + + // 创建新的收款项 + const newPaymentItem = (): PaymentItem => ({ + remit_country_id: '', + remit_country_name_snapshot: '', + remit_date: '', + currency_code: 'CNY', + currency_name_snapshot: '人民币', + account_id: '', + account_name_snapshot: '', + remitter_name: '', + amount: undefined + }) + + const items = ref([newItem()]) + const paymentItems = ref([newPaymentItem()]) + const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null) + + // 弹框状态 + const customerDialog = reactive({ + visible: false, + search: { keyword: '' }, + loading: false, + tableData: [], + pagination: { currentPage: 1, pageSize: 10, total: 0 } + }) + + const vehicleDialog = reactive({ + visible: false, + search: { keyword: '' }, + loading: false, + tableData: [], + pagination: { currentPage: 1, pageSize: 10, total: 0 }, + targetIndex: -1 + }) + + // 数据加载方法 + const getgetChannel = async () => { + const res = await CrmService.getChannel() + channelOptions.value = res || [] + } + + const getcustomer = async () => { + const res = await commonService.getCustomerSelect() + customerOptions.value = res || [] + } + + const getarea = async () => { + const res = await CrmService.getCountry() + res.forEach((item: any) => { + item.cn_addrs = item.cn_addr + '(' + item.en_addr + ')' + }) + // console.log(res) + countryOptions.value = res || [] + } + + const getmodification = async () => { + const res = await commonService.getModificationSelect() + modificationOptions.value = res || [] + } + + const loadColorOptions = async () => { + try { + const res = await commonService.getColorSelect({ type: 1 }) + wcolorOptions.value = res + const response = await commonService.getColorSelect({ type: 2 }) + ncolorOptions.value = response + } catch (error) { + console.error('获取颜色列表失败:', error) + ElMessage.error('获取颜色列表失败') + } + } + + const loadAccountOptions = async () => { + try { + const res = await commonService.getAccountSelect() + accountOptions.value = res + } catch (error) { + console.error('获取账户列表失败:', error) + ElMessage.error('获取账户列表失败') + } + } + + const loadCurrencyOptions = async () => { + try { + const res = await commonService.getCurrencySelect() + currencyOptions.value = res + } catch (error) { + console.error('获取币种列表失败:', error) + ElMessage.error('获取币种列表失败') + } + } + + const loadPortOptions = async () => { + try { + const res = await commonService.getPortSelect({ scene: 'domestic' }) + const resall = await commonService.getPortSelect({}) + portOptions.value = res + portOptionsall.value = resall + } catch (error) { + console.error('获取港口列表失败:', error) + ElMessage.error('获取港口列表失败') + } + } + + const loadDictOptions = async () => { + const response = await commonService.getDictSelect({ scope: '2,15' }) + transportOptions.value = response.transport + trade_termsOptions.value = response.trade_terms + } + + // 币种相关方法 + const getCurrencyRate = (code: string): string => { + try { + const found = (currencyOptions?.value || []).find((c: any) => c.currency_code === code) + return found?.rate || found?.exchange_rate || '' + } catch (e) { + return '' + } + } + + const getCurrencyBounds = (code: string): { min: number; max: number; base: number } => { + const found: any = (currencyOptions?.value || []).find((c: any) => c.currency_code === code) || {} + const base = Number(found?.rate || found?.exchange_rate || 0) || 0 + const up = Number(found?.float_percent_up) + const down = Number(found?.float_percent_down) + if (!Number.isNaN(up) || !Number.isNaN(down)) { + const upPct = (Number.isNaN(up) ? 0 : up) / 100 + const downPct = (Number.isNaN(down) ? 0 : down) / 100 + return { min: base * (1 - downPct), max: base * (1 + upPct), base } + } + return { min: base * 0.9, max: base * 1.1, base } + } + + const validateContractRate = () => { + const code = baseForm.settlement_currency || items.value[0]?.unit_price_currency || 'USD' + const { min, max } = getCurrencyBounds(code) + const val = Number(baseForm.contract_exchange_rate) + if (Number.isNaN(val)) return + if (val < min) { + baseForm.contract_exchange_rate = String(Number(min.toFixed(6))) + ElMessage?.warning?.('合同汇率已按币种浮动下限自动纠正') + } else if (val > max) { + baseForm.contract_exchange_rate = String(Number(max.toFixed(6))) + ElMessage?.warning?.('合同汇率已按币种浮动上限自动纠正') + } + } + + // 金额包装方法 + const wrapMoney = (amount: any, currency: string, remark: string) => ({ + currency: currency || '', + amount: amount === undefined || amount === null ? '' : String(amount), + rate: getCurrencyRate(currency), + remark + }) + + // 利润计算方法 + const getRateNumber = (code: string): number => { + const r = Number(getCurrencyRate(code)) + return Number.isNaN(r) ? 0 : r + } + + const convertCurrency = (amount: any, fromCode?: string, toCode?: string): number => { + const val = amount === undefined || amount === null || amount === '' ? 0 : Number(amount) + if (Number.isNaN(val)) return 0 + const from = fromCode || 'USD' + const to = toCode || 'USD' + if (from === to) return val + const rateFrom = getRateNumber(from) + const rateTo = getRateNumber(to) || 1 + if (!rateFrom) return 0 + return (val * rateFrom) / rateTo + } + + const calcProfit = (it: any): number => { + const targetCurrency = baseForm.settlement_currency || 'USD' + const unit = convertCurrency(it.unit_price, it.unit_price_currency, targetCurrency) + const retrofit = convertCurrency(it.retrofit_fee, (it as any).modify_fee_currency, targetCurrency) + const fob = convertCurrency( + it.fob_transport_fee ?? (it as any).fob_fee, + (it as any).fob_fee_currency, + targetCurrency + ) + const handling = convertCurrency( + it.handling_fee ?? (it as any).helper_fee, + (it as any).helper_fee_currency, + targetCurrency + ) + const port = convertCurrency( + it.port_misc_fee ?? (it as any).port_fee, + (it as any).port_fee_currency, + targetCurrency + ) + const ocean = convertCurrency( + it.ocean_freight ?? (it as any).ocean_freight, + (it as any).ocean_freight_currency, + targetCurrency + ) + const mileageVal = Number(it.mileage ?? 0) + if (mileageVal > 0) { + // 二手车:用挂牌价与二手减价替代官方指导价与优惠 + const listing = convertCurrency( + it.listing_price ?? (it as any).listing_price, + (it as any).listing_price_currency, + targetCurrency + ) + const reduction = convertCurrency( + it.reduction_price ?? (it as any).reduction_price, + (it as any).reduction_price_currency, + targetCurrency + ) + return unit - (retrofit + fob + listing + reduction + handling + port + ocean) + } + + // 新车/里程<=0:保持原有计算(官方指导价与优惠) + const guide = convertCurrency( + it.official_price ?? (it as any).guide_price, + (it as any).guide_price_currency, + targetCurrency + ) + const discount = convertCurrency( + it.discount_price ?? (it as any).discount, + (it as any).discount_currency, + targetCurrency + ) + return unit - (retrofit + fob + guide + discount + handling + port + ocean) + } + + // 合同文件相关方法 + const isImageFile = (file: { name: string; url: string; file_name: string }) => { + return /\.(jpg|jpeg|png|gif)$/i.test(file.name) || file.file_name.startsWith('image/') + } + + const beforeContractUpload = (file: File) => { + const isValidType = /\.(jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|txt)$/i.test(file.name) + if (!isValidType) { + ElMessage.error('只能上传图片或文档文件!') + return false + } + const isLt10M = file.size / 1024 / 1024 < 10 + if (!isLt10M) { + ElMessage.error('文件大小不能超过 10MB!') + return false + } + return true + } + + const handleContractUpload = (response: any) => { + if (response.code === 200) { + const fileInfo = { + name: response.data.original_name || '合同文件', + url: response.data.file_url, + file_name: response.data.file_name + } + contractFile.value = fileInfo + baseForm.contract = response.data.file_name + ElMessage.success('合同上传成功!') + } else { + ElMessage.error(response.message || '合同上传失败!') + } + } + + const removeContractFile = () => { + contractFile.value = null + baseForm.contract = '' + ElMessage.success('合同文件已删除') + } + + // 订单项操作方法 + const copyRow = (idx: number) => { + const copied = JSON.parse(JSON.stringify(items.value[idx])) as ItemRow + items.value.splice(idx + 1, 0, copied) + } + + const addRowAfter = (idx: number) => { + items.value.splice(idx + 1, 0, newItem()) + } + + const removeRow = (idx: number) => { + items.value.splice(idx, 1) + } + + // 收款项操作方法 + const addPaymentItem = () => { + paymentItems.value.push(newPaymentItem()) + } + + const removePaymentItem = (idx: number) => { + if (paymentItems.value.length > 1) { + paymentItems.value.splice(idx, 1) + } + } + + // 客户选择相关方法 + const openCustomerDialog = () => { + customerDialog.visible = true + customerDialog.pagination.currentPage = 1 + getCustomerList() + } + + const getCustomerList = async () => { + customerDialog.loading = true + try { + const { currentPage, pageSize } = customerDialog.pagination + const params = { current: currentPage, size: pageSize, keyword: customerDialog.search.keyword } + const { data, current, size, total } = await CrmService.getcustomerList(params as any) + customerDialog.tableData = data || [] + Object.assign(customerDialog.pagination, { currentPage: current, pageSize: size, total }) + } catch (e) { + } finally { + customerDialog.loading = false + } + } + + const handleCustomerSizeChange = (size: number) => { + customerDialog.pagination.pageSize = size + getCustomerList() + } + + const handleCustomerCurrentChange = (page: number) => { + customerDialog.pagination.currentPage = page + getCustomerList() + } + + const chooseCustomer = (row: any) => { + baseForm.customer_id = row.id || '' + baseForm.customer_snapshot = row.contacts || row.name || '' + baseForm.customer_country_id = row.country_id || '' + baseForm.customer_country_snapshot = row.country_name || '' + baseForm.customer_source_id = row.channel_id || '' + baseForm.customer_source_snapshot = row.channel_name || '' + customerDialog.visible = false + } + + // 车辆选择相关方法 + const openVehicleDialog = (index: number) => { + vehicleDialog.targetIndex = index + vehicleDialog.visible = true + vehicleDialog.pagination.currentPage = 1 + getVehicleList() + } + + const getVehicleList = async () => { + vehicleDialog.loading = true + try { + const { currentPage, pageSize } = vehicleDialog.pagination + const params = { current: currentPage, size: pageSize, keyword: vehicleDialog.search.keyword } + const { records, current, size, total } = await vehicleService.getVehicleList(params) + vehicleDialog.tableData = records || [] + Object.assign(vehicleDialog.pagination, { currentPage: current, pageSize: size, total }) + } catch { + ElMessage.error('获取车辆列表失败') + } finally { + vehicleDialog.loading = false + } + } + + const handleVehicleSizeChange = (size: number) => { + vehicleDialog.pagination.pageSize = size + getVehicleList() + } + + const handleVehicleCurrentChange = (page: number) => { + vehicleDialog.pagination.currentPage = page + getVehicleList() + } + + const chooseVehicle = (row: any) => { + const idx = vehicleDialog.targetIndex + if (idx !== undefined && idx >= 0 && idx < items.value.length) { + const target = items.value[idx] + target.sku_id = row.id || '' + target.model_snapshot = row.model_name || row.name || '' + target.color_exterior_code = row.color_exterior_code || '' + target.color_exterior_snapshot = row.color_exterior_text || '' + target.color_interior_code = row.color_interior_code || '' + target.color_interior_snapshot = row.color_interior_text || '' + target.official_price = row.msrp ?? target.official_price + if (row.currency) { + target.guide_price_currency = row.currency + } + } + vehicleDialog.visible = false + } + + // 数据回显方法 + const populateFromDetail = (res: any) => { + if (!res) return + // 基础信息 + baseForm.pick_up_date = res.pick_up_date || '' + baseForm.collect_all_date = res.collect_all_date || '' + baseForm.pi_no = res.pi_no || '' + baseForm.contract = res.contract || '' + baseForm.biz_date = res.biz_date || '' + baseForm.clearance_country_id = res.clearance_country_id || '' + baseForm.clearance_country_snapshot = res.clearance_country_snapshot || '' + baseForm.customer_source_id = res.customer_source_id || '' + baseForm.customer_source_snapshot = res.customer_source_snapshot || '' + baseForm.customer_id = res.customer_id || '' + baseForm.customer_snapshot = res.customer_snapshot || '' + baseForm.customer_country_id = res.customer_country_id || '' + baseForm.customer_country_snapshot = res.customer_country_snapshot || '' + baseForm.declarant_name = res.declarant_name || '' + baseForm.eta_ship_date = res.eta_ship_date || '' + baseForm.shipping_method_id = res.shipping_method_id ? String(res.shipping_method_id) : '' + baseForm.shipping_method_snapshot = res.shipping_method_snapshot || '' + baseForm.domestic_port_id = res.domestic_port_id || '' + baseForm.domestic_port_snapshot = res.domestic_port_snapshot || '' + baseForm.trade_term_id = res.trade_term_id || '' + baseForm.trade_term_snapshot = res.trade_term_snapshot || '' + baseForm.clearance_port_id = res.clearance_port_id || '' + baseForm.clearance_port_snapshot = res.clearance_port_snapshot || '' + baseForm.latest_arrival_date = res.latest_arrival_date || '' + baseForm.remark = res.remark || '' + baseForm.settlement_currency = res.settlement_currency || 'CNY' + // 合计与汇率 + baseForm.total_cif = res.cif_total_amount ? Number(res.cif_total_amount) : undefined + baseForm.total_cif_currency = res.settlement_currency || baseForm.total_cif_currency + baseForm.contract_exchange_rate = + res.contract_exchange_rate || getCurrencyRate(baseForm.total_cif_currency) + // 明细 + const list = Array.isArray(res.veh_list) ? res.veh_list : [] + items.value = list.map((it: any) => ({ + sku_id: it.sku_id ? String(it.sku_id) : '', + vin: it.vin || '', + _echo_vin: Boolean(it.vin), + mileage: it.mileage ?? 0, + model_snapshot: it.model_snapshot || '', + listing_price: it.listing_price?.amount ? Number(it.listing_price.amount) : undefined, + listing_price_currency: it.listing_price?.currency || 'CNY', + listing_price_currency_snapshot: '', + reduction_price: it.reduction_price?.amount ? Number(it.reduction_price.amount) : undefined, + reduction_price_currency: it.reduction_price?.currency || 'CNY', + reduction_price_currency_snapshot: '', + color_interior_code: it.color_interior_code || '', + color_interior_snapshot: it.color_interior_snapshot || '', + color_exterior_code: it.color_exterior_code || '', + color_exterior_snapshot: it.color_exterior_snapshot || '', + unit_price: it.unit_price?.amount ? Number(it.unit_price.amount) : undefined, + handling_fee: it.handling_fee?.amount ?? '', + fob_transport_fee: it.fob_transport_fee?.amount ? Number(it.fob_transport_fee.amount) : undefined, + port_misc_fee: it.port_misc_fee?.amount ?? '', + ocean_freight: it.ocean_freight?.amount ?? '', + ocean_freight_currency: it.ocean_freight?.currency || 'CNY', + ocean_freight_currency_snapshot: '', + retrofit_fee: it.retrofit_fee?.amount ?? '', + total_price: it.total_price?.amount ? Number(it.total_price.amount) : undefined, + official_price: it.official_price?.amount ? Number(it.official_price.amount) : undefined, + discount_price: it.discount_price?.amount ? Number(it.discount_price.amount) : undefined, + fob_fee: undefined, + fob_fee_currency: it.fob_transport_fee?.currency || 'CNY', + fob_fee_currency_snapshot: '', + helper_fee: undefined, + helper_fee_currency: it.handling_fee?.currency || 'CNY', + helper_fee_currency_snapshot: '', + domestic_freight: undefined, + domestic_freight_currency: it.domestic_freight?.currency || 'CNY', + domestic_freight_currency_snapshot: '', + port_fee: undefined, + port_fee_currency: it.port_misc_fee?.currency || 'CNY', + port_fee_currency_snapshot: '', + unit_price_currency: it.unit_price?.currency || 'CNY', + unit_price_currency_snapshot: '', + total_price_currency: it.total_price?.currency || 'CNY', + total_price_currency_snapshot: '', + guide_price_currency: it.official_price?.currency || 'CNY', + guide_price_currency_snapshot: '', + discount: undefined, + discount_currency: it.discount_price?.currency || 'CNY', + discount_currency_snapshot: '', + contract_rate: baseForm.contract_exchange_rate ? Number(baseForm.contract_exchange_rate) : undefined, + profit: undefined, + profit_type_snapshot: '', + modify_ids: Array.isArray(it.modify_list) ? it.modify_list.map((m: any) => Number(m.option_id)) : [], + modify_list: Array.isArray(it.modify_list) + ? it.modify_list.map((m: any) => ({ option_id: String(m.option_id), option_name_snapshot: m.option_name_snapshot })) + : [], + modify_fee_currency: it.retrofit_fee?.currency || 'CNY', + modify_fee_currency_snapshot: '' + })) + // 计划收款 + const pays = Array.isArray(res.account_list) ? res.account_list : [] + paymentItems.value = pays.map((p: any) => ({ + remit_country_id: p.remit_country_id ? Number(p.remit_country_id) : '', + remit_country_name_snapshot: p.remit_country_name_snapshot || '', + remit_date: p.remit_date || '', + currency_code: p.currency_code || '', + currency_name_snapshot: p.currency_name_snapshot || '', + account_id: p.account_id ? p.account_id : '', + account_name_snapshot: p.account_name_snapshot || '', + remitter_name: p.remitter_name || '', + amount: p.amount ? Number(p.amount) : undefined + })) + // 合同文件回显 + if (res.contract) { + contractFile.value = { + name: '合同文件', + url: res.contract_url || res.contract, + file_name: res.contract + } + } + } + + // 提交方法 + const handleSubmit = async (type: number, edit?: string, isfinal?: string) => { + // 提交校验:最终提交且为提交动作时(type=1),必须填写 VIN + const isFinalFlag = isfinal === '1' || (isfinal as any) === true + if (isFinalFlag && type === 1) { + const hasEmptyVin = (items.value || []).some((it) => !it.vin || String(it.vin).trim() === '') + if (hasEmptyVin) { + ElMessage.error('存在未填写车架号(VIN)的明细,请补全后再提交') + return + } + } + const mappedItems = items.value.map((it) => ({ + sku_id: it.sku_id, + vin: it.vin || '', + mileage: it.mileage, + model_snapshot: it.model_snapshot, + color_interior_code: it.color_interior_code, + color_interior_snapshot: it.color_interior_snapshot, + color_exterior_code: it.color_exterior_code, + color_exterior_snapshot: it.color_exterior_snapshot, + unit_price: wrapMoney(it.unit_price, (it as any).unit_price_currency, '单价'), + handling_fee: wrapMoney(it.handling_fee, (it as any).helper_fee_currency, '手续费'), + fob_transport_fee: wrapMoney( + it.fob_transport_fee ?? (it as any).fob_fee, + (it as any).fob_fee_currency, + 'FOB运输费' + ), + port_misc_fee: wrapMoney( + it.port_misc_fee ?? (it as any).port_fee, + (it as any).port_fee_currency, + '港杂费' + ), + ocean_freight: wrapMoney(it.ocean_freight, (it as any).ocean_freight_currency, '海运费'), + retrofit_fee: wrapMoney((it as any).retrofit_fee, (it as any).modify_fee_currency, '改装费'), + modify_list: it.modify_list, + // total_price: wrapMoney(it.total_price, (it as any).total_price_currency, '总价'), + total_price: wrapMoney(it.unit_price, (it as any).unit_price_currency, '总价'), + official_price: wrapMoney( + (it as any).official_price ?? (it as any).guide_price, + (it as any).guide_price_currency, + '官方指导价' + ), + discount_price: wrapMoney( + (it as any).discount_price ?? (it as any).discount, + (it as any).discount_currency, + '优惠额' + ), + listing_price: wrapMoney(it.listing_price, (it as any).listing_price_currency, '挂牌价'), + reduction_price: wrapMoney(it.reduction_price, (it as any).reduction_price_currency, '二手减价') + })) + + // 从提交参数中移除 settlement_currency + const baseFormWithoutCur = { ...(baseForm as any) } + delete (baseFormWithoutCur as any).total_cif + delete (baseFormWithoutCur as any).total_cif_currency + delete (baseFormWithoutCur as any).total_cif_currency_snapshot + delete (baseFormWithoutCur as any).contract_url + const payload = { + ...baseFormWithoutCur, + veh_list: mappedItems, + account_list: paymentItems.value, + approval_status: type + } + + try { + if (edit) { + const { message } = await CrmService.editStock({ + ...payload, + id: edit + } as any) + ElMessage.success(message || '保存成功') + } else { + const { message } = await CrmService.addStock(payload as any) + ElMessage.success(message || '保存成功') + } + setTimeout(() => { + if (isfinal == '1') { + window.location.href = '/crmpc/stocking/final_list' + } else { + window.location.href = '/crmpc/stocking/stocking_list' + } + }, 1000) + } catch (e) { + ElMessage.error('保存失败') + } + } + + // 监听器 + watch( + () => baseForm.settlement_currency, + (newCurrency) => { + if (newCurrency) { + baseForm.contract_exchange_rate = getCurrencyRate(newCurrency) + if (typeof validateContractRate === 'function') { + validateContractRate() + } + // 结算币种变化时,按新币种重算所有行利润 + (items.value || []).forEach((it: any, index: number) => { + const profit = calcProfit(it) + if (items.value[index]) items.value[index].profit = isNaN(profit) ? 0 : Number(profit.toFixed(2)) + }) + } + } + ) + + watch( + () => baseForm.contract_exchange_rate, + (newCurrency) => { + if (newCurrency) { + if (typeof validateContractRate === 'function') { + validateContractRate() + } + } + } + ) + + watch( + items, + (val) => { + val.forEach((it: any, index: number) => { + const profit = calcProfit(it) + if (items.value[index]) items.value[index].profit = isNaN(profit) ? 0 : Number(profit.toFixed(2)) + }) + + // 合计到岸价 = 所有单价相加 + const sum = (val || []).reduce((acc: number, cur: any) => { + const n = cur?.unit_price === undefined || cur?.unit_price === null || cur?.unit_price === '' ? 0 : Number(cur.unit_price) + return acc + (isNaN(n) ? 0 : n) + }, 0) + baseForm.total_cif = Number(sum.toFixed(2)) + + // 同步合计币种为第一行单价币种(若有) + if (items.value.length > 0) { + const c = items.value[0]?.unit_price_currency + if (c) baseForm.total_cif_currency = c + } + }, + { deep: true } + ) + + // 初始化方法 + const initializeForm = async () => { + await Promise.all([ + getcustomer(), + loadCurrencyOptions(), + getgetChannel(), + getarea(), + getmodification(), + loadColorOptions(), + loadAccountOptions(), + loadPortOptions(), + loadDictOptions() + ]) + + // 设置默认汇率 + baseForm.contract_exchange_rate = getCurrencyRate('CNY') + + // 设置预计发送日默认为当天 + const today = new Date() + const year = today.getFullYear() + const month = String(today.getMonth() + 1).padStart(2, '0') + const day = String(today.getDate()).padStart(2, '0') + baseForm.biz_date = `${year}-${month}-${day}` + } + + return { + // 数据 + baseForm, + items, + paymentItems, + contractFile, + customerDialog, + vehicleDialog, + + // 选项数据 + channelOptions, + countryOptions, + modificationOptions, + currencyOptions, + accountOptions, + customerOptions, + portOptions, + portOptionsall, + wcolorOptions, + ncolorOptions, + transportOptions, + trade_termsOptions, + + // 上传相关 + actionImageUplode, + uploadHeaders, + + // 方法 + initializeForm, + populateFromDetail, + handleSubmit, + getCurrencyRate, + validateContractRate, + wrapMoney, + calcProfit, + + // 合同文件方法 + isImageFile, + beforeContractUpload, + handleContractUpload, + removeContractFile, + + // 订单项方法 + copyRow, + addRowAfter, + removeRow, + newItem, + + // 收款项方法 + addPaymentItem, + removePaymentItem, + newPaymentItem, + + // 客户选择方法 + openCustomerDialog, + getCustomerList, + handleCustomerSizeChange, + handleCustomerCurrentChange, + chooseCustomer, + + // 车辆选择方法 + openVehicleDialog, + getVehicleList, + handleVehicleSizeChange, + handleVehicleCurrentChange, + chooseVehicle + } +} diff --git a/src/composables/useUploadHeaders.ts b/src/composables/useUploadHeaders.ts index be4edca..8450304 100644 --- a/src/composables/useUploadHeaders.ts +++ b/src/composables/useUploadHeaders.ts @@ -4,10 +4,10 @@ import { useUserStore } from '@/store/modules/user' export function useUploadHeaders() { const userStore = useUserStore() const uploadHeaders = computed(() => ({ - 'Fdesk-Language': userStore.language, - 'Fdesk-Corp-Abbr': userStore.companyabbr, - 'Fdesk-System-Code': 'fdesk', - 'Fdesk-Login-Type': userStore.loginType, + 'Fdesk-Language': userStore.language || 'zh', + 'Fdesk-Corp-Abbr': userStore.companyabbr || 'default', + 'Fdesk-System-Code': userStore.systemsabbr || 'fdesk', + 'Fdesk-Login-Type': userStore.loginType || 'upwd', 'Fdesk-Auth-Token': 'Bearer ' + userStore.accessToken })) return { uploadHeaders } diff --git a/src/config/index.ts b/src/config/index.ts index 82ce903..b25752e 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -127,7 +127,7 @@ const appConfig: SystemConfig = { ] as const, // 系统其他项默认配置 systemSetting: { - defaultMenuWidth: 240, // 菜单宽度 + defaultMenuWidth: 200, // 菜单宽度 defaultCustomRadius: '0.75', // 自定义圆角 defaultTabStyle: 'tab-default' // 标签样式 } diff --git a/src/router/guards/beforeEach.ts b/src/router/guards/beforeEach.ts index 2e27d11..dd5be70 100644 --- a/src/router/guards/beforeEach.ts +++ b/src/router/guards/beforeEach.ts @@ -59,27 +59,65 @@ async function handleRouteGuard( // 设置系统主题 setSystemTheme(to) - // 处理登录状态 - if (!(await handleLoginStatus(to, userStore, next))) { - return + // 特殊处理电子签名页面 - 优先处理,避免登录检查 + if (to.path === '/auth/signature/mobile') { + // 设置认证信息 + if (to.query.accessToken || to.query.token) { + const token = (to.query.accessToken as string) || (to.query.token as string) + if (token) { + userStore.setToken(token) + userStore.setLoginStatus(true) + } + } + + // 设置其他必要的参数到userStore + if (to.query.companyid && to.query.companyabbr) { + userStore.setCompany(to.query.companyid as string, to.query.companyabbr as string) + } + + if (to.query.systemsid && to.query.systemsabbr) { + userStore.setSystems(to.query.systemsid as string, to.query.systemsabbr as string) + } + + if (to.query.loginType) { + userStore.setLoginType(to.query.loginType as 'feishu' | 'upwd') + } + + if (to.matched.length > 0) { + setWorktab(to) + setPageTitle(to) + next() + return + } else { + // 如果路由没有匹配,直接跳转到404 + next(RoutesAlias.Exception404) + return + } } - // 处理动态路由注册 - if (!isRouteRegistered.value && userStore.isLogin) { + // 处理其他页面的登录状态(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile') { + if (!(await handleLoginStatus(to, userStore, next))) { + return + } + } + + // 处理动态路由注册(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile' && !isRouteRegistered.value && userStore.isLogin) { await handleDynamicRoutes(to, router, next) return } - // 处理已知的匹配路由 - if (to.matched.length > 0) { + // 处理已知的匹配路由(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile' && to.matched.length > 0) { setWorktab(to) setPageTitle(to) next() return } - // 尝试刷新路由重新注册 - if (userStore.isLogin) { + // 尝试刷新路由重新注册(跳过电子签名页面) + if (to.path !== '/auth/signature/mobile' && userStore.isLogin) { isRouteRegistered.value = false await handleDynamicRoutes(to, router, next) return @@ -97,6 +135,7 @@ async function handleLoginStatus( userStore: ReturnType, next: NavigationGuardNext ): Promise { + // 检查其他页面的登录状态 if (!userStore.isLogin && to.path !== RoutesAlias.Login && !to.meta.noLogin) { userStore.logOut() next({ @@ -105,6 +144,7 @@ async function handleLoginStatus( }) return false } + return true } @@ -200,22 +240,17 @@ async function registerAndStoreMenu( */ function handleMenuError(error: unknown): void { console.error('菜单处理失败:', error) - // 清理所有状态 const userStore = useUserStore() const menuStore = useMenuStore() const worktabStore = useWorktabStore() - // 清空菜单和工作台 menuStore.clearMenuList() worktabStore.clearAll() - // 重置路由注册状态 isRouteRegistered.value = false - // 登出用户 userStore.logOut() - throw error instanceof Error ? error : new Error('获取菜单列表失败,请重新登录') } @@ -251,20 +286,21 @@ function isValidMenuList(menuList: AppRouteRecord[]): boolean { */ export function resetRouterState(router: Router): void { isRouteRegistered.value = false - + // 清理所有动态注册的路由(不仅仅是带有dynamic标记的) const routesToRemove: string[] = [] router.getRoutes().forEach((route) => { // 排除静态路由和登录页面 - if (route.name && - route.name !== 'Login' && - route.name !== 'Exception404' && - route.name !== 'Exception500' && - !route.path.startsWith('/exception/')) { + if ( + route.name && + route.name !== 'Login' && + route.name !== 'Exception404' && + route.name !== 'Exception500' && + !route.path.startsWith('/exception/') + ) { routesToRemove.push(route.name as string) } }) - // 移除动态路由 routesToRemove.forEach((routeName) => { try { @@ -273,11 +309,9 @@ export function resetRouterState(router: Router): void { console.warn(`移除路由失败: ${routeName}`, error) } }) - // 清空菜单数据 const menuStore = useMenuStore() menuStore.clearMenuList() - // 清空工作台标签页 const worktabStore = useWorktabStore() worktabStore.clearAll() diff --git a/src/router/routes/asyncRoutes.ts b/src/router/routes/asyncRoutes.ts index 100633b..28b7384 100644 --- a/src/router/routes/asyncRoutes.ts +++ b/src/router/routes/asyncRoutes.ts @@ -65,6 +65,114 @@ export const asyncRoutes: AppRouteRecord[] = [ component: RoutesAlias.recycle, meta: { title: '回收站', + isHide: true, + keepAlive: true + } + }, + // 备货单相关路由 + { + path: '/crmpc/stocking/stocking_list', + name: 'StockingList', + component: 'crmpc/stocking/stocking_list', + meta: { + title: '备货单列表', + isHide: true + } + }, + { + path: '/crmpc/stocking/Add', + name: 'StockingAdd', + component: 'crmpc/stocking/Add', + meta: { + title: '创建备货单', + isHide: true, + isHideTab: false, + keepAlive: true + } + }, + { + path: '/crmpc/stocking/show', + name: 'StockingShow', + component: 'crmpc/stocking/show', + meta: { + title: '备货单详情', + isHide: true + } + }, + { + path: '/crmpc/stocking/final_add', + name: 'StockingFinalAdd', + component: 'crmpc/stocking/final_add', + meta: { + title: '最终备货单', + isHide: true, + isHideTab: false, + keepAlive: true + } + }, + { + path: '/crmpc/stocking/final_show', + name: 'StockingFinalShow', + component: 'crmpc/stocking/final_show', + meta: { + title: '最终备货单详情', + isHide: true + } + }, + //审批详情 + { + path: '/crmpc/stocking/stocking_auth_detil', + name: 'StockingAuth', + component: 'crmpc/stocking/stocking_auth_detil', + meta: { + title: '最终备货单详情', + isHide: true + } + }, + // 审批流程添加 + { + path: '/admin/approval/approval_add', + name: 'approvalAdd', + component: 'admin/approval/approval_add', + meta: { + title: '审批流程添加', + isHide: true + } + }, + { + path: '/crmpc/approval/auth_detil', + name: 'approvalDetil', + component: 'crmpc/approval/auth_detil', + meta: { + title: '审核详情', + isHide: true + } + }, + // 管理端备货单相关路由 + { + path: '/admin/stock/stocking_list_admin', + name: 'AdminStockingList', + component: 'admin/stock/stocking_list_admin', + meta: { + title: '备货单列表', + isHide: true + } + }, + { + path: '/admin/stock/show_admin', + name: 'AdminStockingShow', + component: 'admin/stock/show_admin', + meta: { + title: '备货单详情', + isHide: true + } + }, + { + path: '/admin/stock/final_show_admin', + name: 'AdminStockingFinalShow', + component: 'admin/stock/final_show_admin', + meta: { + title: '最终备货单详情', isHide: true } } diff --git a/src/router/routes/staticRoutes.ts b/src/router/routes/staticRoutes.ts index 7a011c4..5525e3f 100644 --- a/src/router/routes/staticRoutes.ts +++ b/src/router/routes/staticRoutes.ts @@ -41,6 +41,12 @@ export const staticRoutes: AppRouteRecordRaw[] = [ component: () => import('@/views/auth/feishu-callback.vue'), meta: { title: '飞书回调', noLogin: true, isHideTab: true } }, + { + path: '/auth/signature/mobile', + name: 'SignatureMobile', + component: () => import('@views/auth/signature/mobile.vue'), + meta: { title: '电子签名', isHideTab: true, noLogin: true, setTheme: true } + }, { path: '/exception', component: Home, diff --git a/src/router/routesAlias.ts b/src/router/routesAlias.ts index 3a15dce..7e8ceb7 100644 --- a/src/router/routesAlias.ts +++ b/src/router/routesAlias.ts @@ -62,7 +62,13 @@ export enum RoutesAlias { UserCenter = '/system/user-center', // 用户中心 inquirySystem = '/admin/inquiry_system', // 询单系统 customer = '/admin/customer', // 管理端客户列表 - customeraudit = '/admin/customer/customeraudit' // 管理端待审客户列表 + customeraudit = '/admin/customer/customeraudit', // 管理端待审客户列表 + // 备货单相关路由 + StockingList = '/crmpc/stocking/stocking_list', // 备货单列表 + StockingAdd = '/crmpc/stocking/Add', // 创建备货单 + StockingShow = '/crmpc/stocking/show', // 备货单详情 + StockingFinalAdd = '/crmpc/stocking/final_add', // 最终备货单添加 + StockingFinalShow = '/crmpc/stocking/final_show' // 最终备货单详情 } // 主页路由 diff --git a/src/router/utils/registerRoutes.ts b/src/router/utils/registerRoutes.ts index a6c9195..e98f981 100644 --- a/src/router/utils/registerRoutes.ts +++ b/src/router/utils/registerRoutes.ts @@ -130,15 +130,21 @@ function loadComponent(componentPath: string, routeName: string): () => Promise< } // 构建可能的路径 - const fullPath = `../../views${componentPath}.vue` - const fullPathWithIndex = `../../views${componentPath}/index.vue` + const fullPathNoSlash = `../../views${componentPath}.vue` + const fullPathWithIndexNoSlash = `../../views${componentPath}/index.vue` + const fullPathWithSlash = `../../views/${componentPath}.vue` + const fullPathWithIndexSlash = `../../views/${componentPath}/index.vue` - // 先尝试直接路径,再尝试添加/index的路径 - const module = modules[fullPath] || modules[fullPathWithIndex] + // 依次尝试四种可能路径(兼容是否带斜杠的情况) + const module = + modules[fullPathNoSlash] || + modules[fullPathWithIndexNoSlash] || + modules[fullPathWithSlash] || + modules[fullPathWithIndexSlash] if (!module) { console.error( - `[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}` + `[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPathNoSlash}、${fullPathWithIndexNoSlash}、${fullPathWithSlash} 和 ${fullPathWithIndexSlash}` ) return () => Promise.resolve({ diff --git a/src/store/modules/user copy.ts b/src/store/modules/user copy.ts new file mode 100644 index 0000000..9e53132 --- /dev/null +++ b/src/store/modules/user copy.ts @@ -0,0 +1,212 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { LanguageEnum } from '@/enums/appEnum' +import { router } from '@/router' +import { useSettingStore } from './setting' +import { useWorktabStore } from './worktab' +import { AppRouteRecord } from '@/types/router' +import { setPageTitle } from '@/router/utils/utils' +import { resetRouterState } from '@/router/guards/beforeEach' +import { RoutesAlias } from '@/router/routesAlias' +import { UserService } from '@/api/usersApi' // 新增导入 +let refreshTimeout: ReturnType | null = null // 定时器变量 + +// 用户 +export const useUserStore = defineStore( + 'userStore', + () => { + const language = ref(LanguageEnum.ZH) + const isLogin = ref(false) + const isLock = ref(false) + const lockPassword = ref('') + const info = ref>({}) + const searchHistory = ref([]) + const accessToken = ref('') + const refreshToken = ref('') + const companyid = ref('') + const companyabbr = ref('') + const systemsid = ref('') + const systemsabbr = ref('') + const home_page = ref('') + const loginType = ref<'feishu' | 'upwd'>('upwd') + const tokenExpireAt = ref(0) + + const getUserInfo = computed(() => info.value) + const getSettingState = computed(() => useSettingStore().$state) + const getWorktabState = computed(() => useWorktabStore().$state) + + const setUserInfo = (newInfo: Api.User.UserInfo) => { + info.value = newInfo + } + + const setLoginStatus = (status: boolean) => { + isLogin.value = status + } + + const setLanguage = (lang: LanguageEnum) => { + setPageTitle(router.currentRoute.value) + language.value = lang + } + const setLoginType = (type: 'feishu' | 'upwd') => { + loginType.value = type + } + const setSearchHistory = (list: AppRouteRecord[]) => { + searchHistory.value = list + } + + const setLockStatus = (status: boolean) => { + isLock.value = status + } + + const setLockPassword = (password: string) => { + lockPassword.value = password + } + + const setToken = (newAccessToken: string, newRefreshToken?: string, expiresIn?: number) => { + accessToken.value = newAccessToken + if (newRefreshToken) { + refreshToken.value = newRefreshToken + } + if (expiresIn) { + const expireAt = Date.now() + expiresIn * 1000 + tokenExpireAt.value = expireAt + localStorage.setItem('token_expire_at', String(expireAt)) + // 检查是否需要重新设置定时器 + const currentLeftSeconds = Math.floor((expireAt - Date.now()) / 1000) + if (currentLeftSeconds > 0) { + scheduleTokenRefresh(currentLeftSeconds) + } + } + } + const setCompany = (id: string, abbr: string) => { + companyid.value = id + companyabbr.value = abbr + } + const setSystems = (id: string, abbr: string) => { + systemsid.value = id + systemsabbr.value = abbr + } + // 设置首页 + const sethome_page = (page: string) => { + home_page.value = page + } + function restoreTokenTimer() { + const expireAt = Number(localStorage.getItem('token_expire_at') || '0') + if (accessToken.value && expireAt > Date.now()) { + const leftSeconds = Math.floor((expireAt - Date.now()) / 1000) + scheduleTokenRefresh(leftSeconds) + } + } + + let isScheduled = false + + function scheduleTokenRefresh(expiresIn: number) { + if (refreshTimeout) { + clearTimeout(refreshTimeout) + refreshTimeout = null + } + + // 提前1分钟刷新 + const refreshTime = (expiresIn - 60) * 1000 + + if (refreshTime > 0) { + if (!isScheduled) { + isScheduled = true + refreshTimeout = setTimeout(() => { + refreshTokenAction().finally(() => { + isScheduled = false // 重置标志位 + }) + }, refreshTime) + } else { + console.log('scheduleTokenRefresh is already scheduled, skipping...') + } + } else { + console.error('Invalid refreshTime:', refreshTime) + } + } + + async function refreshTokenAction() { + try { + const { data } = await UserService.refreshToken({ refresh_token: refreshToken.value }) + // 动态计算剩余时间 + const currentTime = Date.now() + const expireAt = currentTime + data.expires_in * 1000 + const leftSeconds = Math.floor((expireAt - currentTime) / 1000) + setToken(data.access_token, data.refresh_token, leftSeconds) + } catch (e) { + console.error('Token refresh failed:', e) + logOut() + } + } + + const logOut = () => { + // 清空所有本地缓存 + localStorage.clear() + sessionStorage.clear() + systemsabbr.value = '' + systemsid.value = '' + companyabbr.value = '' + companyid.value = '' + home_page.value = '' + info.value = {} + isLogin.value = false + isLock.value = false + lockPassword.value = '' + accessToken.value = '' + refreshToken.value = '' + if (refreshTimeout) clearTimeout(refreshTimeout) // 清理定时器 + // 清理工作台标签页 + useWorktabStore().clearAll() + // 清理sessionStorage中的用户信息缓存 + sessionStorage.removeItem('lastUserInfo') + sessionStorage.removeItem('iframeRoutes') + // 重置路由状态 + resetRouterState(router) + // 跳转到登录页 + router.push(RoutesAlias.Login) + } + + return { + language, + isLogin, + isLock, + lockPassword, + info, + companyid, + companyabbr, + systemsid, + systemsabbr, + home_page, + searchHistory, + loginType, + accessToken, + refreshToken, + tokenExpireAt, + getUserInfo, + getSettingState, + getWorktabState, + setUserInfo, + setLoginStatus, + setLanguage, + setSearchHistory, + setLockStatus, + setLockPassword, + setToken, + setCompany, + setSystems, + setLoginType, + sethome_page, + logOut, + restoreTokenTimer + } + }, + { + persist: { + key: 'user', + storage: localStorage, + afterRestore: (ctx: { store: { restoreTokenTimer: () => void } }) => { + ctx.store.restoreTokenTimer() + } + } as any + } +) diff --git a/src/typings/api.d.ts b/src/typings/api.d.ts index 150d665..8d387d7 100644 --- a/src/typings/api.d.ts +++ b/src/typings/api.d.ts @@ -147,6 +147,8 @@ declare namespace Api { interface AddResponse { code: string message: string + status: any + data?: any } /** 修改状态参数 */ interface StatusParams { diff --git a/src/utils/storage/storage.ts b/src/utils/storage/storage.ts index 6b91467..e027419 100644 --- a/src/utils/storage/storage.ts +++ b/src/utils/storage/storage.ts @@ -98,10 +98,10 @@ class StorageCompatibilityManager { } /** - * 检查是否在登录页面 + * 检查是否在登录页面或电子签名页面 */ private isOnLoginPage(): boolean { - return location.href.includes(RoutesAlias.Login) + return location.href.includes(RoutesAlias.Login) || location.href.includes('/auth/signature/mobile') } /** diff --git a/src/utils/sys/wsClient.ts b/src/utils/sys/wsClient.ts new file mode 100644 index 0000000..18b3381 --- /dev/null +++ b/src/utils/sys/wsClient.ts @@ -0,0 +1,273 @@ +import mittBus from './mittBus' +import { useUserStore } from '@/store/modules/user' +import { ElNotification } from 'element-plus' + +type JsonValue = Record | any[] + +const WS_BASE = import.meta.env.VITE_API_URL_WSS + '/ws?token=' +const MAX_RECONNECT_ATTEMPTS = 10 +const MAX_RECONNECT_DELAY = 50000 +const HEARTBEAT_INTERVAL = 50000 + +let socket: WebSocket | null = null +let reconnectTimer: any = null +let heartbeatTimer: any = null +let reconnectAttempts = 0 +let isInitialConnection = true +let isConnecting = false +let lastInitToken: string | null = null +let lastInitAt = 0 +const INIT_DEBOUNCE_MS = 1200 + +// 生成唯一ID +const generateTraceId = (): string => { + try { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return `req-${crypto.randomUUID()}` + } + } catch { + // 忽略错误,使用fallback方案 + } + const timestamp = Date.now().toString(36) + const random = Math.random().toString(36).slice(2) + return `req-${timestamp}-${random}` +} + +// 计算重连延迟 +const getReconnectDelay = () => { + const base = 1000 * Math.pow(2, reconnectAttempts) + return Math.min(base, MAX_RECONNECT_DELAY) +} + +// 清理定时器 +const cleanup = () => { + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = null + } + if (heartbeatTimer) { + clearTimeout(heartbeatTimer) + heartbeatTimer = null + } +} + +// 启动心跳 +const startHeartbeat = () => { + if (heartbeatTimer) { + clearTimeout(heartbeatTimer) + } + heartbeatTimer = setTimeout(() => { + try { + if (socket?.readyState === WebSocket.OPEN) { + const msg = { + event: 'system.ping', + data: {}, + trace_id: generateTraceId() + } + socket.send(JSON.stringify(msg)) + } + } catch { + // 忽略心跳发送错误 + } + startHeartbeat() + }, HEARTBEAT_INTERVAL) +} + +// 发送心跳消息 +const sendPing = () => { + try { + if (socket?.readyState === WebSocket.OPEN) { + const msg = { + event: 'system.ping', + data: {}, + trace_id: generateTraceId() + } + socket.send(JSON.stringify(msg)) + } + } catch { + // 忽略发送错误 + } +} + +// 处理WebSocket消息 +const handleMessage = (event: MessageEvent) => { + let data: any = event.data + try { + data = JSON.parse(event.data) + } catch { + // 忽略解析错误 + } + console.log('WebSocket message:', data) + if (data.event == 'notify.receive') { + ElNotification({ + title: data.data.notice_data.message.title || '通知', + message: data.data.notice_data.message.content, + type: 'primary' + }) + mittBus.emit('notification-data-changed', 1) + } + mittBus.emit('ws:message', data) +} + +// 设置WebSocket事件处理器 +const setupWebSocketHandlers = (ws: WebSocket) => { + ws.onopen = () => { + isInitialConnection = false + isConnecting = false + startHeartbeat() + mittBus.emit('ws:open') + sendPing() + } + + ws.onmessage = handleMessage + + ws.onerror = (err) => { + mittBus.emit('ws:error', err) + isConnecting = false + try { + if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { + ws.close() + } + } catch { + // 忽略关闭错误 + } + } + + ws.onclose = () => { + mittBus.emit('ws:close') + isConnecting = false + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + scheduleReconnect() + } + } +} + +// 安排重连 +const scheduleReconnect = () => { + // 每次安排重连都递增(即使从未成功连接过) + reconnectAttempts += 1 + + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { + console.warn(`WebSocket重连失败,已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`) + mittBus.emit('ws:max-reconnect-reached') + return + } + + cleanup() + const delay = getReconnectDelay() + console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`) + reconnectTimer = setTimeout(() => { + const { accessToken } = useUserStore() + if (accessToken && !isConnecting) { + try { + if (socket) { + try { + socket.close() + } catch { + // 忽略关闭错误 + } + socket = null + } + + const url = `${WS_BASE}${accessToken}` + isConnecting = true + socket = new WebSocket(url) + setupWebSocketHandlers(socket) + } catch (e) { + mittBus.emit('ws:error', e) + isConnecting = false + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + scheduleReconnect() + } + } + } + }, delay) +} + +// 初始化WebSocket连接 +export const initWebSocket = (token: string) => { + try { + if (!token) return + const now = Date.now() + // 简单防抖:在短时间内重复触发则跳过 + if (now - lastInitAt < INIT_DEBOUNCE_MS) { + return + } + // 避免并发初始化:已在连接中或已连接且 token 未变化则跳过 + if (isConnecting) return + if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { + if (lastInitToken === token) { + return + } + // token 变更:需要切换连接 + try { socket.close() } catch {} + socket = null + } + + cleanup() + + const url = `${WS_BASE}${token}` + isConnecting = true + lastInitToken = token + lastInitAt = now + socket = new WebSocket(url) + setupWebSocketHandlers(socket) + } catch (e) { + mittBus.emit('ws:error', e) + isConnecting = false + scheduleReconnect() + } +} + +// 关闭WebSocket连接 +export const closeWebSocket = () => { + cleanup() + try { + socket?.close() + } catch { + // 忽略关闭错误 + } + socket = null + reconnectAttempts = 0 + isInitialConnection = true + isConnecting = false + lastInitToken = null +} + +// 发送WebSocket消息 +export const sendWebSocket = (payload: string | JsonValue) => { + if (!socket || socket.readyState !== WebSocket.OPEN) return false + try { + const data = typeof payload === 'string' ? payload : JSON.stringify(payload) + socket.send(data) + return true + } catch { + return false + } +} + +// 重置重连次数 +export const resetWebSocketReconnect = () => { + reconnectAttempts = 0 + isInitialConnection = true + console.log('WebSocket重连次数已重置') +} + +// 获取当前重连次数 +export const getWebSocketReconnectAttempts = () => reconnectAttempts + +// 获取最大重连次数 +export const getMaxReconnectAttempts = () => MAX_RECONNECT_ATTEMPTS + +// 获取WebSocket连接状态 +export const getWebSocketStatus = () => ({ + socket, + readyState: socket?.readyState, + reconnectAttempts, + maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS, + isReconnecting: reconnectTimer !== null, + isHeartbeating: heartbeatTimer !== null, + isInitialConnection, + isConnecting, + lastInitToken +}) \ No newline at end of file diff --git a/src/views/admin/approval/approval_add.vue b/src/views/admin/approval/approval_add.vue new file mode 100644 index 0000000..9c7fb32 --- /dev/null +++ b/src/views/admin/approval/approval_add.vue @@ -0,0 +1,1279 @@ + + + + + diff --git a/src/views/admin/approval/approval_edit.vue b/src/views/admin/approval/approval_edit.vue new file mode 100644 index 0000000..57537e7 --- /dev/null +++ b/src/views/admin/approval/approval_edit.vue @@ -0,0 +1,1373 @@ + + + + + diff --git a/src/views/admin/approval/approval_list.vue b/src/views/admin/approval/approval_list.vue new file mode 100644 index 0000000..1764301 --- /dev/null +++ b/src/views/admin/approval/approval_list.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/src/views/admin/approval/stocking_auth.vue b/src/views/admin/approval/stocking_auth.vue new file mode 100644 index 0000000..6fc7c83 --- /dev/null +++ b/src/views/admin/approval/stocking_auth.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/src/views/admin/approval/stocking_auth_detil.vue b/src/views/admin/approval/stocking_auth_detil.vue new file mode 100644 index 0000000..65dab8b --- /dev/null +++ b/src/views/admin/approval/stocking_auth_detil.vue @@ -0,0 +1,541 @@ +