From a514bf40090d951f1300d7397b55dd6add230a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 18 Sep 2025 09:55:10 +0800 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 车辆信息,备货单 等页面 --- src/App.vue | 33 + src/api/admin/kpi.ts | 109 ++ src/api/adminApi.ts | 112 +- src/api/commonApi.ts | 97 ++ src/api/crmApi.ts | 80 +- src/api/crmpc/vehicleApi.ts | 98 ++ src/api/financeApi.ts | 74 + src/api/systemApi.ts | 65 + src/api/vehicleApi.ts | 196 +++ src/assets/img/crm/contract.png | Bin 0 -> 1508 bytes src/assets/styles/el-ui.scss | 9 + .../core/layouts/art-page-content/index.vue | 6 +- 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/utils/storage/storage.ts | 4 +- 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 | 1 + src/views/admin/customer/highseas.vue | 1 + src/views/admin/customer/index.vue | 1 + src/views/admin/customer/recycle.vue | 1 + 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 | 2 +- src/views/admin/system/dictionary/index.vue | 538 +++++++ src/views/admin/system/feishu_app.vue | 2 +- src/views/admin/system/menu/index.vue | 14 +- src/views/admin/system/notice/index.vue | 2 +- src/views/admin/system/systems.vue | 2 +- src/views/admin/user/index.vue | 1 + 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/crmpc/approval/auth_detil.vue | 554 +++++++ src/views/crmpc/approval/stocking_auth.vue | 384 +++++ src/views/crmpc/customer/Add.vue | 1 + src/views/crmpc/customer/Edit.vue | 1 + src/views/crmpc/customer/Followadd.vue | 1 + src/views/crmpc/customer/Show.vue | 1 + src/views/crmpc/customer/index copy.vue | 735 +++++++++ src/views/crmpc/customer/index.vue | 1 + src/views/crmpc/highseas/index.vue | 1 + 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 | 1 + 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 | 4 +- src/views/work/order/index.vue | 219 ++- src/views/work/type/index.vue | 346 ++-- 88 files changed, 27057 insertions(+), 491 deletions(-) create mode 100644 src/api/admin/kpi.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/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/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/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/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/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/src/App.vue b/src/App.vue index 3036e35..eacffaf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -14,6 +14,7 @@ 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, accessToken } = storeToRefs(userStore) @@ -178,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) @@ -241,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 f118573..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 }) } @@ -205,4 +211,98 @@ export class manageService { 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 index 22a1cf8..2c812fe 100644 --- a/src/api/commonApi.ts +++ b/src/api/commonApi.ts @@ -35,4 +35,101 @@ export class commonService { 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 69bc4e1..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' }) } @@ -207,4 +207,82 @@ export class CrmService { // 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/systemApi.ts b/src/api/systemApi.ts index b9bbe95..458e369 100644 --- a/src/api/systemApi.ts +++ b/src/api/systemApi.ts @@ -245,4 +245,69 @@ export class systemService { 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/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/crm/contract.png b/src/assets/img/crm/contract.png new file mode 100644 index 0000000000000000000000000000000000000000..eb64eef95707e15fc057381a9eb0237d331af6ce GIT binary patch literal 1508 zcmVSWv)<^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 @@ -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/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/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/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 @@ +