3 Commits

Author SHA1 Message Date
浅念 938ef89ec0 Squashed commit of the following:
commit dee800f90f
Author: 浅念 <email@sample.com>
Date:   Thu Oct 9 18:07:35 2025 +0800

    客户导出 加筛选条件

commit 9925e2e2e4
Author: 浅念 <email@sample.com>
Date:   Wed Sep 24 19:06:26 2025 +0800

    Squashed commit of the following:

    commit 90df51097f
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 24 18:50:35 2025 +0800

        备货单 采购单

    commit 7b1fdf3883
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 24 11:15:31 2025 +0800

        渠道筛选

    commit 1c81d3108c
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 24 10:50:43 2025 +0800

        客户列表 添加渠道筛选

    commit 2b877e9b4d
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 22 10:20:46 2025 +0800

        去掉编码显示

    commit bee80af5af
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 19 16:43:30 2025 +0800

        港口添加 英文

    commit 045ce9289c
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 15:17:30 2025 +0800

        审批历史 和代办审批

    commit c5ec4bb5e1
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 11:31:22 2025 +0800

        修改kpi

    commit 9e260ddcd8
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:49:33 2025 +0800

        修改 kpi

    commit 6feb3ace75
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:42:44 2025 +0800

        kpi 统计

    commit ec257a246d
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 17:04:35 2025 +0800

        修改 弹框表格

    commit fa764852ab
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:54:35 2025 +0800

        添加 账户简称

    commit 65f7f4a123
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:10:28 2025 +0800

        品牌加热门  修改 仪表盘接口

    commit 922cfec81d
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 16 15:57:17 2025 +0800

        调整备货单  crm 端页面

    commit 5c8df36f12
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 15 17:14:38 2025 +0800

        调整备货单 及各页面名称

    commit 5503a14408
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 18:10:03 2025 +0800

        账户添加公司

    commit 3439b45d9a
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:53:26 2025 +0800

        修改终版备货单

    commit 045d13eb7b
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:49:07 2025 +0800

        修改备货单 等页面

    commit da690dfa06
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 11 17:44:37 2025 +0800

        修改 备货单 详情

    commit 853e9591b3
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 10 18:11:39 2025 +0800

        修改 备货单页面

    commit 9052bd1027
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 9 20:53:30 2025 +0800

        备货单  审核功能

    commit 079d9babb8
    Merge: ebd7479 91d810b
    Author: 浅念 <email@sample.com>
    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 ebd7479573
    Merge: 64e53b6 459d306
    Author: 浅念 <email@sample.com>
    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 64e53b6ee9
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 8 19:09:55 2025 +0800

        备货单 加 审批流程

    commit 9bd7c89cb4
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 4 15:09:17 2025 +0800

        车辆信息,备货单  等页面

    # Conflicts:
    #	src/api/adminApi.ts
    #	src/api/crmApi.ts
    #	src/components/custom/OrderDetailContent.vue
    #	src/composables/useOrderDetailDialog.ts
    #	src/composables/useStockingForm.ts
    #	src/views/admin/approval/stocking_auth.vue
    #	src/views/admin/approval/stocking_auth_detil.vue
    #	src/views/admin/stock/final_list_admin.vue
    #	src/views/admin/stock/stocking_list_admin.vue
    #	src/views/admin/user/index.vue
    #	src/views/admin/vehicle/color.vue
    #	src/views/admin/vehicle/modification.vue
    #	src/views/admin/vehicle/port.vue
    #	src/views/admin/vehicle/vehicle_list.vue
    #	src/views/crmpc/approval/stocking_auth.vue
    #	src/views/crmpc/stocking/Add.vue
    #	src/views/crmpc/stocking/final_add.vue
    #	src/views/crmpc/stocking/final_edit.vue
    #	src/views/crmpc/stocking/final_list.vue
    #	src/views/crmpc/stocking/stocking_edit.vue
    #	src/views/crmpc/stocking/stocking_list.vue
    #	src/views/crmpc/vehicle/color.vue
    #	src/views/crmpc/vehicle/modification.vue
    #	src/views/crmpc/vehicle/vehicle_list.vue

commit 4f18a1b8bf
Author: 浅念 <email@sample.com>
Date:   Thu Sep 18 11:34:11 2025 +0800

    Squashed commit of the following:

    commit c5ec4bb5e1
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 11:31:22 2025 +0800

        修改kpi

    commit 9e260ddcd8
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:49:33 2025 +0800

        修改 kpi

    commit 6feb3ace75
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:42:44 2025 +0800

        kpi 统计

    commit ec257a246d
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 17:04:35 2025 +0800

        修改 弹框表格

    commit fa764852ab
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:54:35 2025 +0800

        添加 账户简称

    commit 65f7f4a123
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:10:28 2025 +0800

        品牌加热门  修改 仪表盘接口

    commit 922cfec81d
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 16 15:57:17 2025 +0800

        调整备货单  crm 端页面

    commit 5c8df36f12
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 15 17:14:38 2025 +0800

        调整备货单 及各页面名称

    commit 5503a14408
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 18:10:03 2025 +0800

        账户添加公司

    commit 3439b45d9a
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:53:26 2025 +0800

        修改终版备货单

    commit 045d13eb7b
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:49:07 2025 +0800

        修改备货单 等页面

    commit da690dfa06
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 11 17:44:37 2025 +0800

        修改 备货单 详情

    commit 853e9591b3
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 10 18:11:39 2025 +0800

        修改 备货单页面

    commit 9052bd1027
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 9 20:53:30 2025 +0800

        备货单  审核功能

    commit 079d9babb8
    Merge: ebd7479 91d810b
    Author: 浅念 <email@sample.com>
    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 ebd7479573
    Merge: 64e53b6 459d306
    Author: 浅念 <email@sample.com>
    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 64e53b6ee9
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 8 19:09:55 2025 +0800

        备货单 加 审批流程

    commit 9bd7c89cb4
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 4 15:09:17 2025 +0800

        车辆信息,备货单  等页面

    # Conflicts:
    #	src/views/admin/kpi/kpi_annual.vue

commit a514bf4009
Author: 浅念 <email@sample.com>
Date:   Thu Sep 18 09:55:10 2025 +0800

    Squashed commit of the following:

    commit 9e260ddcd8
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:49:33 2025 +0800

        修改 kpi

    commit 6feb3ace75
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:42:44 2025 +0800

        kpi 统计

    commit ec257a246d
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 17:04:35 2025 +0800

        修改 弹框表格

    commit fa764852ab
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:54:35 2025 +0800

        添加 账户简称

    commit 65f7f4a123
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:10:28 2025 +0800

        品牌加热门  修改 仪表盘接口

    commit 922cfec81d
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 16 15:57:17 2025 +0800

        调整备货单  crm 端页面

    commit 5c8df36f12
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 15 17:14:38 2025 +0800

        调整备货单 及各页面名称

    commit 5503a14408
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 18:10:03 2025 +0800

        账户添加公司

    commit 3439b45d9a
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:53:26 2025 +0800

        修改终版备货单

    commit 045d13eb7b
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:49:07 2025 +0800

        修改备货单 等页面

    commit da690dfa06
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 11 17:44:37 2025 +0800

        修改 备货单 详情

    commit 853e9591b3
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 10 18:11:39 2025 +0800

        修改 备货单页面

    commit 9052bd1027
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 9 20:53:30 2025 +0800

        备货单  审核功能

    commit 079d9babb8
    Merge: ebd7479 91d810b
    Author: 浅念 <email@sample.com>
    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 ebd7479573
    Merge: 64e53b6 459d306
    Author: 浅念 <email@sample.com>
    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 64e53b6ee9
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 8 19:09:55 2025 +0800

        备货单 加 审批流程

    commit 9bd7c89cb4
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 4 15:09:17 2025 +0800

        车辆信息,备货单  等页面

commit 91d810b33a
Author: 浅念 <email@sample.com>
Date:   Tue Sep 9 14:51:23 2025 +0800

    添加公司搜索

commit 459d306504
Author: 浅念 <email@sample.com>
Date:   Tue Sep 9 13:56:43 2025 +0800

    用户添加 公司搜索

commit 8a0d4fc516
Author: 浅念 <email@sample.com>
Date:   Thu Sep 4 14:38:09 2025 +0800

    自定义跟进时间  处理

commit 58f7d56f40
Author: 浅念 <email@sample.com>
Date:   Wed Sep 3 15:13:10 2025 +0800

    工单去掉 移动端不显示限制

commit a91e1d7820
Author: 浅念 <email@sample.com>
Date:   Tue Sep 2 14:13:15 2025 +0800

    将编号 后移

commit a9f8c5c4dc
Author: 浅念 <email@sample.com>
Date:   Wed Aug 27 09:20:56 2025 +0800

    客户重复通知

commit aefd43f79d
Author: 浅念 <email@sample.com>
Date:   Tue Aug 26 09:55:42 2025 +0800

    修改 通知详情样式  和 即时通讯

commit 1bd9d163ec
Author: 浅念 <email@sample.com>
Date:   Mon Aug 25 15:31:14 2025 +0800

    通知 列表富文本内容显示

commit be6093e57d
Author: 浅念 <email@sample.com>
Date:   Mon Aug 25 13:34:13 2025 +0800

    crm 客户添加自定义列  跟进添加图片粘贴上传

commit d83246ba8c
Author: 浅念 <email@sample.com>
Date:   Fri Aug 22 18:33:58 2025 +0800

    显示编号

commit 329acaf68d
Author: 浅念 <email@sample.com>
Date:   Fri Aug 22 18:27:26 2025 +0800

    通知板块 及 功能调整

commit b319944007
Author: 浅念 <email@sample.com>
Date:   Thu Aug 21 11:12:12 2025 +0800

    调整联系方式字段为电话  列表中多个联系方式纵向排列显示   会增加公司信息的自定义输入框  排序搜索 等功能

commit 70d26f0829
Author: 浅念 <email@sample.com>
Date:   Tue Aug 19 16:18:15 2025 +0800

    1.0.3  的相关功能

commit 0e84bedb67
Author: 浅念 <email@sample.com>
Date:   Fri Aug 15 16:31:55 2025 +0800

    v1.0.2 更新内容

commit f5deb7b8f7
Author: 浅念 <email@sample.com>
Date:   Wed Aug 13 19:27:49 2025 +0800

    添加菜单 权限 等页面

commit e797cb24de
Author: 浅念 <email@sample.com>
Date:   Mon Aug 11 19:24:24 2025 +0800

    添加工单附件  及样式

commit 619311dfbe
Author: chenlan <suya123456>
Date:   Fri Aug 8 22:13:25 2025 +0800

    添加crm  归属客户

commit b402e522bf
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:51:17 2025 +0800

    询单权限校验

commit f950f665a9
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:10:45 2025 +0800

    添加 询单图片预览

commit 8deecc8197
Author: 浅念 <email@sample.com>
Date:   Wed Aug 6 18:36:42 2025 +0800

    修改用户页面 询单

commit 3e24d1905f
Author: 浅念 <email@sample.com>
Date:   Tue Aug 5 18:25:32 2025 +0800

    添加用户页面 客户页面接口 等

commit 7542d80e38
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 18:05:07 2025 +0800

    修改 仪表盘页面样式

commit e5548f95e4
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:50:55 2025 +0800

    修改询单字段  仪表盘页面  及添加管理端客户页面

commit d30ea5b7bf
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:48:58 2025 +0800

    添加echarts 插件

commit 0e4160c5ca
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:27:22 2025 +0800

    仪表盘接口

commit bd23288acd
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:26:46 2025 +0800

    修改 仪表盘

commit 6bfb940881
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 10:01:35 2025 +0800

    客户列表 添加公海详情展示

commit b50f650565
Author: 浅念 <email@sample.com>
Date:   Thu Jul 31 17:26:36 2025 +0800

    修改 客户标签样式  个人信息添加邮箱 修复关闭全部页面404

commit ea1dbb7af4
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 18:50:31 2025 +0800

    修改公海和询单页面

commit 1bfbb95bd9
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 17:23:09 2025 +0800

    修改样式和添加用户数据显示

commit a9f5f76b35
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 15:06:03 2025 +0800

    设置默认页面

commit 3ea9d3d5df
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 11:46:31 2025 +0800

    修改飞书登录 公司id 系统id 获取不到问题  客户及公海添加用户信息显示

commit 0cef82279f
Author: 浅念 <email@sample.com>
Date:   Tue Jul 29 16:42:36 2025 +0800

    修改工单 客户等页面

commit 695ebbcc71
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:50:04 2025 +0800

    修改默认值

commit ffc194a1c1
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:05:33 2025 +0800

    飞书授权添加系统码  及登录进入页面路由

commit 48b95e252f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:48:49 2025 +0800

    回收站添加占位

commit 382011d592
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:11:50 2025 +0800

    修改登录页样式公海数据 个人信息默认图

commit 0f790269db
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 09:16:55 2025 +0800

    修改字段名称

commit f18cfdb69e
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 17:28:51 2025 +0800

    调整代码格式

commit 858147fbbd
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 10:17:22 2025 +0800

    修改token刷新 及修改询单详情展示

commit c333b907e6
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 18:22:56 2025 +0800

    对接询单接口

commit 7979a31e36
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:40:02 2025 +0800

    修改代码格式

commit 4a39387865
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:07:13 2025 +0800

    设置客户列表权限

commit fc4de02582
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:08:13 2025 +0800

    feat(router): 调整菜单

commit 2ffe0c9753
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:04:16 2025 +0800

    feat(crmApi.ts): 调整结构

commit c3fb2c48d3
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:00:25 2025 +0800

    feat(router): 路由调整

commit c5b0a8265a
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:54:06 2025 +0800

    chore: 添加询单页面及调整菜单

commit ca06c9bc46
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:35:43 2025 +0800

    询单页面及菜单

commit 3b6de4c7fa
Merge: d3bb4ec 85e8b9b
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 09:22:20 2025 +0800

    Merge branch 'master' into feature/crm_pc

commit d3bb4ec6e5
Merge: c7d8908 d7ab6f1
Author: 浅念 <email@sample.com>
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 c7d890809c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:12:16 2025 +0800

    添加系统码

commit 31cd18fade
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:02:48 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit 59c9c97c08
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 17:56:52 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit d4b577c42e
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 13:03:56 2025 +0800

    feat(crm): 添加相关图片

commit e4967760b7
Merge: 0c67666 85102fc
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:25:49 2025 +0800

    Finish crm_pc

commit 85102fcf32
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:24:06 2025 +0800

    crm 接口对接

commit 0c67666793
Author: 浅念 <email@sample.com>
Date:   Wed Jul 16 17:18:14 2025 +0800

    添加上传头部信息

commit a41c75f703
Author: 浅念 <email@sample.com>
Date:   Tue Jul 15 09:40:53 2025 +0800

    修改页面

commit 22ae0e86c8
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 17:59:02 2025 +0800

    修改页面

commit 812e3c4b0c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 10:09:10 2025 +0800

    修改菜单问题

commit 2ad479b006
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:45:57 2025 +0800

    修改样式

commit 5802e157bf
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:43:34 2025 +0800

    修改工单

commit 713d43f7b4
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 18:37:16 2025 +0800

    添加登录

commit 719911918f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 4 17:16:38 2025 +0800

    项目创建

commit ce47b39fa5
Author: 浅念 <email@sample.com>
Date:   Wed Jul 2 09:37:51 2025 +0800

    删除

# Conflicts:
#	.env.development
#	src/api/adminApi.ts
#	src/api/crmApi.ts
#	src/components/custom/OrderDetailContent.vue
#	src/composables/useOrderDetailDialog.ts
#	src/composables/useStockingForm.ts
#	src/views/admin/approval/stocking_auth.vue
#	src/views/admin/approval/stocking_auth_detil.vue
#	src/views/admin/customer/index.vue
#	src/views/admin/kpi/kpi_annual.vue
#	src/views/admin/stock/final_list_admin.vue
#	src/views/admin/stock/stocking_list_admin.vue
#	src/views/admin/user/index.vue
#	src/views/admin/vehicle/color.vue
#	src/views/admin/vehicle/modification.vue
#	src/views/admin/vehicle/port.vue
#	src/views/admin/vehicle/vehicle_list.vue
#	src/views/crmpc/approval/stocking_auth.vue
#	src/views/crmpc/stocking/Add.vue
#	src/views/crmpc/stocking/final_add.vue
#	src/views/crmpc/stocking/final_edit.vue
#	src/views/crmpc/stocking/final_list.vue
#	src/views/crmpc/stocking/stocking_edit.vue
#	src/views/crmpc/stocking/stocking_list.vue
#	src/views/crmpc/vehicle/color.vue
#	src/views/crmpc/vehicle/modification.vue
#	src/views/crmpc/vehicle/vehicle_list.vue
2025-10-10 10:14:26 +08:00
浅念 c7cdec27e0 Squashed commit of the following:
commit a514bf4009
Author: 浅念 <email@sample.com>
Date:   Thu Sep 18 09:55:10 2025 +0800

    Squashed commit of the following:

    commit 9e260ddcd8
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:49:33 2025 +0800

        修改 kpi

    commit 6feb3ace75
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 18 09:42:44 2025 +0800

        kpi 统计

    commit ec257a246d
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 17:04:35 2025 +0800

        修改 弹框表格

    commit fa764852ab
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:54:35 2025 +0800

        添加 账户简称

    commit 65f7f4a123
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 17 15:10:28 2025 +0800

        品牌加热门  修改 仪表盘接口

    commit 922cfec81d
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 16 15:57:17 2025 +0800

        调整备货单  crm 端页面

    commit 5c8df36f12
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 15 17:14:38 2025 +0800

        调整备货单 及各页面名称

    commit 5503a14408
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 18:10:03 2025 +0800

        账户添加公司

    commit 3439b45d9a
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:53:26 2025 +0800

        修改终版备货单

    commit 045d13eb7b
    Author: 浅念 <email@sample.com>
    Date:   Fri Sep 12 17:49:07 2025 +0800

        修改备货单 等页面

    commit da690dfa06
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 11 17:44:37 2025 +0800

        修改 备货单 详情

    commit 853e9591b3
    Author: 浅念 <email@sample.com>
    Date:   Wed Sep 10 18:11:39 2025 +0800

        修改 备货单页面

    commit 9052bd1027
    Author: 浅念 <email@sample.com>
    Date:   Tue Sep 9 20:53:30 2025 +0800

        备货单  审核功能

    commit 079d9babb8
    Merge: ebd7479 91d810b
    Author: 浅念 <email@sample.com>
    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 ebd7479573
    Merge: 64e53b6 459d306
    Author: 浅念 <email@sample.com>
    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 64e53b6ee9
    Author: 浅念 <email@sample.com>
    Date:   Mon Sep 8 19:09:55 2025 +0800

        备货单 加 审批流程

    commit 9bd7c89cb4
    Author: 浅念 <email@sample.com>
    Date:   Thu Sep 4 15:09:17 2025 +0800

        车辆信息,备货单  等页面

commit 91d810b33a
Author: 浅念 <email@sample.com>
Date:   Tue Sep 9 14:51:23 2025 +0800

    添加公司搜索

commit 459d306504
Author: 浅念 <email@sample.com>
Date:   Tue Sep 9 13:56:43 2025 +0800

    用户添加 公司搜索

commit 8a0d4fc516
Author: 浅念 <email@sample.com>
Date:   Thu Sep 4 14:38:09 2025 +0800

    自定义跟进时间  处理

commit 58f7d56f40
Author: 浅念 <email@sample.com>
Date:   Wed Sep 3 15:13:10 2025 +0800

    工单去掉 移动端不显示限制

commit a91e1d7820
Author: 浅念 <email@sample.com>
Date:   Tue Sep 2 14:13:15 2025 +0800

    将编号 后移

commit a9f8c5c4dc
Author: 浅念 <email@sample.com>
Date:   Wed Aug 27 09:20:56 2025 +0800

    客户重复通知

commit aefd43f79d
Author: 浅念 <email@sample.com>
Date:   Tue Aug 26 09:55:42 2025 +0800

    修改 通知详情样式  和 即时通讯

commit 1bd9d163ec
Author: 浅念 <email@sample.com>
Date:   Mon Aug 25 15:31:14 2025 +0800

    通知 列表富文本内容显示

commit be6093e57d
Author: 浅念 <email@sample.com>
Date:   Mon Aug 25 13:34:13 2025 +0800

    crm 客户添加自定义列  跟进添加图片粘贴上传

commit d83246ba8c
Author: 浅念 <email@sample.com>
Date:   Fri Aug 22 18:33:58 2025 +0800

    显示编号

commit 329acaf68d
Author: 浅念 <email@sample.com>
Date:   Fri Aug 22 18:27:26 2025 +0800

    通知板块 及 功能调整

commit b319944007
Author: 浅念 <email@sample.com>
Date:   Thu Aug 21 11:12:12 2025 +0800

    调整联系方式字段为电话  列表中多个联系方式纵向排列显示   会增加公司信息的自定义输入框  排序搜索 等功能

commit 70d26f0829
Author: 浅念 <email@sample.com>
Date:   Tue Aug 19 16:18:15 2025 +0800

    1.0.3  的相关功能

commit 0e84bedb67
Author: 浅念 <email@sample.com>
Date:   Fri Aug 15 16:31:55 2025 +0800

    v1.0.2 更新内容

commit f5deb7b8f7
Author: 浅念 <email@sample.com>
Date:   Wed Aug 13 19:27:49 2025 +0800

    添加菜单 权限 等页面

commit e797cb24de
Author: 浅念 <email@sample.com>
Date:   Mon Aug 11 19:24:24 2025 +0800

    添加工单附件  及样式

commit 619311dfbe
Author: chenlan <suya123456>
Date:   Fri Aug 8 22:13:25 2025 +0800

    添加crm  归属客户

commit b402e522bf
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:51:17 2025 +0800

    询单权限校验

commit f950f665a9
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:10:45 2025 +0800

    添加 询单图片预览

commit 8deecc8197
Author: 浅念 <email@sample.com>
Date:   Wed Aug 6 18:36:42 2025 +0800

    修改用户页面 询单

commit 3e24d1905f
Author: 浅念 <email@sample.com>
Date:   Tue Aug 5 18:25:32 2025 +0800

    添加用户页面 客户页面接口 等

commit 7542d80e38
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 18:05:07 2025 +0800

    修改 仪表盘页面样式

commit e5548f95e4
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:50:55 2025 +0800

    修改询单字段  仪表盘页面  及添加管理端客户页面

commit d30ea5b7bf
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:48:58 2025 +0800

    添加echarts 插件

commit 0e4160c5ca
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:27:22 2025 +0800

    仪表盘接口

commit bd23288acd
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:26:46 2025 +0800

    修改 仪表盘

commit 6bfb940881
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 10:01:35 2025 +0800

    客户列表 添加公海详情展示

commit b50f650565
Author: 浅念 <email@sample.com>
Date:   Thu Jul 31 17:26:36 2025 +0800

    修改 客户标签样式  个人信息添加邮箱 修复关闭全部页面404

commit ea1dbb7af4
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 18:50:31 2025 +0800

    修改公海和询单页面

commit 1bfbb95bd9
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 17:23:09 2025 +0800

    修改样式和添加用户数据显示

commit a9f5f76b35
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 15:06:03 2025 +0800

    设置默认页面

commit 3ea9d3d5df
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 11:46:31 2025 +0800

    修改飞书登录 公司id 系统id 获取不到问题  客户及公海添加用户信息显示

commit 0cef82279f
Author: 浅念 <email@sample.com>
Date:   Tue Jul 29 16:42:36 2025 +0800

    修改工单 客户等页面

commit 695ebbcc71
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:50:04 2025 +0800

    修改默认值

commit ffc194a1c1
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:05:33 2025 +0800

    飞书授权添加系统码  及登录进入页面路由

commit 48b95e252f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:48:49 2025 +0800

    回收站添加占位

commit 382011d592
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:11:50 2025 +0800

    修改登录页样式公海数据 个人信息默认图

commit 0f790269db
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 09:16:55 2025 +0800

    修改字段名称

commit f18cfdb69e
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 17:28:51 2025 +0800

    调整代码格式

commit 858147fbbd
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 10:17:22 2025 +0800

    修改token刷新 及修改询单详情展示

commit c333b907e6
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 18:22:56 2025 +0800

    对接询单接口

commit 7979a31e36
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:40:02 2025 +0800

    修改代码格式

commit 4a39387865
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:07:13 2025 +0800

    设置客户列表权限

commit fc4de02582
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:08:13 2025 +0800

    feat(router): 调整菜单

commit 2ffe0c9753
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:04:16 2025 +0800

    feat(crmApi.ts): 调整结构

commit c3fb2c48d3
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:00:25 2025 +0800

    feat(router): 路由调整

commit c5b0a8265a
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:54:06 2025 +0800

    chore: 添加询单页面及调整菜单

commit ca06c9bc46
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:35:43 2025 +0800

    询单页面及菜单

commit 3b6de4c7fa
Merge: d3bb4ec 85e8b9b
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 09:22:20 2025 +0800

    Merge branch 'master' into feature/crm_pc

commit d3bb4ec6e5
Merge: c7d8908 d7ab6f1
Author: 浅念 <email@sample.com>
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 c7d890809c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:12:16 2025 +0800

    添加系统码

commit 31cd18fade
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:02:48 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit 59c9c97c08
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 17:56:52 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit d4b577c42e
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 13:03:56 2025 +0800

    feat(crm): 添加相关图片

commit e4967760b7
Merge: 0c67666 85102fc
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:25:49 2025 +0800

    Finish crm_pc

commit 85102fcf32
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:24:06 2025 +0800

    crm 接口对接

commit 0c67666793
Author: 浅念 <email@sample.com>
Date:   Wed Jul 16 17:18:14 2025 +0800

    添加上传头部信息

commit a41c75f703
Author: 浅念 <email@sample.com>
Date:   Tue Jul 15 09:40:53 2025 +0800

    修改页面

commit 22ae0e86c8
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 17:59:02 2025 +0800

    修改页面

commit 812e3c4b0c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 10:09:10 2025 +0800

    修改菜单问题

commit 2ad479b006
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:45:57 2025 +0800

    修改样式

commit 5802e157bf
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:43:34 2025 +0800

    修改工单

commit 713d43f7b4
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 18:37:16 2025 +0800

    添加登录

commit 719911918f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 4 17:16:38 2025 +0800

    项目创建

commit ce47b39fa5
Author: 浅念 <email@sample.com>
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
2025-09-18 10:57:17 +08:00
浅念 5d906087d3 Squashed commit of the following:
commit 0e84bedb67
Author: 浅念 <email@sample.com>
Date:   Fri Aug 15 16:31:55 2025 +0800

    v1.0.2 更新内容

commit f5deb7b8f7
Author: 浅念 <email@sample.com>
Date:   Wed Aug 13 19:27:49 2025 +0800

    添加菜单 权限 等页面

commit e797cb24de
Author: 浅念 <email@sample.com>
Date:   Mon Aug 11 19:24:24 2025 +0800

    添加工单附件  及样式

commit 619311dfbe
Author: chenlan <suya123456>
Date:   Fri Aug 8 22:13:25 2025 +0800

    添加crm  归属客户

commit b402e522bf
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:51:17 2025 +0800

    询单权限校验

commit f950f665a9
Author: 浅念 <email@sample.com>
Date:   Thu Aug 7 10:10:45 2025 +0800

    添加 询单图片预览

commit 8deecc8197
Author: 浅念 <email@sample.com>
Date:   Wed Aug 6 18:36:42 2025 +0800

    修改用户页面 询单

commit 3e24d1905f
Author: 浅念 <email@sample.com>
Date:   Tue Aug 5 18:25:32 2025 +0800

    添加用户页面 客户页面接口 等

commit 7542d80e38
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 18:05:07 2025 +0800

    修改 仪表盘页面样式

commit e5548f95e4
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:50:55 2025 +0800

    修改询单字段  仪表盘页面  及添加管理端客户页面

commit d30ea5b7bf
Author: 浅念 <email@sample.com>
Date:   Mon Aug 4 14:48:58 2025 +0800

    添加echarts 插件

commit 0e4160c5ca
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:27:22 2025 +0800

    仪表盘接口

commit bd23288acd
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 18:26:46 2025 +0800

    修改 仪表盘

commit 6bfb940881
Author: 浅念 <email@sample.com>
Date:   Fri Aug 1 10:01:35 2025 +0800

    客户列表 添加公海详情展示

commit b50f650565
Author: 浅念 <email@sample.com>
Date:   Thu Jul 31 17:26:36 2025 +0800

    修改 客户标签样式  个人信息添加邮箱 修复关闭全部页面404

commit ea1dbb7af4
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 18:50:31 2025 +0800

    修改公海和询单页面

commit 1bfbb95bd9
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 17:23:09 2025 +0800

    修改样式和添加用户数据显示

commit a9f5f76b35
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 15:06:03 2025 +0800

    设置默认页面

commit 3ea9d3d5df
Author: 浅念 <email@sample.com>
Date:   Wed Jul 30 11:46:31 2025 +0800

    修改飞书登录 公司id 系统id 获取不到问题  客户及公海添加用户信息显示

commit 0cef82279f
Author: 浅念 <email@sample.com>
Date:   Tue Jul 29 16:42:36 2025 +0800

    修改工单 客户等页面

commit 695ebbcc71
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:50:04 2025 +0800

    修改默认值

commit ffc194a1c1
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 17:05:33 2025 +0800

    飞书授权添加系统码  及登录进入页面路由

commit 48b95e252f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:48:49 2025 +0800

    回收站添加占位

commit 382011d592
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 15:11:50 2025 +0800

    修改登录页样式公海数据 个人信息默认图

commit 0f790269db
Author: 浅念 <email@sample.com>
Date:   Fri Jul 25 09:16:55 2025 +0800

    修改字段名称

commit f18cfdb69e
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 17:28:51 2025 +0800

    调整代码格式

commit 858147fbbd
Author: 浅念 <email@sample.com>
Date:   Thu Jul 24 10:17:22 2025 +0800

    修改token刷新 及修改询单详情展示

commit c333b907e6
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 18:22:56 2025 +0800

    对接询单接口

commit 7979a31e36
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:40:02 2025 +0800

    修改代码格式

commit 4a39387865
Author: 浅念 <email@sample.com>
Date:   Wed Jul 23 14:07:13 2025 +0800

    设置客户列表权限

commit fc4de02582
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:08:13 2025 +0800

    feat(router): 调整菜单

commit 2ffe0c9753
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:04:16 2025 +0800

    feat(crmApi.ts): 调整结构

commit c3fb2c48d3
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 17:00:25 2025 +0800

    feat(router): 路由调整

commit c5b0a8265a
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:54:06 2025 +0800

    chore: 添加询单页面及调整菜单

commit ca06c9bc46
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 16:35:43 2025 +0800

    询单页面及菜单

commit 3b6de4c7fa
Merge: d3bb4ec 85e8b9b
Author: 浅念 <email@sample.com>
Date:   Tue Jul 22 09:22:20 2025 +0800

    Merge branch 'master' into feature/crm_pc

commit d3bb4ec6e5
Merge: c7d8908 d7ab6f1
Author: 浅念 <email@sample.com>
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 c7d890809c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:12:16 2025 +0800

    添加系统码

commit 31cd18fade
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 18:02:48 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit 59c9c97c08
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 17:56:52 2025 +0800

    Merge branch 'master' into feature/crm_pc

    # Conflicts:
    #	.gitignore

commit d4b577c42e
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 13:03:56 2025 +0800

    feat(crm): 添加相关图片

commit e4967760b7
Merge: 0c67666 85102fc
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:25:49 2025 +0800

    Finish crm_pc

commit 85102fcf32
Author: 浅念 <email@sample.com>
Date:   Mon Jul 21 11:24:06 2025 +0800

    crm 接口对接

commit 0c67666793
Author: 浅念 <email@sample.com>
Date:   Wed Jul 16 17:18:14 2025 +0800

    添加上传头部信息

commit a41c75f703
Author: 浅念 <email@sample.com>
Date:   Tue Jul 15 09:40:53 2025 +0800

    修改页面

commit 22ae0e86c8
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 17:59:02 2025 +0800

    修改页面

commit 812e3c4b0c
Author: 浅念 <email@sample.com>
Date:   Mon Jul 14 10:09:10 2025 +0800

    修改菜单问题

commit 2ad479b006
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:45:57 2025 +0800

    修改样式

commit 5802e157bf
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 19:43:34 2025 +0800

    修改工单

commit 713d43f7b4
Author: 浅念 <email@sample.com>
Date:   Fri Jul 11 18:37:16 2025 +0800

    添加登录

commit 719911918f
Author: 浅念 <email@sample.com>
Date:   Fri Jul 4 17:16:38 2025 +0800

    项目创建

commit ce47b39fa5
Author: 浅念 <email@sample.com>
Date:   Wed Jul 2 09:37:51 2025 +0800

    删除
2025-08-15 16:37:41 +08:00
121 changed files with 7604 additions and 23009 deletions
@@ -1,237 +0,0 @@
# 备货单车辆明细附件上传 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 `Add.vue` 车辆明细每行增加行内附件上传,附件随车辆提交到 `stock_order_items.attachment`。
**Architecture:** 数据层改共用的 `useStockingForm.ts`:`ItemRow` 加 `attachment_files`(展示用 FileItem[])、`newItem` 初始化、`mappedItems` 映射成 `attachment`(`{url, original_name}` 数组,与 `contract` 实际格式一致)、新增上传/删除方法并暴露 `uploadHeaders`/`itemUploadAction`。UI 在 `Add.vue` 明细行加一个 `ElCol`:`el-upload` 按钮 + 已传文件 `el-tag` 列表。上传走现成 `/ticket/upload?scene=stock_orders`,无需改上传接口。
**Tech Stack:** Vue 3 `<script setup>`、TypeScript、element-plus 2.8、`useStockingForm` composable、`useUploadHeaders`(已导入)。
## Global Constraints
- **无前端测试框架**:验收 = `npm run build`(含 `vue-tsc --noEmit` 类型检查)通过 + `dev` 手动核对。
- **attachment 提交格式**:`{ url, original_name }` 对象数组(参照 `baseForm.contract` 实际代码 useStockingForm.ts:697-700),**不是** file_name 数组——这样回填/预览有 url 可用。
- `useUploadHeaders` 已在 useStockingForm.ts:6 导入、:155 取出 `uploadHeaders`,直接复用、暴露。
- 后端 `stock_order_items.attachment` 字段已加(DB 就绪);接口层需对接 `veh_list[].attachment` 的接收与返回。
- commit 规范:每任务一次 commit,`feat:` 开头。
## 文件结构
| 文件 | 责任 | 改动 |
|---|---|---|
| `src/composables/useStockingForm.ts` | 车辆表单数据/上传方法(Add/edit 共用) | Modify |
| `src/views/crmpc/stocking/Add.vue` | 添加备货单页,车辆明细行内附件 UI | Modify |
---
## Task 1: useStockingForm.ts 数据层 + 上传方法
**Files:**
- Modify: `src/composables/useStockingForm.ts`(`ItemRow` 接口 ~49、`newItem` ~218、上传方法区 ~706、`mappedItems` ~1234、`populateFromDetail` items 映射 ~974、`return` ~1515)
**Interfaces:**
- Consumes: `useUploadHeaders`(已导入)、`items: ref<ItemRow[]>`(已有)、`/ticket/upload` 返回 `{code, data:{original_name, file_url, file_name}}`。
- Produces: `ItemRow.attachment_files`;暴露 `uploadHeaders`/`itemUploadAction`/`beforeItemAttachmentUpload`/`handleItemAttachmentUpload`/`removeItemAttachment`。
- [ ] **Step 1: `ItemRow` 接口加字段**
在 `export interface ItemRow { ... }` 内(如 `remark: string` 之后)加:
```ts
attachment_files?: { name: string; url: string; file_name: string }[]
```
- [ ] **Step 2: `newItem()` 初始化**
在 `const newItem = (): ItemRow => ({ ... })` 对象内(如 `remark: ''` 之后)加:
```ts
attachment_files: [],
```
- [ ] **Step 3: 新增上传 action 与方法**
在 `handleContractUpload` 之后(约 line 706 后)插入:
```ts
// 车辆明细附件上传
const itemUploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=stock_orders`
const beforeItemAttachmentUpload = (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 handleItemAttachmentUpload = (response: any, idx: number) => {
if (response.code === 200) {
const fileInfo = {
name: response.data.original_name || '附件',
url: response.data.file_url,
file_name: response.data.file_name
}
if (!items.value[idx].attachment_files) {
items.value[idx].attachment_files = []
}
items.value[idx].attachment_files!.push(fileInfo)
ElMessage.success('附件上传成功!')
} else {
ElMessage.error(response.message || '附件上传失败!')
}
}
const removeItemAttachment = (idx: number, fileIdx: number) => {
const list = items.value[idx].attachment_files
if (list) {
list.splice(fileIdx, 1)
ElMessage.success('附件已删除')
}
}
```
- [ ] **Step 4: `mappedItems` 映射 `attachment`**
在 `mappedItems` 的 `increase_price: wrapMoney(...)` 之后(约 line 1234 后、`}))` 之前)加:
```ts
attachment: (it.attachment_files || []).map((f) => ({
url: f.url,
original_name: f.name
}))
```
- [ ] **Step 5: `populateFromDetail` 回填**
在 `populateFromDetail` 中 `items.value = list.map((it: any) => ({ ... }))` 的映射对象内(与 `model_snapshot` 等同层)加,兼容后端两种返回格式:
```ts
attachment_files: (it.attachment || []).map((f: any) => ({
name: f.original_name || f.name || '附件',
url: f.url || f.file_url || '',
file_name: f.file_name || f.url || ''
}))
```
- [ ] **Step 6: `return` 暴露**
在 `return {`(约 line 1515)对象内加:
```ts
uploadHeaders,
itemUploadAction,
beforeItemAttachmentUpload,
handleItemAttachmentUpload,
removeItemAttachment,
```
- [ ] **Step 7: 类型检查(阶段性)**
Run: `npx vue-tsc --noEmit 2>&1 | grep -E "useStockingForm|error TS" | head`
Expected: 无 `useStockingForm.ts` 相关 error。(dayjs 等无关报错忽略)
- [ ] **Step 8: Commit**
```bash
git add src/composables/useStockingForm.ts
git commit -m "feat(stocking): useStockingForm 支持车辆明细附件上传
ItemRow 增加 attachment_files,mappedItems 映射 attachment,
新增车辆附件上传/删除方法。
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 2: Add.vue 行内附件 UI
**Files:**
- Modify: `src/views/crmpc/stocking/Add.vue`(解构 ~1189-1211、车辆明细行 ~116+)
**Interfaces:**
- Consumes: Task 1 暴露的 `uploadHeaders`/`itemUploadAction`/`beforeItemAttachmentUpload`/`handleItemAttachmentUpload`/`removeItemAttachment`;`items`、`idx`(已有)。
- Produces: 每行附件上传交互。
- [ ] **Step 1: 解构新方法**
在 Add.vue 从 `useStockingForm(false)` 解构的块内(`chooseVehicle` 等同处,约 line 1206-1210)加:
```ts
uploadHeaders,
itemUploadAction,
beforeItemAttachmentUpload,
handleItemAttachmentUpload,
removeItemAttachment
```
- [ ] **Step 2: 明细行加「附件」ElCol**
在车辆明细 `v-for="(item, idx) in items"` 的行内(建议放在「里程」`ElCol` 之后,约 line 196 后)加:
```html
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="附件" label-width="85px">
<el-upload
:action="itemUploadAction"
:headers="uploadHeaders"
:show-file-list="false"
:before-upload="beforeItemAttachmentUpload"
:on-success="(res: any) => handleItemAttachmentUpload(res, idx)"
multiple
accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.txt"
>
<el-button size="small" type="primary" plain>上传附件</el-button>
</el-upload>
<div
v-if="item.attachment_files && item.attachment_files.length"
style="margin-top: 4px"
>
<el-tag
v-for="(f, fIdx) in item.attachment_files"
:key="fIdx"
closable
@close="removeItemAttachment(idx, fIdx)"
style="margin: 2px 4px 2px 0"
>
{{ f.name }}
</el-tag>
</div>
</ElFormItem>
</ElCol>
```
- [ ] **Step 3: 类型检查 + 构建**
Run: `npm run build`
Expected: vue-tsc 无 error,vite build 产出 dist。
- [ ] **Step 4: 手动核对(dev)**
Run: `npm run dev`,进入添加备货单页。
核对:每辆车行有「上传附件」按钮;选文件上传后出现文件名 tag;tag × 可删;提交时 Network `veh_list[].attachment` 为 `{url, original_name}` 数组。
- [ ] **Step 5: Commit**
```bash
git add src/views/crmpc/stocking/Add.vue
git commit -m "feat(stocking): 添加备货单车辆明细行内附件上传
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## 后端联调清单(交付后端)
- `stock_order_items.attachment` 字段已就绪。
- store/update:接收 `veh_list[].attachment`(`{url, original_name}` 对象数组),写入 `attachment`(json 序列化)。
- show/detail:返回 `items[].attachment`(同 `{url, original_name}` 结构),前端据此回显。
@@ -1,272 +0,0 @@
# 备货单 / 终版列表「全部 / 我的」视图切换 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 `stocking_list.vue` 与 `final_list.vue` 给 `userId === 73` 的特权用户增加「全部 / 我的」视图切换,切换时给列表接口传 `view_scope=all|self`。
**Architecture:** 纯前端 UI + 传参改动。新增本地状态 `viewScope`,工具栏渲染条件 `canViewAll = userStore.getUserInfo.userId === 73`;`getList` 把 `viewScope.value` 作为 `view_scope` 随请求发出;切换时重置页码并重新请求。是否返回全量数据由后端按 token 鉴权决定(后端改造见 spec 第 3.4 节,不在本计划内)。
**Tech Stack:** Vue 3 `<script setup>` + TypeScript、element-plus 2.8、Pinia(`useUserStore`)、`unplugin-auto-import`(`ref/reactive/computed/onMounted/watch` 自动导入,无需手写 import)。
## Global Constraints
- **无前端单元测试框架**:本项目 `package.json` 无 `test` 脚本。验收方式 = `lint` 通过 + 本地 `dev` 手动核对,不写自动化测试。
- **后端是前置依赖**:前端「全部」真正生效需后端 `GET /crm/stock/order/list` 支持 `view_scope` 参数(见 spec 3.4)。前端可先行独立完成 UI 与传参;完整功能联调待后端就绪。
- **安全边界在后端**:前端 `userId === 73` 仅控制按钮显隐,非授权依据。
- **代码风格**:遵循现有写法——`<script setup lang="ts">`、element-plus 组件、snake_case 请求参数。
- **commit 规范**:每任务一次 commit,信息以 `feat:` 开头。
## 文件结构
| 文件 | 责任 | 改动类型 |
|---|---|---|
| `src/views/crmpc/stocking/stocking_list.vue` | 备货单列表,加视图切换 + `view_scope` 传参 | Modify |
| `src/views/crmpc/stocking/final_list.vue` | 终版列表,同构改动 | Modify |
两文件改动完全同构,仅 `is_final`(0 vs 1)不同。不新建文件。
---
## Task 1: stocking_list.vue 增加视图切换
**Files:**
- Modify: `src/views/crmpc/stocking/stocking_list.vue`
- 模板工具栏(约 `line 9-22` 的 `toolbar-right`)
- `getList` 参数(约 `line 339-344`)
- script 状态(约 `line 280-281` `useAuth` 之后)
- script import(约 `line 264-274`)
**Interfaces:**
- Consumes: `useUserStore`(来自 `@/store/modules/user`),其 `getUserInfo: computed => Partial<{ userId: number }>`;`CrmService.getStockList(params)`(已存在)。
- Produces: 列表请求新增参数字段 `view_scope: 'all' | 'self'`。
- [ ] **Step 1: 添加 import**
在 `<script setup lang="ts">` 顶部 import 区(`import { useAuth } from '@/composables/useAuth'` 同处)增加一行:
```ts
import { useUserStore } from '@/store/modules/user'
```
- [ ] **Step 2: 添加状态与切换处理**
在 `const { hasAuth } = useAuth()`(约 line 281)之后插入:
```ts
const userStore = useUserStore()
// 仅 userId=73 的特权用户可切换「全部/我的」
const canViewAll = computed(() => userStore.getUserInfo.userId === 73)
const viewScope = ref<'all' | 'self'>('self')
const onScopeChange = () => {
pagination.currentPage = 1
getList()
}
```
说明:`computed` / `ref` 由 `unplugin-auto-import` 提供,无需 import。`getUserInfo.userId` 类型为 `number | undefined`,`undefined === 73` 为 `false`,判空安全。
- [ ] **Step 3: 修改 `getList` 请求参数**
定位 `getList` 内(约 line 339-344):
```ts
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
```
改为(增加 `view_scope`):
```ts
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value,
view_scope: viewScope.value
}
```
- [ ] **Step 4: 工具栏添加切换控件**
定位模板 `toolbar-right`(约 line 9-21),在 `<el-input>` 之前插入 `el-radio-group`:
原:
```html
<div class="toolbar-right">
<el-input
v-model="keyword"
```
改为:
```html
<div class="toolbar-right">
<el-radio-group
v-if="canViewAll"
v-model="viewScope"
@change="onScopeChange"
style="margin-right: 12px"
>
<el-radio-button value="self">我的</el-radio-button>
<el-radio-button value="all">全部</el-radio-button>
</el-radio-group>
<el-input
v-model="keyword"
```
说明:用 `value`(element-plus 2.6+ 推荐)而非 `label` 作值,避免 2.8 的 label 弃用告警。文字走默认插槽。
- [ ] **Step 5: Lint 校验**
Run: `npm run fix -- src/views/crmpc/stocking/stocking_list.vue`
Expected: 无 error。若有,按提示修复(常见:prettier 格式)。
- [ ] **Step 6: 手动核对(前端独立部分)**
Run: `npm run dev`,浏览器打开备货单列表页。
核对项:
1. 用**非 73 账号**登录:工具栏**不出现**「全部/我的」控件,列表行为同改动前。
2. 打开 DevTools Network,看 `/crm/stock/order/list` 请求 Payload:无论是否 73,都应带上 `view_scope=self`(默认值)。
3. (需 73 登录态)用 73 账号登录:出现「全部/我的」,默认选中「我的」;点「全部」后 Network 请求带 `view_scope=all`,点回「我的」带 `view_scope=self`,且每次切换分页回到第 1 页。
4. 「全部」是否真返回全量数据,取决于后端是否已支持——后端未就绪时,切到「全部」数据可能不变,属预期。
- [ ] **Step 7: Commit**
```bash
git add src/views/crmpc/stocking/stocking_list.vue
git commit -m "feat(stocking): 备货单列表为特权用户增加「全部/我的」视图切换
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## Task 2: final_list.vue 增加视图切换(同构)
**Files:**
- Modify: `src/views/crmpc/stocking/final_list.vue`
- 模板工具栏(约 `line 7-19` 的 `toolbar-right`)
- `getList` 参数(约 `line 278-283`)
- script 状态(约 `line 229` `useAuth` 之后)
- script import(约 `line 206-216`)
**Interfaces:**
- Consumes: 同 Task 1(`useUserStore`、`CrmService.getStockList`)。
- Produces: 同 Task 1(`view_scope` 参数)。
- [ ] **Step 1: 添加 import**
在 `<script setup lang="ts">` 顶部 import 区增加:
```ts
import { useUserStore } from '@/store/modules/user'
```
- [ ] **Step 2: 添加状态与切换处理**
在 `const { hasAuth } = useAuth()`(约 line 229)之后插入:
```ts
const userStore = useUserStore()
// 仅 userId=73 的特权用户可切换「全部/我的」
const canViewAll = computed(() => userStore.getUserInfo.userId === 73)
const viewScope = ref<'all' | 'self'>('self')
const onScopeChange = () => {
pagination.currentPage = 1
getList()
}
```
- [ ] **Step 3: 修改 `getList` 请求参数**
定位 `getList` 内(约 line 278-283):
```ts
const params = {
current: currentPage,
size: pageSize,
is_final: 1, // 只获取备货单,非终版
keyword: keyword.value
}
```
改为:
```ts
const params = {
current: currentPage,
size: pageSize,
is_final: 1, // 终版
keyword: keyword.value,
view_scope: viewScope.value
}
```
(注:原代码注释「非终版」是笔误,此处顺手修正为「终版」。)
- [ ] **Step 4: 工具栏添加切换控件**
定位模板 `toolbar-right`(约 line 7-19),在 `<el-input>` 之前插入:
原:
```html
<div class="toolbar-right">
<el-input
v-model="keyword"
```
改为:
```html
<div class="toolbar-right">
<el-radio-group
v-if="canViewAll"
v-model="viewScope"
@change="onScopeChange"
style="margin-right: 12px"
>
<el-radio-button value="self">我的</el-radio-button>
<el-radio-button value="all">全部</el-radio-button>
</el-radio-group>
<el-input
v-model="keyword"
```
- [ ] **Step 5: Lint 校验**
Run: `npm run fix -- src/views/crmpc/stocking/final_list.vue`
Expected: 无 error。
- [ ] **Step 6: 手动核对(前端独立部分)**
Run: `npm run dev`,浏览器打开终版列表页。核对项同 Task 1 Step 6(注意此处是 `is_final=1` 的终版页)。
- [ ] **Step 7: Commit**
```bash
git add src/views/crmpc/stocking/final_list.vue
git commit -m "feat(stocking): 终版列表为特权用户增加「全部/我的」视图切换
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## 后端联调清单(交付后端,不在本计划执行范围)
接口 `GET /crm/stock/order/list` 增加可选参数 `view_scope`:
| 请求 | 后端行为 |
|---|---|
| 不传 / `view_scope=self` | 按现状:依 token 用户过滤 |
| `view_scope=all` 且 token 用户 id ∈ {73} | 返回全量 |
| `view_scope=all` 且 token 用户 id ∉ {73} | 降级为 `self`(返回该用户自己的) |
要点:**只信 token,忽略前端任何用户标识**,防 IDOR。
@@ -1,120 +0,0 @@
# 备货单车辆明细附件上传 设计
- 日期:2026-08-06
- 分支:feature/crm_pc
- 涉及前端:`src/views/crmpc/stocking/Add.vue`、`src/composables/useStockingForm.ts`
- 涉及后端:`stock_order_items.attachment`(已加字段)、备货单 store/update/show 接口
## 1. 背景
添加备货单(`Add.vue`)时,车辆明细(`items`,每行一辆车)目前只能录入车型 / 颜色 / 价格等字段,**没有附件**。业务需要每辆车能上传若干附件(如该车相关文件),随车辆一起保存。
后端 `stock_order_items` 表已新增 `attachment` 字段(本次由后端同事完成)。
## 2. 需求
- 添加备货单时,车辆明细**每辆车**可上传**多个**附件。
- UI 为**行内紧凑**形态:每行一个「上传附件」入口,已传文件以文件名标签列出、可删除,不撑高行高。
- 附件随车辆数据一起提交,存入 `stock_order_items.attachment`。
- 字段名统一用 `attachment`,值为 `file_name` 字符串数组(与备货单级 `contract`/`pi_doc`/`transaction_slip` 的存储模式一致)。
## 3. 现状(复用基础)
- 车辆明细 `items` 由 `useStockingForm(false)` 提供,每行 `item` 是 `ItemRow`。
- 备货单级文件已有成熟模式:前端维护 `FileItem[]`(`{name, url, file_name}`,带 url 供预览/下载),提交时映射成 `file_name[]` 存入 `baseForm.contract` 等。
- 上传走通用接口 `${VITE_API_URL}/ticket/upload?scene=stock_orders`,header 带 token(`useUploadHeaders`),成功返回 `{ data: { original_name, file_url, file_name } }`。**此接口无需改动。**
- 现成组件 `MultiFileUpload`(120px 大拖拽区)适合整页文件区,不适合塞进行内——故行内用 `el-upload` 直接内联。
## 4. 设计
### 4.1 数据层 `useStockingForm.ts`
- `ItemRow` 类型增加 `attachment_files: { name: string; url: string; file_name: string }[]`。
- `newItem()` 初始化 `attachment_files: []`。
- 提交映射 `mappedItems` 增加:
```ts
attachment: (it.attachment_files || []).map((f) => f.file_name)
```
- 详情回填 `populateFromDetail`:从后端返回的 `it.attachment` 反解为 `attachment_files`(格式见 4.4,Add 为新建不走回填,但 composable 与 edit 共享,一并支持)。
- `copyRow` 已是 `JSON.parse(JSON.stringify(...))`,自动带上附件,无需改。
- 草稿 `pickDraftPayload` 已含 `items: toRaw(items.value)`,附件自然入草稿,无需改。
- 新增车辆附件上传/删除方法(仿现有 `handleContractUpload` 等):
- `handleItemAttachmentUpload(response: any, idx: number)`:校验 `response.code===200`,把 `{ name: data.original_name, url: data.file_url, file_name: data.file_name }` push 进 `items.value[idx].attachment_files`。
- `removeItemAttachment(idx: number, fileIdx: number)`:从 `items.value[idx].attachment_files` 删除指定项。
### 4.2 UI 层 `Add.vue`
在车辆明细 `v-for="(item, idx) in items"` 的行内末尾增加一个 `ElCol`「附件」:
```html
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="附件" label-width="85px">
<el-upload
:action="`${VITE_API_URL}/ticket/upload?scene=stock_orders`"
:headers="uploadHeaders"
:show-file-list="false"
:before-upload="(file) => beforeItemAttachmentUpload(file)"
:on-success="(res) => handleItemAttachmentUpload(res, idx)"
multiple
:accept="itemAttachmentAccept"
>
<el-button size="small" type="primary" plain>上传附件</el-button>
</el-upload>
<div class="item-attachment-tags" v-if="item.attachment_files?.length">
<el-tag
v-for="(f, fIdx) in item.attachment_files"
:key="fIdx"
closable
@close="removeItemAttachment(idx, fIdx)"
style="margin: 2px 4px 2px 0"
>
{{ f.name }}
</el-tag>
</div>
</ElFormItem>
</ElCol>
```
- `VITE_API_URL`、`uploadHeaders`(`useUploadHeaders`)、`beforeItemAttachmentUpload`(类型/大小校验,复用 `MultiFileUpload` 的 accept 与 10MB 限制)、`handleItemAttachmentUpload`、`removeItemAttachment` 均由 `useStockingForm` return,模板解构使用,与现有 `contractFiles` 等用法一致。
### 4.3 后端配合
- `stock_order_items.attachment` 字段已加(DB 就绪)。
- 接口层需对接:
- **store / update**:接收 `items[].attachment`(`file_name` 数组),写入 `stock_order_items.attachment`(按 json/序列化存储)。
- **show / detail**:返回 `items[].attachment`(`file_name` 数组,或含 `original_name`/`file_url` 的对象数组——见 4.4 约定)。
### 4.4 回填格式约定(需与后端对齐)
详情接口返回的 `items[].attachment` 有两种可能,择一与后端确认:
- **A**:`file_name` 字符串数组(如 `["xxx.pdf"]`)——前端回填时 `url` 需另行拼接,预览受限。
- **B(推荐)**:对象数组(如 `[{ original_name, file_url, file_name }]`,与 `/ticket/upload` 返回一致)——前端直接还原成 `attachment_files`,预览/下载可用。
实现(plan)阶段以前端实际请求详情接口的返回为准;若为 A,前端按现有 `populateFromDetail` 中文件字段的拼 url 方式处理。
## 5. 安全 / 限制
- 复用 `beforeUpload`:类型限制 `.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.txt`,单文件 ≤ 10MB。
- 上传 header 带 token(`useUploadHeaders`),鉴权由后端 `/ticket/upload` 既有机制处理。
- `attachment` 只存 `file_name`,不含敏感信息。
## 6. 改动清单
| 端 | 文件 | 改动 |
|---|---|---|
| 前端 | `src/composables/useStockingForm.ts` | `ItemRow` 加 `attachment_files`;`newItem` 初始化;`mappedItems` 映射 `attachment`;`populateFromDetail` 回填;新增 `handleItemAttachmentUpload`/`removeItemAttachment`/`beforeItemAttachmentUpload`/暴露 `uploadHeaders` |
| 前端 | `src/views/crmpc/stocking/Add.vue` | 车辆明细行末尾加「附件」`ElCol`(`el-upload` + 标签列表);解构新方法 |
| 后端 | store/update/show 接口 | 对接 `items[].attachment`(DB 字段已就绪) |
## 7. 验收标准
1. 添加备货单:每辆车行有「上传附件」按钮,点选多文件逐个上传成功后,文件名以标签列出。
2. 标签可点 × 删除;删除后提交不包含该文件。
3. 提交备货单,Network 里 `items[].attachment` 为对应 `file_name` 数组;后端 `stock_order_items.attachment` 落库正确。
4. (后端 show 返回后)进入编辑/详情,已存附件能回显为标签列表。
5. 超格式 / 超 10MB 文件被拦截并提示。
## 8. 未做项 / 后续
- 本次只改 `Add.vue` 模板;`stocking_edit.vue` 共用 `useStockingForm`,数据层已支持,如需在编辑页也展示附件上传,后续加同款 `ElCol` 即可。
- 终版(final_add/final_edit)是否需要同等能力,待业务确认。
@@ -1,99 +0,0 @@
# 备货单 / 终版列表「全部 / 我的」视图切换 设计
- 日期:2026-08-06
- 分支:feature/crm_pc
- 涉及页面:`src/views/crmpc/stocking/stocking_list.vue`、`src/views/crmpc/stocking/final_list.vue`
- 涉及接口:`GET /crm/stock/order/list`
## 1. 背景
备货单列表(`stocking_list`,`is_final=0`)与终版列表(`final_list`,`is_final=1`)当前共用同一个列表接口 `GET /crm/stock/order/list`。后端依据请求 token 中的用户 id 过滤数据,即每个登录用户只能看到自己作为业务员的备货单 / 终版。前端请求参数只有 `current / size / is_final / keyword`,无任何范围控制。
业务上存在一个固定特权用户(user_id = 73),需要在「看自己」之外,额外能查看「全公司所有备货单 / 终版」。
## 2. 需求
- 给 user_id = 73 这一个用户,在两个列表页增加「全部 / 我的」视图切换。
- 选「全部」时,该用户看到全公司数据;选「我的」时,行为同现状(只看自己)。
- 其他用户(非 73)完全不受影响:不显示切换控件,行为同现状。
- 默认视图为「我的」。
## 3. 设计
### 3.1 UI
- 在两个列表页的工具栏增加一组互斥切换控件「全部 / 我的」(`el-radio-group` button 样式,二选一)。
- 控件位置:工具栏右侧、搜索框左边,两个页面位置保持一致(`final_list` 现有 `toolbar-left` 被注释,不在那里放,统一放右侧,改动更小)。
- 渲染条件:`useUserStore().getUserInfo.userId === 73`。`getUserInfo` 是 `userStore.info` 的 computed,类型 `Partial<UserInfo>`,`userId` 可能为 `undefined`,`undefined === 73` 为 `false`,判断安全,无需额外判空。
### 3.2 前端逻辑
- 新增本地状态 `viewScope = ref<'all' | 'self'>('self')`,初值 `'self'`(即默认「我的」)。
- 切换控件绑定 `viewScope`;`@change` 时:
1. `pagination.currentPage = 1`(切回第一页,避免越界空页);
2. 调用 `getList()`。
- `getList()` 构造参数时,当 `userId === 73` 把 `viewScope.value` 作为 `view_scope` 一并传入:
- 「全部」→ `view_scope: 'all'`
- 「我的」→ `view_scope: 'self'`
- 非 73 用户:不渲染控件,`viewScope` 恒为 `'self'`,传 `view_scope: 'self'`(或不传,见 3.3)。为简单起见统一传,后端对非特权用户会忽略该值。
### 3.3 请求参数
列表请求参数由
```jsonc
{ "current": 1, "size": 10, "is_final": 0, "keyword": "" }
```
扩展为(73 用户)
```jsonc
{ "current": 1, "size": 10, "is_final": 0, "keyword": "", "view_scope": "all" }
```
- 参数名 `view_scope` 沿用现有 snake_case 风格(与 `is_final` / `keyword` 一致)。
- 取值为语义化的 `'all' | 'self'`,**不传用户 id**。
### 3.4 后端配合
`GET /crm/stock/order/list` 增加可选参数 `view_scope`,处理逻辑:
| 请求情况 | 后端行为 |
|---|---|
| 不传 / `view_scope=self` | 现状逻辑:按 token 用户过滤,返回自己的数据 |
| `view_scope=all` 且 token 用户 id ∈ {73} | 返回全公司全量数据 |
| `view_scope=all` 且 token 用户 id ∉ {73} | 降级:当作 `self` 处理,返回该用户自己的数据(忽略 `all`) |
后端「可看全部」白名单暂写死为 `{73}`,与前端判断一致。
### 3.5 默认值
73 用户打开页面,`viewScope` 初值 `'self'`,即默认「我的」。需看全部时手动切换。
## 4. 安全考量
- **后端只认 token**:是否返回全量数据,由后端依据 token 中的用户 id 判断,绝不信任前端传入的任何用户标识。前端 `userId` 存于 localStorage,可被篡改。
- 前端写死 `userId === 73` 仅用于**控件显隐**的便捷实现,不是安全边界。即便有人改前端强制显示按钮并传 `view_scope=all`,后端鉴权不通过仍只返回其自己的数据。
- 结论:语义化参数 + 后端 token 鉴权,可兜住水平越权(IDOR)风险。
## 5. 改动清单
| 端 | 文件 / 接口 | 改动 |
|---|---|---|
| 前端 | `stocking_list.vue` | 工具栏加「全部/我的」`el-radio-group`(仅 73 显示);`getList` 传 `view_scope`;切换重置页码并重新请求 |
| 前端 | `final_list.vue` | 同上(`is_final=1`) |
| 后端 | `GET /crm/stock/order/list` | 接收 `view_scope`;token 用户 ∈ {73} 且 `=all` 时返回全量,否则按现状过滤 |
## 6. 验收标准
1. 以非 73 账号登录两个列表页:不出现「全部/我的」控件,数据范围与改动前一致。
2. 以 73 账号登录:
- 默认显示「我的」选中态,列表为 73 自己的数据。
- 切到「全部」,列表变为全公司数据;切回「我的」恢复。
- 切换时分页重置到第 1 页。
3. 用任意账号在浏览器手动把请求改成 `view_scope=all`:后端只返回该账号自己的数据(非 73 不越权)。
## 7. 未做项 / 后续 TODO
- 前端 `userId === 73` 为硬编码,加人需改前端发版。后续建议:`getUserInfo` 下发权限标记(如 `stock@view_all` / `final_stock@view_all`),前端用现有 `hasAuth(...)` 控制显隐;后端白名单同步改为权限点判断。本次保留此扩展点,先不动。
- `view_scope` 选择不持久化(每次进页面回到默认「我的」)。
@@ -1,104 +0,0 @@
# 备货单车辆明细附件上传 — 后端对接说明
> 前端已完成:添加备货单页,每辆车行有「上传附件」入口。本文档仅说明后端需配合的部分。
## 1. 功能说明
备货单(`stock_orders`)的车辆明细(`stock_order_items`),**每辆车**需要支持上传若干附件(如该车相关单据)。附件随车辆一起保存、详情回显。
## 2. 数据库(已就绪)
`stock_order_items` 表已新增 `attachment` 字段。
- 字段名:`attachment`
- 类型:建议 `varchar` / `text` / `json`,存 **JSON 字符串**
- 含义:该车辆的附件列表
## 3. 字段格式约定
`attachment` 存「对象数组」的 JSON:
```json
[
{ "url": "https://xxx.com/file.pdf", "original_name": "装箱单.pdf" }
]
```
| 字段 | 类型 | 说明 |
|---|---|---|
| `url` | string | 文件访问地址(即 `/ticket/upload` 返回的 `file_url`) |
| `original_name` | string | 原始文件名 |
- 每辆车 0~N 个附件;无附件时为空数组 `[]` 或 `null`。
> **重要**:此格式与备货单级 `contract` 字段**完全一致**(`contract` 也是 `[{url, original_name}]`)。后端可直接复用现有处理 `contract` 的读写逻辑,区别仅在于:`attachment` 挂在 `veh_list` 每一项、落到 `stock_order_items` 表。
## 4. 需要修改的接口
均在前端调用路径 `/crm/stock/order/` 下。
### 4.1 新增 `POST /crm/stock/order/store`
- 请求体 `veh_list` 每一项现在多一个 `attachment` 字段(`[{url, original_name}]`)。
- 解析 `veh_list` 入库时,把每项 `attachment` 写入对应 `stock_order_items` 行的 `attachment` 字段(JSON 序列化)。
### 4.2 编辑 `POST /crm/stock/order/update`
- 同 `store`。
### 4.3 详情 `GET /crm/stock/order/show`
- 返回的 `veh_list` 每一项需带上 `attachment`(`[{url, original_name}]`)。
### 4.4 详情 `GET /crm/stock/order/detail`
- 同 `show`,返回 `veh_list` 每一项带 `attachment`。
## 5. 请求 / 响应示例
**提交(store / update)payload:**
```jsonc
{
// ...备货单级字段(含 contract / pi_doc / transaction_slip)
"veh_list": [
{
"sku_id": "123",
"model_snapshot": "丰田 卡罗拉",
// ...其他车辆字段
"attachment": [
{ "url": "https://xxx/a.pdf", "original_name": "装箱单.pdf" }
]
}
],
"account_list": [],
"approval_status": 0
}
```
**详情(show / detail)响应:**
```jsonc
{
"veh_list": [
{
"sku_id": "123",
"model_snapshot": "丰田 卡罗拉",
"attachment": [
{ "url": "https://xxx/a.pdf", "original_name": "装箱单.pdf" }
]
}
]
}
```
## 6. 上传接口(无需改动)
`/ticket/upload`(通用文件上传)保持原样。前端已用它上传文件,拿到 `{ file_url, file_name, original_name }`,再自行拼成 `{ url, original_name }` 存进 `attachment`。**后端无需在上传接口做任何业务关联。**
## 7. 验收标准
1. 前端添加备货单、每辆车上传附件 → 提交 → `stock_order_items.attachment` 正确落库(JSON 数组)。
2. 调 `show` / `detail`,返回的 `veh_list` 每项 `attachment` 与落库内容一致。
3. (编辑页后续加 UI 后)编辑备货单时,已存附件可回显、可增删。
## 8. 备注
- 本次前端只在「添加备货单」页加了上传 UI;编辑页(`stocking_edit`)共用同一套提交/回填逻辑,数据层已支持 `attachment`,编辑页 UI 后续补,**不影响后端接口契约**。
- 终版(final)备货单如需同等能力,接口字段一致,可一并支持。
-1
View File
@@ -59,7 +59,6 @@
"@wangeditor/editor-for-vue": "next",
"axios": "^1.7.5",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.21",
"echarts": "^5.4.0",
"echarts-gl": "^2.0.9",
"element-plus": "^2.8.0",
+5387 -1895
View File
File diff suppressed because it is too large Load Diff
+30 -18
View File
@@ -152,17 +152,6 @@
// 监听ws消息,用于系统级通知
mittBus.on('ws:message', handleWsMessageNotification)
// 初始化菜单徽标系统(全局初始化,避免重复)
import('@/composables/useMenuBadge').then(({ useMenuBadge }) => {
const { initBadgeSystem } = useMenuBadge()
initBadgeSystem()
})
// 加载菜单徽标测试工具(仅开发环境)
if (import.meta.env.DEV) {
import('@/utils/menuBadgeTest')
}
})
// 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新)
@@ -177,12 +166,6 @@
) {
initWebSocket(token)
}
// 重新初始化徽标系统(用户重新登录时)
import('@/composables/useMenuBadge').then(({ useMenuBadge }) => {
const { initBadgeSystem } = useMenuBadge()
initBadgeSystem()
})
} else {
// 退出登录时,关闭并重置
const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient')
@@ -196,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)
@@ -236,7 +248,7 @@
.el-dialog__footer {
background: transparent !important;
position: relative;
z-index: inherit;
z-index: 1;
}
.el-dialog__header {
display: flex;
-122
View File
@@ -1,122 +0,0 @@
import request from '@/utils/http'
export class invitemService {
// 物品列表
static getinvitemlist(params: Api.Common.PaginatingParams) {
return request.get<Api.Common.CommonListData>({
url: '/admin/inv/item/list',
params
})
}
// 添加物品
static invitemadd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/inv/item/store',
params
})
}
// 修改物品
static invitemupdate(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/inv/item/update',
params
})
}
// 删除物品
static invitemdelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/inv/item/destroy',
params
})
}
// 借用审批相关接口
static getBorrowList(params: Api.Common.PaginatingParams) {
return request.get<Api.Common.CommonListData>({
url: '/admin/inv/requisition/list',
params
})
}
// 借用申请详情
static getBorrowDetail(params: { id: number }) {
return request.get<any>({
url: '/admin/inv/requisition/show',
params
})
}
// 借用审批
static borrowApproval(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/inv/requisition/approval',
params
})
}
// 归还审批列表
static getReturnList(params: Api.Common.PaginatingParams) {
return request.get<Api.Common.CommonListData>({
url: '/mobile/inv/requisition/return/list',
params
})
}
// 归还申请详情
static getReturnDetail(params: { id: number }) {
return request.get<any>({
url: '/admin/inv/requisition/return/show',
params
})
}
// 确认归还
static returnconfirm(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/inv/requisition/return/confirm',
params
})
}
// 生成二维码
static getQrCode(params: { id: number; qr_claim_limit?: number }) {
return request.post<Api.Common.AddResponse>({
url: '/admin/inv/item/qr-code',
params
})
}
// 删除二维码
static deleteQrCode(params: { id: number }) {
return request.post<Api.Common.AddResponse>({
url: '/admin/inv/item/qr-code/delete',
params
})
}
// 管理员带领
static proxyClaim(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/requisition/proxy-claim',
params
})
}
// 获取库存变化记录
static getStockLogList(params: {
item_id: number
current?: number
size?: number
change_type?: number
}) {
return request.get<Api.Common.CommonListData>({
url: '/admin/stock-log/list',
params
})
}
// 获取扫码记录
static getQrClaimsList(params: {
item_id: number
current?: number
size?: number
claim_typeid?: number
start_time?: string
end_time?: string
}) {
return request.get<Api.Common.CommonListData>({
url: '/admin/requisition/qr-claims',
params
})
}
}
-35
View File
@@ -1,35 +0,0 @@
import request from '@/utils/http'
export class dashboardService {
//卡片统计
static officeclerkcard() {
return request.get<any>({
url: '/admin/dashboard/officeclerk/card'
})
}
//待审批
static officeclerkpending() {
return request.get<any>({
url: '/admin/dashboard/officeclerk/approval'
})
}
//业务员备货单统计
static salestockingcard() {
return request.get<any>({
url: '/admin/dashboard/officeclerk/curve'
})
}
//待认领
static officeclerkunclaimed(params: any) {
return request.get<any>({
url: '/admin/dashboard/officeclerk/claim',
params
})
}
//财务仪表盘统计
static officeclerkchart() {
return request.get<any>({
url: '/admin/fin/statistics'
})
}
}
+1 -22
View File
@@ -67,28 +67,7 @@ export class financeService {
//付款申请-删除
static deletePayment(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/payment/delete',
params
})
}
// 绑定付款记录到收款记录
static bindPaymentRecord(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/receipt/payment',
params
})
}
//收款记录历史
static getReceiptHistory(params: any) {
return request.get<any>({
url: '/admin/receipt/his/list',
params
})
}
//确认收款
static confirmReceipt(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/receipt/credit',
url: '/admin/payment/destroy',
params
})
}
-46
View File
@@ -1,46 +0,0 @@
import request from '@/utils/http'
export class logService {
// 更新日志-列表
static getChangelogList(params:any) {
return request.get<Api.Common.CommonListData>({
url: '/admin/update/log/list',
params
})
}
// 更新日志-添加
static addChangelog(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/update/log/store',
params
})
}
// 更新日志-删除
static deleteChangelog(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/update/log/destroy',
params
})
}
// 更新日志-详情
static detailChangelog(params: any) {
return request.get<any>({
url: '/admin/update/log/show',
params
})
}
// 更新日志-修改
static updateChangelog(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/update/log/update',
params
})
}
//操作日志-列表
static getOperationLogList(params: any) {
return request.get<Api.Common.CommonListData>({
url: '/admin/op/log/list',
params
})
}
}
-32
View File
@@ -1,32 +0,0 @@
import request from '@/utils/http'
export class userConService {
//分组列表
static getGroupList(params: Api.Common.PaginatingParams) {
return request.get<Api.Common.CommonListData>({
url: '/admin/user/group/list',
params
})
}
//添加分组
static addGroup(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/user/group/store',
data: params
})
}
//编辑分组
static editGroup(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/user/group/update',
data: params
})
}
//删除分组
static deleteGroup(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/user/group/destroy',
data: params
})
}
}
-7
View File
@@ -298,13 +298,6 @@ export class manageService {
params
})
}
//备货单删除
static delectStock(params: any) {
return request.get<any>({
url: '/admin/stock/order/destroy',
params
})
}
//打印
static printStock(params: any) {
return request.get<any>({
-48
View File
@@ -61,13 +61,6 @@ export class commonService {
params
})
}
//车型下拉列表
static getModelsSelect(params: any) {
return request.get<any>({
url: '/crm/vehicle/sku/preset',
params
})
}
//港口下拉列表
static getPortSelect(params: any) {
return request.get<any>({
@@ -132,13 +125,6 @@ export class commonService {
url: '/common/preset/users'
})
}
//用户下拉框
static getUserSelectparams(params: any) {
return request.get<any>({
url: '/common/preset/users',
params
})
}
//签名 编辑
static signatureEdit(params: any) {
return request.post<any>({
@@ -146,38 +132,4 @@ export class commonService {
params
})
}
//人员分组场景
static getUserGroupScene() {
return request.get<any>({
url: '/common/preset/usergroup/scene'
})
}
//人员分组
static getUserGroupSelect(params: any) {
return request.get<any>({
url: '/common/preset/usergroup',
params
})
}
//关键字抓取车辆
static getcrawlKeyword(params: any) {
return request.get<any>({
// url: '/common/brand/series/crawlKeyword',
url: '/common/brand/series/crawl',
params
})
}
//关键字抓取车辆保存
static crawlModelCreate(params: any) {
return request.post<Api.Work.AddResponse>({
url: '/common/brand/series/crawlKeywordCreate',
params
})
}
//仪表盘选择下拉
static getDashboardSelect() {
return request.get<any>({
url: '/admin/resources/list/menu?is_home=1'
})
}
}
-35
View File
@@ -285,39 +285,4 @@ export class CrmService {
params
})
}
//备货单撤回
static stockrecall(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/stock/order/revoke/approval',
params
})
}
//备货单作废
static stockinvalid(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/stock/order/nullify/approval',
params
})
}
//备货单删除
static stockdelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/stock/order/destroy',
params
})
}
//审核详情
static stockapprovaldetailborder(params: any) {
return request.get<any>({
url: '/crm/approval/records/border',
params
})
}
//审批转交
static stockapprovaltransfer(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/approval/records/transfer',
params
})
}
}
-123
View File
@@ -95,127 +95,4 @@ export class vehicleService {
params
})
}
// 港口管理
static getPortList(params: Api.Common.PaginatingParams & any) {
return request.get<Api.Common.CommonListData>({
url: '/common/port/list',
params
})
}
static portAdd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/port/store',
params
})
}
static portDetail(params: any) {
return request.get<any>({
url: '/common/port/show',
params
})
}
static portEdit(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/port/update',
params
})
}
static portDelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/port/destroy',
params
})
}
// 车辆品牌列表
static getBrandList(params: Api.Common.PaginatingParams & any) {
return request.get<Api.Common.CommonListData>({
url: '/common/brand/list',
params
})
}
// 车辆品牌新增/详情/编辑/删除
static brandAdd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/store',
params
})
}
static brandDetail(params: any) {
return request.get<any>({
url: '/common/brand/show',
params
})
}
static brandEdit(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/update',
params
})
}
static brandDelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/destroy',
params
})
}
// 车辆系列列表
static getSeriesList(params: Api.Common.PaginatingParams & any) {
return request.get<Api.Common.CommonListData>({
url: '/common/brand/series/list',
params
})
}
// 车辆系列新增/详情/编辑/删除
static seriesAdd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/store',
params
})
}
static seriesDetail(params: any) {
return request.get<any>({
url: '/common/brand/series/show',
params
})
}
static seriesEdit(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/update',
params
})
}
static seriesDelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/destroy',
params
})
}
// 车系采集相关接口
static collectVehicleSeries(params: any) {
return request.get<any>({
url: '/common/brand/crawlSeries',
params
})
}
static saveVehicleSeries(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/crawlSeriesCreate',
params
})
}
// 车型采集相关接口
static collectVehicleModels(params: any) {
return request.get<any>({
url: '/common/brand/series/crawlModels',
params
})
}
static saveVehicleModels(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/crawlModelCreate',
params
})
}
}
-35
View File
@@ -310,39 +310,4 @@ export class systemService {
params
})
}
//仪表盘设置列表
static getDashboardList(params: any) {
return request.get<Api.Common.CommonListData>({
url: '/admin/dashboard/rule/list',
params
})
}
//仪表盘设置新增
static dashboardAdd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/dashboard/rule/store',
params
})
}
//仪表盘设置详情
static dashboardDetail(params: any) {
return request.get<any>({
url: '/admin/dashboard/rule/show',
params
})
}
//仪表盘设置编辑
static dashboardEdit(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/dashboard/rule/update',
params
})
}
//仪表盘设置删除
static dashboardDelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/dashboard/rule/destroy',
params
})
}
}
-28
View File
@@ -193,32 +193,4 @@ export class vehicleService {
params
})
}
// 车系采集相关接口
static collectVehicleSeries(params: any) {
return request.get<any>({
url: '/common/brand/crawlSeries',
params
})
}
static saveVehicleSeries(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/crawlSeriesCreate',
params
})
}
// 车型采集相关接口
static collectVehicleModels(params: any) {
return request.get<any>({
url: '/common/brand/series/crawlModels',
params
})
}
static saveVehicleModels(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/crawlModelCreate',
params
})
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

+1 -1
View File
@@ -6,7 +6,7 @@
//
'white': #ffffff,
'black': #000000,
'success': ('base': #00A43F),
'success': ('base': #13deb9),
'warning': ('base': #ffae1f),
'danger': ('base': #ff4d4f),
'error': ('base': #fa896b)
+1 -1
View File
@@ -8,7 +8,7 @@
//
'white': #ffffff,
'black': #000000,
'success': ('base': #00A43F),
'success': ('base': #13deb9),
'warning': ('base': #ffae1f),
'danger': ('base': #ff4d4f),
'error': ('base': #fa896b)
@@ -1,284 +0,0 @@
<template>
<div class="multi-file-upload">
<!-- 上传按钮 -->
<el-upload
class="upload-box"
:action="actionImageUplode"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleUpload"
:before-upload="beforeUpload"
multiple
:accept="accept"
>
<div class="upload-trigger">
<img class="upload-icon" :src="iconSrc" />
<div class="upload-text">
<span>{{ uploadText }}</span>
<span v-if="showHint" class="upload-hint">{{ hintText }}</span>
</div>
</div>
</el-upload>
<!-- 文件列表显示 -->
<div v-if="fileList.length > 0" class="files-list">
<div v-for="(file, index) in fileList" :key="index" class="file-item">
<div class="file-info">
<img v-if="isImageFile(file)" :src="file.url" class="file-preview-image" />
<div v-else class="file-icon">
<i class="el-icon-document"></i>
</div>
<span class="file-name">{{ file.name }}</span>
</div>
<div class="file-actions">
<el-button type="text" size="small" @click="filePreview(file)" icon="el-icon-download">
预览
</el-button>
<el-button type="text" size="small" @click="downloadFile(file)" icon="el-icon-download">
下载
</el-button>
<el-button
type="text"
size="small"
@click="removeFile(index)"
icon="el-icon-delete"
style="color: #f56c6c"
>
删除
</el-button>
</div>
</div>
</div>
<files-previews v-model="fileshow" :file="filepviews" @close="onClose" />
</div>
</template>
<script setup lang="ts" name="MultiFileUpload">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import FilesPreviews from '@/components/custom/files_previews.vue'
const actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=stock_orders`
const { uploadHeaders } = useUploadHeaders()
const fileshow = ref(false)
const filepviews = ref({} as FileItem)
function onClose() {
console.log('文件预览已关闭')
}
interface FileItem {
name: string
url: string
file_name: string
}
interface Props {
modelValue: FileItem[]
uploadText?: string
hintText?: string
showHint?: boolean
iconSrc?: string
accept?: string
maxSize?: number // MB
}
interface Emits {
(e: 'update:modelValue', value: FileItem[]): void
(e: 'change', value: FileItem[]): void
}
const props = withDefaults(defineProps<Props>(), {
uploadText: '点击上传文件',
hintText: '支持多个文件上传',
showHint: true,
iconSrc: '/src/assets/img/crm/contract.png',
accept: '.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.txt',
maxSize: 10
})
const emit = defineEmits<Emits>()
const fileList = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', value)
emit('change', value)
}
})
const filePreview = (file: FileItem) => {
filepviews.value = file
fileshow.value = true
}
const isImageFile = (file: FileItem) => {
const stripQuery = (s?: string) => (s ? s.split('?')[0] : '')
const byExt = (s?: string) => /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(stripQuery(s || ''))
// Prefer extension checks on url, then file_name, then name
if (byExt(file.url)) return true
if (byExt(file.file_name)) return true
if (byExt(file.name)) return true
// Fallback: some backends may return mime in file_name like 'image/jpeg'
return (file.file_name || '').toLowerCase().startsWith('image/')
}
const beforeUpload = (file: File) => {
const isValidType = new RegExp(props.accept.replace(/\./g, '\\.').replace(/,/g, '|'), 'i').test(
file.name
)
if (!isValidType) {
ElMessage.error(`只能上传${props.accept}格式的文件!`)
return false
}
const isLtMaxSize = file.size / 1024 / 1024 < props.maxSize
if (!isLtMaxSize) {
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB!`)
return false
}
return true
}
const handleUpload = (response: any) => {
if (response.code === 200) {
const fileInfo: FileItem = {
name: response.data.original_name || '文件',
url: response.data.file_url,
file_name: response.data.file_name
}
const newFileList = [...fileList.value, fileInfo]
fileList.value = newFileList
ElMessage.success('文件上传成功!')
} else {
ElMessage.error(response.message || '文件上传失败!')
}
}
const removeFile = (index: number) => {
const newFileList = [...fileList.value]
newFileList.splice(index, 1)
fileList.value = newFileList
ElMessage.success('文件已删除')
}
const downloadFile = (file: FileItem) => {
// 创建下载链接
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
// 导出组件
defineExpose({})
</script>
<style scoped lang="scss">
.multi-file-upload {
.upload-box {
width: 100%;
}
.upload-trigger {
width: 100%;
height: 120px;
// border: 2px dashed #d9d9d9;
// border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
// background: #fafafa;
&:hover {
border-color: #409eff;
background: #f0f9ff;
}
}
.upload-icon {
width: 48px;
height: 48px;
margin-bottom: 8px;
}
.upload-text {
display: flex;
flex-direction: column;
align-items: center;
font-size: 14px;
color: #606266;
}
.upload-hint {
font-size: 12px;
color: #999;
margin-top: 4px;
}
// 文件列表样式
.files-list {
margin: 16px 0;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 6px;
margin-bottom: 8px;
background: #fafafa;
transition: all 0.3s;
&:hover {
background: #f0f9ff;
border-color: #409eff;
}
}
.file-info {
display: flex;
align-items: center;
flex: 1;
}
.file-preview-image {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 4px;
margin-right: 12px;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: #f5f7fa;
border-radius: 4px;
margin-right: 12px;
border: 1px dashed #d9d9d9;
i {
font-size: 20px;
color: #409eff;
}
}
.file-name {
font-size: 14px;
color: #606266;
word-break: break-all;
}
.file-actions {
display: flex;
gap: 8px;
}
}
</style>
@@ -334,8 +334,13 @@
const goPage = (path: string) => {
router.push(path)
}
const { systemsid } = useUserStore()
const gouserInfo = () => {
router.push('/admin/user/user_info')
if (systemsid == '1') {
router.push('/admin/user/user_info')
} else {
router.push('/system/user-center')
}
}
// const toDocs = () => {
@@ -32,10 +32,7 @@
v-html="item.meta.icon"
></i>
<span>{{ formatMenuTitle(item.meta.title) }}</span>
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge"></div>
<div v-if="getMenuTextBadge(item.path)" class="text-badge">
{{ getMenuTextBadge(item.path) }}
</div>
<div class="badge" v-if="item.meta.showBadge"></div>
</el-menu-item>
</template>
@@ -44,7 +41,6 @@
import { AppRouteRecord } from '@/types/router'
import { handleMenuJump } from '@/utils/navigation'
import { formatMenuTitle } from '@/router/utils/utils'
import { useMenuBadge } from '@/composables/useMenuBadge'
const props = defineProps({
item: {
@@ -64,9 +60,6 @@
const emit = defineEmits(['close'])
// 使用菜单徽标系统
const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge()
// 计算当前项是否有子菜单
const hasChildren = computed(() => {
return props.item.children && props.item.children.length > 0
@@ -112,17 +112,16 @@
top: 0;
right: 12px;
bottom: 0;
min-width: 16px;
height: 16px;
// padding: 0 5px;
min-width: 20px;
height: 20px;
padding: 0 5px;
margin: auto;
font-size: 10px;
line-height: 16px;
font-size: 12px;
line-height: 20px;
color: #fff;
text-align: center;
background: #fd4e4e;
// border-radius: 5px;
border-radius: 50%;
border-radius: 5px;
}
.header {
@@ -5,10 +5,7 @@
<template #title>
<MenuItemIcon :icon="item.meta.icon" :color="theme?.iconColor" />
<span class="menu-name">{{ formatMenuTitle(item.meta.title) }}</span>
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge" style="right: 35px" />
<div v-if="getMenuTextBadge(item.path)" class="text-badge" style="right: 35px">
{{ getMenuTextBadge(item.path) }}
</div>
<div v-if="item.meta.showBadge" class="badge" style="right: 35px" />
</template>
<SidebarSubmenu
:list="item.children"
@@ -29,9 +26,9 @@
<MenuItemIcon :icon="item.meta.icon" :color="theme?.iconColor" />
<template #title>
<span class="menu-name">{{ formatMenuTitle(item.meta.title) }}</span>
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge" />
<div v-if="getMenuTextBadge(item.path)" class="text-badge">
{{ getMenuTextBadge(item.path) }}
<div v-if="item.meta.showBadge" class="badge" />
<div v-if="item.meta.showTextBadge" class="text-badge">
{{ item.meta.showTextBadge }}
</div>
</template>
</el-menu-item>
@@ -43,7 +40,6 @@
import type { AppRouteRecord } from '@/types/router'
import { formatMenuTitle } from '@/router/utils/utils'
import { handleMenuJump } from '@/utils/navigation'
import { useMenuBadge } from '@/composables/useMenuBadge'
// 类型定义
interface Props {
@@ -70,9 +66,6 @@
(e: 'close'): void
}>()
// 使用菜单徽标系统
const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge()
// 计算属性
const filteredMenuItems = computed(() => filterRoutes(props.list))
+1 -5
View File
@@ -14,7 +14,6 @@
v-loading="loading"
:data="tableData"
:row-key="rowKey"
:row-class-name="rowClassName"
:height="height"
:max-height="maxHeight"
:show-header="showHeader"
@@ -124,8 +123,6 @@
marginTop?: number
/** 表格大小 */
size?: 'small' | 'default' | 'large'
/** 行样式类名函数,透传给 el-table 的 row-class-name */
rowClassName?: (args: { row: any; rowIndex: number }) => string
}
const props = withDefaults(defineProps<TableProps>(), {
@@ -149,8 +146,7 @@
paginationSize: 'default',
paginationLayout: '',
showHeaderBackground: null,
marginTop: 20,
rowClassName: undefined
marginTop: 20
})
/*
+2 -12
View File
@@ -24,9 +24,7 @@
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
<script setup lang="ts">
interface ApprovalRecordItem {
name?: string
role?: string
@@ -38,15 +36,7 @@
comment?: string
}
export default defineComponent({
name: 'ApprovalTimeline',
props: {
records: {
type: Array as () => ApprovalRecordItem[],
default: () => []
}
}
})
defineProps<{ records: ApprovalRecordItem[] }>()
</script>
<style scoped lang="scss">
@@ -1,554 +0,0 @@
<template>
<el-dialog
v-model="visible"
title="详情"
width="800px"
:close-on-click-modal="false"
@close="handleClose"
>
<div class="payment-detail-dialog" v-loading="loading">
<div v-if="paymentData" class="detail-content">
<!-- 基本信息 -->
<div class="section">
<div class="section-title">
<span class="border"></span>
<span class="title">基本信息</span>
</div>
<div class="info-grid">
<div class="info-item">
<label>创建日期:</label>
<span>{{ formatDate(paymentData.created_at) }}</span>
</div>
<!-- <div class="info-item">
<label>付款来源:</label>
<span>{{ paymentData.country_name || '—' }}</span>
</div> -->
<div class="info-item">
<label>汇款人:</label>
<span>{{ paymentData.remitter || '—' }}</span>
</div>
<div class="info-item">
<label>账户:</label>
<span>{{ paymentData.account_name || '—' }}</span>
</div>
<div class="info-item">
<label>币种:</label>
<span>{{ paymentData.currency_name || '—' }}</span>
</div>
<div class="info-item">
<label>金额:</label>
<span class="amount">{{ paymentData.amount || '—' }}</span>
</div>
<div class="info-item">
<label>外汇到账回单:</label>
<span>
<el-image
v-if="paymentData.fx_receipt_file_full"
:src="paymentData.fx_receipt_file_full"
:preview-src-list="[paymentData.fx_receipt_file_full]"
style="width: 100px; height: 100px"
/>
</span>
</div>
</div>
</div>
<!-- 关联信息 -->
<div class="section" v-if="paymentData.payment_apply_id">
<div class="section-title">
<span class="border"></span>
<span class="title">关联信息</span>
</div>
<div class="info-grid">
<div class="info-item">
<label>付款申请ID:</label>
<span>{{ paymentData.payment_apply_id }}</span>
</div>
<div class="info-item">
<label>阿里订单号/PI号:</label>
<span>{{ paymentData.payment_apply.stock[0].pi_no }}</span>
</div>
<div class="info-item">
<label>业务员:</label>
<span>{{ paymentData.payment_apply.sales_user_name || '—' }}</span>
</div>
<div class="info-item">
<label>付款公司:</label>
<span>{{ paymentData.payment_apply.account_name || '—' }}</span>
</div>
<div class="info-item">
<label>付款来源:</label>
<span>{{ paymentData.payment_apply.country_name || '—' }}</span>
</div>
<div class="info-item">
<label>汇款人:</label>
<span>{{ paymentData.payment_apply.remitter || '—' }}</span>
</div>
<div class="info-item">
<label>账户:</label>
<span>{{ paymentData.payment_apply.account_short_desc || '—' }}</span>
</div>
<div class="info-item">
<label>币种:</label>
<span>{{ paymentData.payment_apply.currency_name || '—' }}</span>
</div>
<div class="info-item">
<label>汇率:</label>
<span>{{ paymentData.payment_apply.exchange_rate || '—' }}</span>
</div>
<div class="info-item">
<label>总金额:</label>
<span>{{ paymentData.payment_apply.amount || '—' }}</span>
</div>
<div class="info-item">
<label>汇款行:</label>
<span>{{ paymentData.payment_apply.swift || '—' }}</span>
</div>
<div class="info-item">
<label>客户付款水单:</label>
<span>
<el-image
v-if="paymentData.payment_apply.voucher_file_full"
:src="paymentData.payment_apply.voucher_file_full"
:preview-src-list="[paymentData.payment_apply.voucher_file_full]"
style="width: 100px; height: 100px"
/>
</span>
</div>
<div class="info-item">
<label>入账资料:</label>
<div v-if="paymentData.payment_apply.submit_files.length > 0" class="files-list">
<div
v-for="(file, index) in paymentData.payment_apply.submit_files"
:key="index"
class="file-item"
>
<div v-if="isImageFile(file.file_full)" class="image-preview">
<img
:src="file.file_full"
:alt="file.file_name"
class="preview-image"
@click="previewImage(file)"
/>
<div class="file-info">
<span class="file-name">{{ file.file_name }}</span>
<div class="file-actions">
<!-- <el-button type="text" size="small" @click="previewImage(file)">预览</el-button> -->
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
</div>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ file.file_name }}</span>
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 收款记录关联 -->
<div
class="section"
v-if="paymentData.receipt_records && paymentData.receipt_records.length"
>
<div class="section-title">
<span class="border"></span>
<span class="title">关联收款记录</span>
</div>
<el-table :data="paymentData.receipt_records" style="margin-top: 12px">
<el-table-column label="收款记录ID" prop="id" min-width="120" />
<el-table-column label="创建日期" prop="created_at" min-width="140">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="金额" prop="amount" min-width="120">
<template #default="scope">
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
</template>
</el-table-column>
<el-table-column label="状态" prop="status" min-width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 备注信息 -->
<div class="section" v-if="paymentData.remark">
<div class="section-title">
<span class="border"></span>
<span class="title">备注信息</span>
</div>
<div class="remark-content">
{{ paymentData.remark }}
</div>
</div>
</div>
<!-- 无数据状态 -->
<div v-else-if="!loading" class="no-data">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无数据</span>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { financeService } from '@/api/admin/finance'
interface PaymentRecord {
id: string | number
created_at: string
country_name: string
remitter: string
account_name: string
currency_name: string
amount: number
status: string
payment_apply_id?: any
receipt_records?: any[]
payment_apply?: any
remark?: string
fx_receipt_file_full?: string
}
interface Props {
modelValue: boolean
paymentId?: string | number
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const loading = ref(false)
const paymentData = ref<PaymentRecord | null>(null)
// 获取付款记录详情
const getPaymentDetail = async () => {
if (!props.paymentId) return
loading.value = true
try {
const res = await financeService.getReceiptDetail({ id: props.paymentId })
paymentData.value = res
} catch (error) {
console.error('获取付款记录详情失败:', error)
paymentData.value = null
} finally {
loading.value = false
}
}
// 关闭对话框
const handleClose = () => {
visible.value = false
paymentData.value = null
}
// 打印功能
const handlePrint = () => {
window.print()
}
// 格式化日期
const formatDate = (dateStr: string) => {
if (!dateStr) return '—'
try {
return new Date(dateStr).toLocaleString('zh-CN')
} catch {
return dateStr
}
}
// 格式化金额
const formatAmount = (amount: number, currency: string) => {
return `${currency || ''} ${amount?.toLocaleString() || 0}`
}
// 获取状态类型
const getStatusType = (status: string): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
const statusMap: Record<string, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
available: 'success',
used: 'info',
pending: 'warning',
cancelled: 'danger',
bound: 'success',
unbound: 'info'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'available': '可用',
'used': '已使用',
'pending': '待处理',
'cancelled': '已取消',
'bound': '已绑定',
'unbound': '未绑定'
}
return statusMap[status] || status
}
// 判断是否为图片文件
const isImageFile = (file: any) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: any) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = async (file: any) => {
try {
// 对所有文件类型都使用 fetch 下载,确保真正的下载
const response = await fetch(file.file_full, {
method: 'GET',
headers: {
Accept: '*/*'
}
})
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = file.name || file.file_name || 'download'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('下载失败:', error)
const errorMessage = error instanceof Error ? error.message : '未知错误'
ElMessage.error(`下载失败: ${errorMessage}`)
}
}
// 监听对话框显示状态
watch(visible, (newVal) => {
if (newVal && props.paymentId) {
getPaymentDetail()
}
})
// 监听付款ID变化
watch(
() => props.paymentId,
(newId) => {
if (visible.value && newId) {
getPaymentDetail()
}
}
)
</script>
<style scoped lang="scss">
.payment-detail-dialog {
.detail-content {
.section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
font-weight: 600;
color: #222;
.border {
width: 4px;
height: 16px;
background: #3d7bff;
margin-right: 8px;
border-radius: 2px;
}
.title {
font-size: 16px;
}
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
.info-item {
display: flex;
align-items: center;
//padding: 8px 0;
label {
font-weight: 500;
color: #666;
min-width: 100px;
margin-right: 8px;
}
span {
color: #333;
flex: 1;
&.amount {
font-weight: 600;
color: #3d7bff;
font-size: 16px;
}
}
}
}
.remark-content {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
color: #666;
line-height: 1.6;
min-height: 60px;
}
}
}
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #999;
i {
font-size: 48px;
margin-bottom: 12px;
}
}
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 12px;
}
// 打印样式
@media print {
.dialog-footer {
display: none;
}
.payment-detail-dialog {
.section-title {
.border {
background: #000 !important;
}
}
}
}
</style>
-325
View File
@@ -1,325 +0,0 @@
<template>
<div class="file-preview-container">
<div class="form-title">
<span class="title">{{ title }}</span>
</div>
<div class="file-preview">
<div v-if="files.length > 0" class="files-list">
<div v-for="(file, index) in files" :key="index" class="file-item">
<div v-if="isImageFile(file)" class="image-preview">
<img
:src="file.url"
:alt="file.name"
class="preview-image"
@click="previewImage(file)"
/>
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<div class="file-actions">
<!-- <el-button type="text" size="small" @click="previewImage(file)">预览</el-button> -->
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
</div>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<el-button type="text" size="small" @click="previewFile(file)">预览</el-button>
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
</div>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>{{ emptyText }}</span>
</div>
</div>
<files-previews v-model="fileshow" :file="filepviews" @close="onClose" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { ElMessage } from 'element-plus'
import FilesPreviews from '@/components/custom/files_previews.vue'
interface FileItem {
name: string
url: string
file_name: string
}
export default defineComponent({
name: 'FilePreview',
components: { FilesPreviews },
props: {
title: {
type: String,
required: true
},
files: {
type: Array as () => FileItem[],
default: () => []
},
emptyText: {
type: String,
default: '暂无文件'
}
},
setup() {
const fileshow = ref(false)
const filepviews = ref({} as FileItem)
function onClose() {
fileshow.value = false
console.log('文件预览已关闭')
}
// 判断是否为图片文件
const isImageFile = (file: FileItem) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: FileItem) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = async (file: FileItem) => {
try {
// 对所有文件类型都使用 fetch 下载,确保真正的下载
const response = await fetch(file.url, {
method: 'GET',
headers: {
Accept: '*/*'
}
})
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = file.name || file.file_name || 'download'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('下载失败:', error)
const errorMessage = error instanceof Error ? error.message : '未知错误'
ElMessage.error(`下载失败: ${errorMessage}`)
}
}
const previewFile = (file: FileItem) => {
filepviews.value = file
fileshow.value = true
}
return {
fileshow,
filepviews,
previewFile,
onClose,
isImageFile,
previewImage,
downloadFile
}
}
})
</script>
<style scoped lang="scss">
.file-preview-container {
.form-title {
font-weight: bold;
color: #222;
margin-bottom: 15px;
.title {
margin-left: 6px;
}
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
.files-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.file-item {
flex: 0 0 auto;
min-width: 200px;
max-width: 300px;
}
.file-preview {
position: relative;
width: 100%;
max-height: 120px;
padding: 10px;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
background: #fafafa;
}
.image-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.preview-image {
flex: 1;
max-height: 70px;
object-fit: cover;
cursor: pointer;
transition: transform 0.2s;
}
.preview-image:hover {
transform: scale(1.05);
}
.document-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.file-icon {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.file-icon i {
font-size: 48px;
color: #409eff;
}
.file-info {
padding: 8px;
background: rgba(255, 255, 255, 0.9);
width: 100%;
display: flex;
// flex-direction: column;
align-items: center;
gap: 4px;
}
.file-actions {
display: flex;
gap: 8px;
align-items: center;
}
.file-name {
font-size: 12px;
color: #606266;
text-align: center;
word-break: break-all;
max-width: 100%;
}
}
</style>
-212
View File
@@ -1,212 +0,0 @@
<template>
<el-dialog
:model-value="visible"
@update:model-value="emit('update:visible', $event)"
title="内勤专用"
width="90%"
:close-on-click-modal="false"
>
<div style="margin-bottom: 12px; display: flex; align-items: center; gap: 12px">
<span>显示币种:</span>
<el-select v-model="selectedCurrency" placeholder="选择币种" style="width: 200px">
<el-option
v-for="opt in currencyOptions"
:key="opt.currency_code"
:label="opt.currency_name + ' (' + opt.currency_code + ')'"
:value="opt.currency_code"
/>
</el-select>
</div>
<div>
<el-table :data="displayData" style="width: 100%">
<el-table-column label="编号" width="80" align="center">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="model" label="车型" min-width="160" />
<el-table-column prop="qty" label="数量" width="80" />
<el-table-column prop="clearance_price" label="报关价" min-width="140">
<template #default="scope">{{ scope.row.clearance_price }}</template>
</el-table-column>
<el-table-column prop="fob_transport_fee" label="FOB运费" min-width="140">
<template #default="scope">{{ scope.row.fob_transport_fee }}</template>
</el-table-column>
<el-table-column prop="car_price" label="车价" min-width="140">
<template #default="scope">{{ scope.row.car_price }}</template>
</el-table-column>
<el-table-column prop="ocean_freight" label="海运费" min-width="140">
<template #default="scope">{{ scope.row.ocean_freight }}</template>
</el-table-column>
<el-table-column prop="commission" label="佣金" min-width="140">
<template #default="scope">{{ scope.row.commission }}</template>
</el-table-column>
<el-table-column prop="retention_price" label="挂账" min-width="140">
<template #default="scope">{{ scope.row.retention_price }}</template>
</el-table-column>
<el-table-column prop="port_misc_fee" label="港杂" min-width="140">
<template #default="scope">{{ scope.row.port_misc_fee }}</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { commonService } from '@/api/commonApi'
defineOptions({
name: 'InternalStaffView'
})
interface CurrencyOption {
currency_code: string
currency_name: string
rate?: number
exchange_rate?: number
}
interface VehicleData {
model_snapshot?: string
model?: string
qty?: number
clearance_price?: any
clearance_price_currency?: string
fob_transport_fee?: any
fob_fee_currency?: string
car_price?: any
car_price_currency?: string
ocean_freight?: any
ocean_freight_currency?: string
commission?: any
commission_currency?: string
retention_price?: any
retention_price_currency?: string
port_misc_fee?: any
port_fee_currency?: string
}
const props = defineProps<{
visible: boolean
vehicleData: VehicleData[]
defaultCurrency?: string
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
}>()
const selectedCurrency = ref('')
const currencyOptions = ref<CurrencyOption[]>([])
const loadCurrencyOptions = async () => {
try {
const res = await commonService.getCurrencySelect()
currencyOptions.value = res || []
} catch (e) {
console.error('获取币种失败', e)
}
}
const rateOf = (c?: string) => {
const found = (currencyOptions.value || []).find((x: any) => x.currency_code === c)
return Number(found?.rate || found?.exchange_rate || 0) || 0
}
const convert = (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 rf = rateOf(from)
const rt = rateOf(to) || 1
if (!rf) return 0
return (val * rf) / rt
}
const fmt = (n: number, cur: string) => `${n.toFixed(2)} ${cur}`
const displayData = computed(() => {
const code = selectedCurrency.value
return (props.vehicleData || []).map((it: VehicleData) => {
return {
model: it.model_snapshot || it.model || '',
qty: it.qty || 1,
clearance_price: fmt(
convert(
it.clearance_price?.amount ?? it.clearance_price,
it.clearance_price?.currency || it.clearance_price_currency,
code
),
code
),
fob_transport_fee: fmt(
convert(
it.fob_transport_fee?.amount ?? it.fob_transport_fee,
it.fob_transport_fee?.currency || it.fob_fee_currency || it.ocean_freight_currency,
code
),
code
),
car_price: fmt(
convert(
it.car_price?.amount ?? it.car_price,
it.car_price?.currency || it.car_price_currency,
code
),
code
),
ocean_freight: fmt(
convert(
it.ocean_freight?.amount ?? it.ocean_freight,
it.ocean_freight?.currency || it.ocean_freight_currency,
code
),
code
),
commission: fmt(
convert(
it.commission?.amount ?? it.commission,
it.commission?.currency || it.commission_currency,
code
),
code
),
retention_price: fmt(
convert(
it.retention_price?.amount ?? it.retention_price,
it.retention_price?.currency || it.retention_price_currency,
code
),
code
),
port_misc_fee: fmt(
convert(
it.port_misc_fee?.amount ?? it.port_misc_fee,
it.port_misc_fee?.currency || it.port_fee_currency,
code
),
code
)
}
})
})
// 监听 visible 变化,加载币种选项
watch(
() => props.visible,
(newVisible) => {
if (newVisible) {
loadCurrencyOptions()
// 设置默认币种
if (props.defaultCurrency) {
selectedCurrency.value = props.defaultCurrency
} else {
selectedCurrency.value = 'USD'
}
}
}
)
</script>
@@ -1,210 +0,0 @@
<template>
<div style="margin-bottom: 12px; display: flex; align-items: center; gap: 12px">
<span>显示币种:</span>
<el-select v-model="selectedCurrency" placeholder="选择币种" style="width: 200px">
<el-option
v-for="opt in currencyOptions"
:key="opt.currency_code"
:label="opt.currency_name + ' (' + opt.currency_code + ')'"
:value="opt.currency_code"
/>
</el-select>
</div>
<div>
<el-table :data="displayData" style="width: 100%">
<el-table-column label="编号" width="80" align="center">
<template #default="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column prop="model" label="车型" min-width="160" />
<el-table-column prop="qty" label="数量" width="80" />
<el-table-column prop="clearance_price" label="报关价" min-width="140">
<template #default="scope">{{ scope.row.clearance_price }}</template>
</el-table-column>
<el-table-column prop="fob_transport_fee" label="FOB运费" min-width="140">
<template #default="scope">{{ scope.row.fob_transport_fee }}</template>
</el-table-column>
<el-table-column prop="car_price" label="车价" min-width="140">
<template #default="scope">{{ scope.row.car_price }}</template>
</el-table-column>
<el-table-column prop="ocean_freight" label="海运费" min-width="140">
<template #default="scope">{{ scope.row.ocean_freight }}</template>
</el-table-column>
<el-table-column prop="commission" label="佣金" min-width="140">
<template #default="scope">{{ scope.row.commission }}</template>
</el-table-column>
<el-table-column prop="retention_price" label="挂账" min-width="140">
<template #default="scope">{{ scope.row.retention_price }}</template>
</el-table-column>
<el-table-column prop="port_misc_fee" label="港杂" min-width="140">
<template #default="scope">{{ scope.row.port_misc_fee }}</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { commonService } from '@/api/commonApi'
defineOptions({
name: 'InternalStaffViews'
})
interface CurrencyOption {
currency_code: string
currency_name: string
rate?: number
exchange_rate?: number
}
interface VehicleData {
model_snapshot?: string
model?: string
qty?: number
clearance_price?: any
clearance_price_currency?: string
fob_transport_fee?: any
fob_fee_currency?: string
car_price?: any
car_price_currency?: string
ocean_freight?: any
ocean_freight_currency?: string
commission?: any
commission_currency?: string
retention_price?: any
retention_price_currency?: string
port_misc_fee?: any
port_fee_currency?: string
}
const props = defineProps<{
visible: boolean
vehicleData: VehicleData[]
defaultCurrency?: string
}>()
// const emit = defineEmits<{
// 'update:visible': [value: boolean]
// }>()
const selectedCurrency = ref('')
const currencyOptions = ref<CurrencyOption[]>([])
const loadCurrencyOptions = async () => {
try {
const res = await commonService.getCurrencySelect()
currencyOptions.value = res || []
} catch (e) {
console.error('获取币种失败', e)
}
}
const rateOf = (c?: string) => {
const found = (currencyOptions.value || []).find((x: any) => x.currency_code === c)
return Number(found?.rate || found?.exchange_rate || 0) || 0
}
const convert = (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 rf = rateOf(from)
const rt = rateOf(to) || 1
if (!rf) return 0
return (val * rf) / rt
}
const fmt = (n: number, cur: string) => `${n.toFixed(2)} ${cur}`
const displayData = computed(() => {
const code = selectedCurrency.value
return (props.vehicleData || []).map((it: VehicleData) => {
return {
model: it.model_snapshot || it.model || '',
qty: it.qty || 1,
clearance_price: fmt(
convert(
it.clearance_price?.amount ?? it.clearance_price,
it.clearance_price?.currency || it.clearance_price_currency,
code
),
code
),
fob_transport_fee: fmt(
convert(
it.fob_transport_fee?.amount ?? it.fob_transport_fee,
it.fob_transport_fee?.currency || it.fob_fee_currency || it.ocean_freight_currency,
code
),
code
),
car_price: fmt(
convert(
it.car_price?.amount ?? it.car_price,
it.car_price?.currency || it.car_price_currency,
code
),
code
),
ocean_freight: fmt(
convert(
it.ocean_freight?.amount ?? it.ocean_freight,
it.ocean_freight?.currency || it.ocean_freight_currency,
code
),
code
),
commission: fmt(
convert(
it.commission?.amount ?? it.commission,
it.commission?.currency || it.commission_currency,
code
),
code
),
retention_price: fmt(
convert(
it.retention_price?.amount ?? it.retention_price,
it.retention_price?.currency || it.retention_price_currency,
code
),
code
),
port_misc_fee: fmt(
convert(
it.port_misc_fee?.amount ?? it.port_misc_fee,
it.port_misc_fee?.currency || it.port_fee_currency,
code
),
code
)
}
})
})
loadCurrencyOptions()
// 设置默认币种
if (props.defaultCurrency) {
selectedCurrency.value = props.defaultCurrency
} else {
selectedCurrency.value = 'USD'
}
// 监听 visible 变化,加载币种选项
// watch(
// () => props.visible,
// (newVisible) => {
// if (newVisible) {
// loadCurrencyOptions()
// // 设置默认币种
// if (props.defaultCurrency) {
// selectedCurrency.value = props.defaultCurrency
// } else {
// selectedCurrency.value = 'USD'
// }
// }
// }
// )
</script>
+142 -291
View File
@@ -61,261 +61,152 @@
<ElTableColumn type="expand" width="30">
<template #default="scope">
<div style="padding: 6px">
<div style="display: flex">
<div><strong>备注:</strong></div>
<div><span v-html="scope.row.remark || '暂无备注'"></span></div>
</div>
<div
v-if="getItemAttachments(scope.row).length"
style="display: flex; margin-top: 6px"
>
<div><strong>附件:</strong></div>
<div style="display: flex; flex-wrap: wrap; gap: 12px">
<a
v-for="(f, aIdx) in getItemAttachments(scope.row)"
:key="aIdx"
:href="f.url"
target="_blank"
rel="noopener"
style="color: #3471ff; text-decoration: none"
>
<i class="iconfont-sys">&#xe814;</i> {{ f.name }}
</a>
</div>
</div>
<strong>备注:</strong>
<span>{{ scope.row.remark || '暂无备注' }}</span>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="编号" width="60" align="center">
<template #default="scope">{{ scope.$index + 1 }}</template>
</ElTableColumn>
<ElTableColumn label="车型" width="160" align="center">
<ElTableColumn label="车型" min-width="160">
<template #default="scope">
<div>{{ scope.row.model_snapshot || scope.row.model || '—' }}</div>
<div v-if="scope.row.mileage > 0">里程:{{ scope.row.mileage }}km</div>
<div v-if="scope.row.vin && scope.row.vin != ''">VIN: {{ scope.row.vin }}</div>
{{ scope.row.model_snapshot || scope.row.model || '—' }}
<span v-if="scope.row.mileage > 0">({{ scope.row.mileage }}km)</span>
</template>
</ElTableColumn>
<ElTableColumn label="数量/台" width="90" align="center">
<template #default="scope">{{ scope.row.num ?? scope.row.quantity ?? 1 }}</template>
</ElTableColumn>
<ElTableColumn label="颜色" width="120" align="center">
<ElTableColumn label="颜色/外" align="center" prop="color_exterior_snapshot" />
<ElTableColumn
label="内饰"
min-width="120"
align="center"
prop="color_interior_snapshot"
/>
<ElTableColumn label="改装" min-width="120" align="center" prop="modify" />
<ElTableColumn label="官方指导价/二手挂牌价" min-width="175" align="center">
<template #default="scope">
<div> 外饰:{{ scope.row.color_exterior_snapshot || '—' }} </div>
<div> 内饰:{{ scope.row.color_interior_snapshot || '—' }} </div>
<span v-if="scope.row.mileage > 0">
{{
scope.row.listing_price?.currency_info ||
scope.row.listing_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
<span v-else>
{{
scope.row.official_price?.currency_info ||
scope.row.official_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="费用" min-width="200" align="center">
<ElTableColumn label="优惠" min-width="120" align="center">
<template #default="scope">
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :span="8" class="item-row">
<span>官方指导价/二手挂牌价:</span>
<span v-if="scope.row.mileage > 0" class="price-num">
{{
scope.row.listing_price?.currency_info ||
scope.row.listing_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
<span v-else class="price-num">
{{
scope.row.official_price?.currency_info ||
scope.row.official_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>优惠:</span>
<span v-if="scope.row.mileage > 0" class="price-num">
{{
scope.row.reduction_price?.currency_info ||
scope.row.reduction_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
<span v-else class="price-num">
{{
scope.row.discount_price?.currency_info ||
scope.row.discount_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>手续费:</span>
<span class="price-num">
{{
scope.row.handling_fee?.currency_info ||
scope.row.handling_fee?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
</ElRow>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :span="8" class="item-row">
<span>加价:</span>
<span class="price-num">
{{
scope.row.increase_price?.currency_info ||
scope.row.increase_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>改装费:</span>
<span class="price-num">
{{
scope.row.retrofit_fee?.currency_info ||
scope.row.retrofit_fee?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>配件费:</span>
<span class="price-num">
{{
scope.row.accessory_price?.currency_info ||
scope.row.accessory_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
</ElRow>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :span="8" class="item-row">
<span>FOB运费:</span>
<span class="price-num">
{{
scope.row.fob_transport_fee?.currency_info ||
scope.row.fob_transport_fee?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>港杂费:</span>
<span class="price-num">
{{
scope.row.port_misc_fee?.currency_info ||
scope.row.port_misc_fee?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>海运费:</span>
<span class="price-num">
{{
scope.row.ocean_freight?.currency_info ||
scope.row.ocean_freight?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
</ElRow>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :span="8" class="item-row">
<span>佣金:</span>
<span class="price-num">
{{
scope.row.commission?.currency_info ||
scope.row.commission?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>挂账:</span>
<span class="price-num">
{{
scope.row.retention_price?.currency_info ||
scope.row.retention_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>购置税:</span>
<span class="price-num">
{{
scope.row.purchase_tax?.currency_info ||
scope.row.purchase_tax?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
</ElRow>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :span="8" class="item-row">
<span>预估退税额:</span>
<span class="price-num">
{{ scope.row.tax_refund?.currency_info || scope.row.tax_refund?.amount || '—' }}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>车价:</span>
<span class="price-num">
{{ scope.row.car_price?.currency_info || scope.row.car_price?.amount || '—' }}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>收款价:</span>
<span class="price-num">
{{
scope.row.unit_price?.currency_info ||
scope.row.unit_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
</ElRow>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :span="8" class="item-row">
<span>总价:</span>
<span class="price-num">
{{
scope.row.total_price?.currency_info ||
scope.row.total_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row">
<span>报关价:</span>
<span class="price-num">
{{
scope.row.clearance_price?.currency_info ||
scope.row.clearance_price?.amount ||
scope.row.clearance_price ||
'—'
}}
</span>
</ElCol>
<ElCol :span="8" class="item-row"></ElCol>
</ElRow>
<span v-if="scope.row.mileage > 0">
{{
scope.row.reduction_price?.currency_info ||
scope.row.reduction_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
<span v-else>
{{
scope.row.discount_price?.currency_info ||
scope.row.discount_price?.amount ||
scope.row.discount ||
'—'
}}
</span>
</template>
</ElTableColumn>
<ElTableColumn fixed="right" label="利润" width="150" align="center">
<ElTableColumn label="手续费" min-width="120" align="center">
<template #default="scope">{{
scope.row.handling_fee?.currency_info ||
scope.row.handling_fee?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="改装费" min-width="120" align="center">
<template #default="scope">{{
scope.row.retrofit_fee?.currency_info ||
scope.row.retrofit_fee?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="FOB运费" min-width="120" align="center">
<template #default="scope">{{
scope.row.fob_transport_fee?.currency_info ||
scope.row.fob_transport_fee?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="港杂费" min-width="120" align="center">
<template #default="scope">{{
scope.row.port_misc_fee?.currency_info ||
scope.row.port_misc_fee?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="海运费" min-width="120" align="center">
<template #default="scope">{{
scope.row.ocean_freight?.currency_info ||
scope.row.ocean_freight?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="佣金" min-width="120" align="center">
<template #default="scope">{{
scope.row.commission?.currency_info ||
scope.row.commission?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="购置税" min-width="120" align="center">
<template #default="scope">{{
scope.row.purchase_tax?.currency_info ||
scope.row.purchase_tax?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="预估退税额" min-width="120" align="center">
<template #default="scope">{{
scope.row.tax_refund?.currency_info ||
scope.row.tax_refund?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="单价" min-width="120" align="center">
<template #default="scope">{{
scope.row.unit_price?.currency_info ||
scope.row.unit_price?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="总价" min-width="120" align="center">
<template #default="scope">{{
scope.row.total_price?.currency_info ||
scope.row.total_price?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn fixed="right" label="利润" min-width="120" align="center">
<template #default="scope">{{
scope.row.profit?.currency_info ||
scope.row.profit?.amount ||
@@ -334,7 +225,11 @@
</div>
<div class="summary-item">
<span class="label">合计到岸价:</span>
<span class="value"> {{ orderData.cif_total?.currency_info || '—' }}</span>
<span class="value"
>${{
formatPrice(Number(orderData.cif_total_amount?.amount ?? orderData.totalCif ?? 0))
}}</span
>
</div>
<div class="summary-item">
<span class="label">利润:</span>
@@ -355,16 +250,12 @@
orderData.declarant_name || orderData.declarant || '—'
}}</span>
</td>
<td v-if="orderData.is_final == 0">
<td>
<span class="info-label"> 预计发运日期:</span>
<span class="info-value">{{
orderData.eta_ship_date || orderData.etaShipDate || '—'
}}</span>
</td>
<td v-else>
<span class="info-label"> 全款收齐日:</span>
<span class="info-value">{{ orderData.collect_all_date || '—' }}</span>
</td>
<td>
<span class="info-label"> 出运方式:</span>
<span class="info-value">{{
@@ -378,25 +269,19 @@
}}</span>
</td>
<td>
<span class="info-label"> 贸易条款:</span>
<span class="info-value">{{
orderData.trade_term_snapshot || orderData.tradeTerms || '—'
}}</span>
<span class="info-label"> 贸易条款/清关目的港:</span>
<span class="info-value"
>{{ orderData.trade_term_snapshot || orderData.tradeTerms || '—' }}/{{
orderData.clearance_port_snapshot || '—'
}}</span
>
</td>
<td>
<span class="info-label"> 清关目的港:</span>
<span class="info-value">{{ orderData.clearance_port_snapshot || '—' }}</span>
</td>
<td v-if="orderData.is_final == 0">
<span class="info-label"> 最迟到港时间:</span>
<span class="info-value">{{
orderData.latest_arrival_date || orderData.latestArrival || '—'
}}</span>
</td>
<td v-else>
<span class="info-label"> 提车日期:</span>
<span class="info-value">{{ orderData.pick_up_date || '—' }}</span>
</td>
</tr>
</table>
</div>
@@ -462,7 +347,7 @@
currency: p.currency_name_snapshot || '—',
account: p.account_name_snapshot || '—',
remitter: p.remitter_name || '—',
amount: p.amount || '—'
amount: `$${formatPrice(Number(p.amount || 0))}`
}))
: (props.orderData as any).payments || []
if (!list.length) return []
@@ -480,25 +365,6 @@
const vehItems = computed(() => {
return (props.orderData as any).veh_list || (props.orderData as any).items || []
})
// 车辆附件(后端返回数组;兼容历史字符串数据)
const getItemAttachments = (row: any) => {
const raw = row?.attachment
let list: any[] = []
if (Array.isArray(raw)) {
list = raw
} else if (typeof raw === 'string' && raw.trim()) {
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) list = parsed
} catch {
list = []
}
}
return list
.filter((f: any) => f && f.url)
.map((f: any) => ({ name: f.original_name || f.name || '附件', url: f.url }))
}
const tableRef = ref()
const expandWhenReady = () => {
nextTick(() => {
@@ -526,8 +392,7 @@
paymentTableData,
vehItems,
formatPrice,
tableRef,
getItemAttachments
tableRef
}
}
})
@@ -706,20 +571,6 @@
padding: 10px;
background-color: #f9f9f9;
}
.item-row-wrap {
margin: 0 !important;
}
.item-row-wrap .item-row {
border: 1px solid #e0e0e0;
padding: 5px 0;
// min-height: 40px;
text-align: left;
}
.price-num {
font-weight: bold;
color: #000;
margin-left: 5px;
}
/* 打印优化 */
@media print {
.order-detail-content {
@@ -1,380 +0,0 @@
<template>
<el-dialog
v-model="visible"
title="付款记录详情"
width="800px"
:close-on-click-modal="false"
@close="handleClose"
>
<div class="payment-detail-dialog" v-loading="loading">
<div v-if="paymentData" class="detail-content">
<!-- 基本信息 -->
<div class="section">
<div class="section-title">
<span class="border"></span>
<span class="title">基本信息</span>
</div>
<div class="info-grid">
<div class="info-item">
<label>创建日期:</label>
<span>{{ formatDate(paymentData.created_at) }}</span>
</div>
<div class="info-item">
<label>付款来源:</label>
<span>{{ paymentData.country_name || '—' }}</span>
</div>
<div class="info-item">
<label>汇款人:</label>
<span>{{ paymentData.remitter || '—' }}</span>
</div>
<div class="info-item">
<label>账户:</label>
<span>{{ paymentData.account_name || '—' }}</span>
</div>
<div class="info-item">
<label>币种:</label>
<span>{{ paymentData.currency_name || '—' }}</span>
</div>
<div class="info-item">
<label>金额:</label>
<span class="amount">{{ paymentData.amount }}</span>
</div>
<div class="info-item">
<label>付款水单:</label>
<div>
<p v-for="(item, index) in paymentData.voucher_file" :key="index">
<el-image
v-if="
/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test(
(item.file_full || '').split('?')[0]
)
"
:src="item.file_full"
:preview-src-list="item.file_full ? [item.file_full] : []"
style="width: 100px; height: 100px"
/>
<a
v-else
:href="item.file_full"
target="_blank"
rel="noopener"
style="
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
"
>{{ item.file_name }}</a
>
</p>
</div>
</div>
</div>
</div>
<!-- 关联信息 -->
<div class="section" v-if="paymentData.payment_apply_id">
<div class="section-title">
<span class="border"></span>
<span class="title">关联信息</span>
</div>
<div class="info-grid">
<div class="info-item">
<label>付款申请ID:</label>
<span>{{ paymentData.payment_apply_id }}</span>
</div>
<div class="info-item">
<label>关联状态:</label>
<el-tag :type="getStatusType(paymentData.status)">
{{ getStatusText(paymentData.status) }}
</el-tag>
</div>
</div>
</div>
<!-- 收款记录关联 -->
<div
class="section"
v-if="paymentData.receipt_records && paymentData.receipt_records.length"
>
<div class="section-title">
<span class="border"></span>
<span class="title">关联收款记录</span>
</div>
<el-table :data="paymentData.receipt_records" style="margin-top: 12px">
<el-table-column label="收款记录ID" prop="id" min-width="120" />
<el-table-column label="创建日期" prop="created_at" min-width="140">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="金额" prop="amount" min-width="120">
<template #default="scope">
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
</template>
</el-table-column>
<el-table-column label="状态" prop="status" min-width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<!-- 备注信息 -->
<div class="section" v-if="paymentData.remark">
<div class="section-title">
<span class="border"></span>
<span class="title">备注信息</span>
</div>
<div class="remark-content">
{{ paymentData.remark }}
</div>
</div>
</div>
<!-- 无数据状态 -->
<div v-else-if="!loading" class="no-data">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无数据</span>
</div>
</div>
<!-- <div class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</div> -->
</el-dialog>
</template>
<script setup lang="ts">
import { financeService } from '@/api/admin/finance'
interface PaymentRecord {
id: string | number
created_at: string
country_name: string
remitter: string
account_name: string
currency_name: string
amount: number
status: string
payment_apply_id?: string | number
receipt_records?: any[]
voucher_file?: any[]
submit_files?: string[]
remark?: string
}
interface Props {
modelValue: boolean
paymentId?: string | number
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const loading = ref(false)
const paymentData = ref<PaymentRecord | null>(null)
// 获取付款记录详情
const getPaymentDetail = async () => {
if (!props.paymentId) return
loading.value = true
try {
const res = await financeService.getPaymentDetail({ id: props.paymentId })
paymentData.value = res
} catch (error) {
console.error('获取付款记录详情失败:', error)
paymentData.value = null
} finally {
loading.value = false
}
}
// 关闭对话框
const handleClose = () => {
visible.value = false
paymentData.value = null
}
// 打印功能
const handlePrint = () => {
window.print()
}
// 格式化日期
const formatDate = (dateStr: string) => {
if (!dateStr) return '—'
try {
return new Date(dateStr).toLocaleString('zh-CN')
} catch {
return dateStr
}
}
// 格式化金额
const formatAmount = (amount: number, currency: string) => {
return `${currency || ''} ${amount?.toLocaleString() || 0}`
}
// 获取状态类型
const getStatusType = (status: string): 'success' | 'info' | 'warning' | 'danger' | 'primary' => {
const statusMap: Record<string, 'success' | 'info' | 'warning' | 'danger' | 'primary'> = {
available: 'success',
used: 'info',
pending: 'warning',
cancelled: 'danger',
bound: 'success',
unbound: 'info'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: Record<string, string> = {
'available': '可用',
'used': '已使用',
'pending': '待处理',
'cancelled': '已取消',
'bound': '已绑定',
'unbound': '未绑定'
}
return statusMap[status] || status
}
// 监听对话框显示状态
watch(visible, (newVal) => {
if (newVal && props.paymentId) {
getPaymentDetail()
}
})
// 监听付款ID变化
watch(() => props.paymentId, (newId) => {
if (visible.value && newId) {
getPaymentDetail()
}
})
</script>
<style scoped lang="scss">
.payment-detail-dialog {
.detail-content {
.section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
font-weight: 600;
color: #222;
.border {
width: 4px;
height: 16px;
background: #3d7bff;
margin-right: 8px;
border-radius: 2px;
}
.title {
font-size: 16px;
}
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
.info-item {
display: flex;
align-items: center;
// padding: 8px 0;
label {
font-weight: 500;
color: #666;
min-width: 100px;
margin-right: 8px;
}
span {
color: #333;
flex: 1;
&.amount {
font-weight: 600;
color: #3d7bff;
font-size: 16px;
}
}
}
}
.remark-content {
padding: 12px;
background: #f8f9fa;
border-radius: 6px;
color: #666;
line-height: 1.6;
min-height: 60px;
}
}
}
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 0;
color: #999;
i {
font-size: 48px;
margin-bottom: 12px;
}
}
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 12px;
}
// 打印样式
@media print {
.dialog-footer {
display: none;
}
.payment-detail-dialog {
.section-title {
.border {
background: #000 !important;
}
}
}
}
</style>
@@ -1,298 +0,0 @@
<template>
<el-dialog
v-model="visible"
title="选择付款记录"
width="80%"
:close-on-click-modal="false"
@close="handleClose"
>
<div class="payment-record-dialog">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input
v-model="searchKeyword"
placeholder="请输入汇款人、账户或金额"
clearable
style="width: 300px"
@keyup.enter="getPaymentRecords"
>
<template #suffix>
<el-icon class="clickable" @click="getPaymentRecords"><Search /></el-icon>
</template>
</el-input>
<el-button type="primary" @click="getPaymentRecords">搜索</el-button>
</div>
<!-- 付款记录表格 -->
<el-table
ref="tableRef"
:data="paymentRecords"
:loading="loading"
row-key="id"
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
highlight-current-row
style="margin-top: 16px"
>
<el-table-column label="创建日期" prop="created_at" min-width="160" />
<el-table-column label="付款来源" prop="country_name" min-width="120" />
<el-table-column label="汇款人" prop="remitter" min-width="120" />
<el-table-column label="账户" prop="account_name" min-width="120" />
<el-table-column label="账币" prop="currency_name" min-width="120" />
<el-table-column label="金额" prop="amount" min-width="120">
<template #default="scope">
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
</template>
</el-table-column>
<el-table-column label="选择" width="80">
<template #default="scope">
<el-checkbox
v-model="checkboxStates[scope.row.id]"
@change="(val) => handleCheckboxChange(scope.row, !!val)"
@click.stop
/>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :disabled="!selectedRecord" @click="handleConfirm">
确认绑定
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { financeService } from '@/api/admin/finance'
import { ElMessage } from 'element-plus'
interface PaymentRecord {
id: string | number
created_at: string
country_name: string
remitter: string
account_name: string
currency_name: string
amount: number
status: string
}
interface Props {
modelValue: boolean
receiptId?: string | number
}
interface Emits {
(e: 'update:modelValue', value: boolean): void
(e: 'confirm', record: PaymentRecord): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const loading = ref(false)
const searchKeyword = ref('')
// const paymentRecords = ref<PaymentRecord[]>([])
const paymentRecords = ref<any[]>([])
const selectedRecord = ref<PaymentRecord | null>(null)
// const selectedRecord = ref<any[]>([])
const selectedRecordId = ref<any>(null)
const checkboxStates = reactive<Record<string | number, boolean>>({})
const tableRef = ref()
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 获取付款记录列表
const getPaymentRecords = async () => {
loading.value = true
try {
const params = {
current: pagination.currentPage,
size: pagination.pageSize,
keyword: searchKeyword.value,
scene: 'correlation',
receipt_id: props.receiptId,
correlation: 'unbind' // 只显示可用的付款记录
}
const { records, current, size, total } = await financeService.getPaymentList(params)
paymentRecords.value = records || []
// 初始化 checkbox 状态
records?.forEach((record: any) => {
checkboxStates[record.id] = false
})
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取付款记录失败:', error)
ElMessage.error('获取付款记录失败')
} finally {
loading.value = false
}
}
// 处理选择变化(保留兼容性,单选模式下不使用此方法)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleSelectionChange = (_selection: PaymentRecord[]) => {
// 单选模式下不使用此方法
}
// 处理 checkbox 变化(单选逻辑)
const handleCheckboxChange = (record: PaymentRecord, checked: boolean) => {
if (checked) {
// 先清除所有选中状态(单选逻辑)
Object.keys(checkboxStates).forEach((id) => {
checkboxStates[id] = false
})
// 选中当前项
checkboxStates[record.id] = true
selectedRecord.value = record
selectedRecordId.value = record.id
} else {
// 取消选中
selectedRecord.value = null
selectedRecordId.value = null
}
}
// 处理行点击
const handleRowClick = (record: PaymentRecord) => {
// 如果点击的是已选中的项,则取消选中;否则选中该项
if (selectedRecordId.value === record.id) {
// 取消选中
selectedRecord.value = null
selectedRecordId.value = null
checkboxStates[record.id] = false
} else {
// 先清除所有选中状态
Object.keys(checkboxStates).forEach((id) => {
checkboxStates[id] = false
})
// 选中当前项
selectedRecord.value = record
selectedRecordId.value = record.id
checkboxStates[record.id] = true
}
}
// 分页处理
const handleSizeChange = (size: number) => {
pagination.pageSize = size
pagination.currentPage = 1
getPaymentRecords()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getPaymentRecords()
}
// 确认绑定
const handleConfirm = () => {
if (!selectedRecord.value) {
ElMessage.warning('请选择一条付款记录')
return
}
emit('confirm', selectedRecord.value)
handleClose()
}
// 关闭对话框
const handleClose = () => {
visible.value = false
selectedRecord.value = null
selectedRecordId.value = null
Object.keys(checkboxStates).forEach((id) => {
checkboxStates[id] = false
})
searchKeyword.value = ''
pagination.currentPage = 1
}
// 格式化金额显示
const formatAmount = (amount: number, currency: string) => {
return `${currency} ${amount?.toLocaleString() || 0}`
}
// 获取状态类型(暂未使用)
// const getStatusType = (status: string) => {
// const statusMap: Record<string, string> = {
// available: 'success',
// used: 'info',
// pending: 'warning',
// cancelled: 'danger'
// }
// return statusMap[status] || 'info'
// }
// 获取状态文本(暂未使用)
// const getStatusText = (status: string) => {
// const statusMap: Record<string, string> = {
// available: '可用',
// used: '已使用',
// pending: '待处理',
// cancelled: '已取消'
// }
// return statusMap[status] || status
// }
// 监听对话框显示状态
watch(visible, (newVal) => {
if (newVal) {
getPaymentRecords()
}
})
</script>
<style scoped lang="scss">
.payment-record-dialog {
.search-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.clickable {
cursor: pointer;
}
.pagination-wrapper {
display: flex;
justify-content: center;
margin-top: 20px;
}
}
.dialog-footer {
display: flex;
justify-content: center;
gap: 12px;
}
</style>
-219
View File
@@ -1,219 +0,0 @@
<template>
<div class="print-preview-container">
<div class="preview-header">
<div class="header-actions">
<el-button @click="download" v-if="showDownload">下载</el-button>
<el-button @click="print" type="primary">打印</el-button>
<el-button @click="close">关闭</el-button>
</div>
</div>
<div class="preview-content">
<div
v-if="pdf.images.length"
class="images-container"
>
<img
v-for="(img, idx) in pdf.images"
:key="idx"
:src="img"
class="preview-image"
/>
</div>
<iframe
v-else-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
class="preview-iframe"
@load="onIframeLoad"
@error="onIframeError"
/>
<div v-else class="no-content">
<p>没有可预览的内容</p>
<p>传入的 PDF URL: {{ pdfUrl }}</p>
<p>PDF Object URL: {{ pdf.url }}</p>
<p>PDF Blob URL: {{ pdf.blobUrl }}</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue'
import { usePdfPreview } from '@/composables/usePdfPreview'
export default defineComponent({
name: 'PrintPreview',
props: {
visible: {
type: Boolean,
default: false
},
showDownload: {
type: Boolean,
default: true
},
pdfUrl: {
type: String,
default: ''
}
},
emits: ['close'],
setup(props, { emit }) {
// 使用公共的 usePdfPreview
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
// 监听 visible 变化,当显示时加载 PDF
watch(() => props.visible, (newVisible) => {
console.log('PrintPreview visible 变化:', newVisible)
if (newVisible && props.pdfUrl) {
// 构造响应对象,模拟 API 返回格式
const response = {
url: props.pdfUrl,
file_url: props.pdfUrl,
file_url_full: props.pdfUrl
}
openWithResponse(response)
console.log('PrintPreview 设置 PDF URL:', props.pdfUrl)
}
})
// 监听 pdfUrl 变化
watch(() => props.pdfUrl, (newUrl) => {
console.log('PrintPreview pdfUrl 变化:', newUrl)
if (newUrl) {
// 构造响应对象,模拟 API 返回格式
const response = {
url: newUrl,
file_url: newUrl,
file_url_full: newUrl
}
openWithResponse(response)
console.log('PrintPreview 更新 PDF URL:', newUrl)
}
})
const close = () => {
emit('close')
}
// iframe 加载事件
const onIframeLoad = () => {
console.log('iframe 加载完成')
}
// iframe 错误事件
const onIframeError = (error: any) => {
console.error('iframe 加载错误:', error)
}
return {
frameRef,
pdf,
download,
print,
close,
onIframeLoad,
onIframeError
}
}
})
</script>
<style scoped lang="scss">
.print-preview-container {
display: flex;
flex-direction: column;
height: 100%;
max-height: 80vh;
}
.preview-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
background: #fafafa;
border-radius: 6px 6px 0 0;
}
.header-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.preview-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.images-container {
overflow: auto;
border: 1px solid #eee;
border-radius: 6px;
padding: 8px;
background: #fff;
}
.preview-image {
display: block;
width: 100%;
margin-bottom: 8px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preview-iframe {
flex: 1;
width: 100%;
border: 1px solid #eee;
border-radius: 6px;
background: #fff;
}
.no-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
font-size: 14px;
p {
margin: 5px 0;
}
}
// 打印样式
@media print {
.print-preview-container {
max-height: none;
height: auto;
}
.preview-header {
display: none;
}
.preview-content {
height: auto;
}
.images-container {
border: none;
padding: 0;
}
.preview-image {
margin-bottom: 0;
box-shadow: none;
}
.preview-iframe {
border: none;
}
}
</style>
-164
View File
@@ -1,164 +0,0 @@
<template>
<div class="approval-timeline">
<div v-for="(rec, idx) in records" :key="idx" class="timeline-item">
<div class="left">
<div class="avatar-box">
<span class="initial">{{ rec.updated_by?.slice(0, 1) || '—' }}</span>
</div>
</div>
<div class="right">
<div class="row name-role">
<span class="name">编辑人:{{ rec.updated_by }}</span>
</div>
<div class="row time">{{ rec.updated_at }}</div>
<div class="card-box">
<el-card>
<h4>汇款人:{{ rec.remitter }}</h4>
<p
><span>付款来源:{{ rec.payment_apply_id }}</span>
<span>账户:{{ rec.account_short_desc }}</span>
</p>
<p
><span>金额:{{ rec.amount }} </span><span>币种:{{ rec.currency_name }}</span></p
>
<p
><span>折让金额:{{ rec.discount_amount }}</span
><span>实际汇率:{{ rec.exchange_rate }}</span>
</p>
<p>总金额(人民币)实际汇率:{{ rec.total_amount_cny }}</p>
<p>收款时间:{{ rec.received_at }}</p>
</el-card>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface ApprovalRecordItem {
account_id?: string
account_name?: string
amount?: string
arrived_at?: string
country_id?: string
country_name?: string
created_at?: string
created_by?: string
currency_id?: string
currency_name?: string
discount_amount?: string
exchange_rate?: string
fx_receipt_file_full?: string
account_short_desc?: string
id?: string
is_domestic_return?: string
payment_apply_id?: string
receipt_id?: string
receipt_no?: string
received_at?: string
remitter?: string
total_amount_cny?: string
updated_at?: string
updated_by?: string
voucher_no?: string
}
defineProps<{ records: ApprovalRecordItem[] }>()
</script>
<style scoped lang="scss">
/* 审批记录 时间线 */
.approval-timeline {
padding: 8px 4px 8px 0;
max-height: 600px;
overflow-y: scroll;
}
.timeline-item {
display: flex;
align-items: flex-start;
gap: 10px;
position: relative;
}
.timeline-item + .timeline-item {
margin-top: 50px;
}
.left {
width: 46px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.avatar-box {
width: 46px;
height: 46px;
background: #3d7bff;
border-radius: 10px;
color: #fff;
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 20px;
}
.status-dot {
position: absolute;
right: -4px;
bottom: -4px;
font-size: 9px;
padding: 3px 1px;
border-radius: 50%;
background: #22c55e;
border: 2px solid #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
}
.status-dot.pending {
background: #3471ff;
padding: 3px 3px;
}
.status-dot.rejected {
background: #ff0000;
padding: 3px 3px;
}
/* 连接下一个节点的虚线 */
.timeline-item:not(:last-child) .left::after {
content: '';
position: absolute;
top: 52px; /* avatar 46px + 间距 6px */
left: 23px; /* 盒子宽度一半,确保竖线居中 */
width: 0;
height: calc(100% + 116px); /* 加上相邻项之间的间距 */
border-left: 2px dotted #bfbfbf;
}
.right {
flex: 1;
padding-top: 2px;
}
.row.name-role {
color: #222;
margin-bottom: 10px;
}
.name {
font-weight: 600;
}
.role {
color: #666;
margin-left: 4px;
}
.time {
color: #888;
font-size: 12px;
margin-top: 2px;
margin-top: 10px;
}
.empty {
color: #999;
padding: 8px 0 4px 4px;
}
.card-box p{
display: flex;
justify-content: space-between;
}
</style>
-457
View File
@@ -1,457 +0,0 @@
<template>
<el-dialog
:model-value="visibleLocal"
:title="currentFile?.name || '文件预览'"
:close-on-click-modal="false"
:destroy-on-close="false"
width="90%"
:fullscreen="isFullscreen"
@close="handleClose"
>
<div class="file-preview-toolbar">
<div class="left">
<!-- <el-button-group>
<el-button size="small" @click="prevFile" :disabled="!hasPrev">上一页</el-button>
<el-button size="small" @click="nextFile" :disabled="!hasNext">下一页</el-button>
</el-button-group>
<el-divider direction="vertical" />
<el-button size="small" @click="zoomOut">-</el-button>
<el-button size="small" @click="resetZoom">重置</el-button>
<el-button size="small" @click="zoomIn">+</el-button>
<el-divider direction="vertical" /> -->
<!-- <el-button size="small" @click="rotateLeft">⟲</el-button> -->
<!-- <el-button size="small" @click="rotateRight">⟳</el-button> -->
<!-- <el-divider direction="vertical" />
<el-button size="small" @click="toggleFullscreen">{{
isFullscreen ? '退出全屏' : '全屏'
}}</el-button> -->
</div>
<div class="right">
<!-- <el-button size="small" type="primary" :href="currentFile?.url" target="_blank"
>在新标签打开</el-button
> -->
<!-- <el-button size="small" @click="downloadCurrent">下载</el-button> -->
<el-button size="small" type="danger" @click="handleClose">关闭</el-button>
</div>
</div>
<div class="file-preview-body">
<template v-if="!currentFile">
<div class="no-file">无要预览的文件</div>
</template>
<template v-else>
<!-- IMAGE -->
<div v-if="fileType === 'image'" class="viewer-wrap" :key="`image-${currentFile.url}`">
<div class="viewer" :style="viewerStyle">
<img
:src="currentFile.url"
:key="currentFile.url"
@error="onError"
@load="onLoadImage"
/>
</div>
</div>
<!-- PDF -->
<div v-else-if="fileType === 'pdf'" class="viewer-wrap" :key="`pdf-${currentFile.url}`">
<div class="viewer" :style="viewerStyle">
<!-- 使用 object / iframe 作为预览,支持大多数现代浏览器 -->
<object
:key="`pdf-obj-${currentFile.url}`"
:data="appendPdfParams(currentFile.url)"
type="application/pdf"
width="100%"
height="100%"
>
<iframe
:key="`pdf-iframe-${currentFile.url}`"
:src="appendPdfParams(currentFile.url)"
width="100%"
height="100%"
></iframe>
<div class="fallback"
>无法在浏览器内预览 PDF。<a :href="currentFile.url" target="_blank"
>点击打开</a
></div
>
</object>
</div>
</div>
<!-- OFFICE (Word, Excel, PPT) 使用微软 Office 在线查看器,仅当文件对外可访问时有效 -->
<div
v-else-if="fileType === 'office'"
class="viewer-wrap"
:key="`office-${currentFile.url}`"
>
<div class="viewer" :style="viewerStyle">
<template v-if="isHttpUrl(currentFile.url)">
<iframe
:key="`office-iframe-${currentFile.url}`"
:src="officeViewerUrl(currentFile.url)"
width="100%"
height="100%"
frameborder="0"
></iframe>
</template>
<template v-else>
<div class="fallback"
>Office 文件必须是可通过公网访问的 URL 才能使用在线预览。<a
:href="currentFile.url"
target="_blank"
>点击打开或下载</a
></div
>
</template>
</div>
</div>
<!-- 文本 / 代码 -->
<div v-else-if="fileType === 'text'" class="viewer-wrap" :key="`text-${currentFile.url}`">
<div class="viewer text-viewer" :style="viewerStyle">
<pre v-if="textContent !== null">{{ textContent }}</pre>
<div v-else class="loading">正在加载文本内容...</div>
<div v-if="textError" class="error">加载文本失败:{{ textError }}</div>
</div>
</div>
<!-- 未知 -->
<div v-else class="viewer-wrap unknown-viewer" :key="`unknown-${currentFile.url}`">
<div class="viewer" :style="viewerStyle">
<div class="unknown">
<p>无法在线预览此类型文件。</p>
<p><a :href="currentFile.url" target="_blank">点击打开或下载文件</a></p>
</div>
</div>
</div>
</template>
</div>
<template #footer>
<div class="footer-info">{{ currentFile?.name || '' }}</div>
</template>
</el-dialog>
</template>
<script lang="ts" setup name="FilesPreviews">
import { ref, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
// --- Props ---
interface FileItem {
file_name: any
name?: string
url: string
mime?: string // 可选,优先使用
}
const props = defineProps<{
modelValue: boolean
file?: FileItem | null // 单文件
files?: FileItem[] | null // 多文件
startIndex?: number
enableFullscreen?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
(e: 'close'): void
}>()
// --- Local state ---
const visibleLocal = ref<boolean>(!!props.modelValue)
const currentIndex = ref<number>(props.startIndex ?? 0)
const isFullscreen = ref<boolean>(!!props.enableFullscreen)
const zoom = ref<number>(1)
const rotate = ref<number>(0) // deg
const textContent = ref<string | null>(null)
const textError = ref<string | null>(null)
// --- Computed ---
const currentFile = computed((): FileItem | null => {
if (props.files && props.files.length > 0) {
const idx = Math.min(Math.max(0, currentIndex.value), props.files.length - 1)
return props.files[idx]
}
return props.file ?? null
})
function resetViewState(clearText = true) {
zoom.value = 1
rotate.value = 0
if (clearText) {
textContent.value = null
textError.value = null
}
}
// --- Watchers ---
watch(
() => props.modelValue,
(v) => (visibleLocal.value = v)
)
watch(visibleLocal, (v) => emit('update:modelValue', v))
watch(
() => props.file,
() => {
currentIndex.value = 0
resetViewState()
loadTextIfNeeded()
}
)
watch(
() => props.files,
() => {
currentIndex.value = props.startIndex ?? 0
resetViewState()
loadTextIfNeeded()
}
)
watch(currentIndex, () => {
resetViewState(false)
loadTextIfNeeded()
})
// 监听当前文件变化,确保文件切换时正确更新
watch(
() => currentFile.value?.url,
(newUrl, oldUrl) => {
if (newUrl !== oldUrl && newUrl) {
resetViewState()
loadTextIfNeeded()
}
}
)
const hasNext = computed(
() => !!(props.files && currentIndex.value < (props.files?.length ?? 0) - 1)
)
const hasPrev = computed(() => !!(props.files && currentIndex.value > 0))
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function prevFile() {
if (hasPrev.value) currentIndex.value -= 1
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function nextFile() {
if (hasNext.value) currentIndex.value += 1
}
function handleClose() {
visibleLocal.value = false
emit('close')
}
// --- Viewer helpers ---
function getExtension(name = '') {
const m = name.split('.')
return (m.length > 1 ? m[m.length - 1].toLowerCase() : '').trim()
}
function mimeFromName(name = '') {
const ext = getExtension(name)
const map: Record<string, string> = {
pdf: 'application/pdf',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
bmp: 'image/bmp',
webp: 'image/webp',
txt: 'text/plain',
md: 'text/markdown',
csv: 'text/csv',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
}
return map[ext] ?? ''
}
const fileType = computed(() => {
const f = currentFile.value
if (!f) return 'unknown'
let names = f.name
if (names == '') {
names = f.file_name
}
const mime = (f.mime || mimeFromName(names) || '').toLowerCase()
const ext = getExtension(names)
if (mime.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'].includes(ext))
return 'image'
if (mime === 'application/pdf' || ext === 'pdf') return 'pdf'
if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) return 'office'
if (mime.startsWith('text/') || ['txt', 'md', 'csv', 'log', 'json', 'xml'].includes(ext))
return 'text'
return 'unknown'
})
function isHttpUrl(u?: string) {
return !!u && /^https?:\/\//i.test(u)
}
function officeViewerUrl(url: string) {
// 使用微软 Office 在线查看器
return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`
}
function appendPdfParams(url: string) {
// 某些浏览器可以在 pdf 后加参数调整行为,例如 #toolbar=0
return url
}
// --- transform style for viewer ---
const viewerStyle = computed(() => {
return {
transform: `scale(${zoom.value}) rotate(${rotate.value}deg)`,
transformOrigin: 'center top'
} as Record<string, string>
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function zoomIn() {
zoom.value = Math.min(zoom.value + 0.25, 4)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function zoomOut() {
zoom.value = Math.max(zoom.value - 0.25, 0.25)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function resetZoom() {
zoom.value = 1
rotate.value = 0
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function rotateLeft() {
rotate.value = (rotate.value - 90) % 360
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function rotateRight() {
rotate.value = (rotate.value + 90) % 360
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function toggleFullscreen() {
isFullscreen.value = !isFullscreen.value
}
// --- text loading ---
async function loadTextIfNeeded() {
if (!currentFile.value) return
if (fileType.value !== 'text') return
textContent.value = null
textError.value = null
try {
const resp = await fetch(currentFile.value.url)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const txt = await resp.text()
textContent.value = txt
} catch (err: any) {
textError.value = String(err?.message ?? err)
}
}
// --- image handlers ---
function onError() {
ElMessage.warning('图片加载失败')
}
function onLoadImage() {
// 可以在这里获取自然尺寸进行布局优化
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function downloadCurrent() {
if (!currentFile.value) return
// 打开下载链接(浏览器通常会下载)
const a = document.createElement('a')
a.href = currentFile.value.url
a.download = currentFile.value.name || ''
a.target = '_blank'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
// expose some controls externally if needed
onMounted(() => {
if (props.modelValue) visibleLocal.value = true
if (fileType.value === 'text') loadTextIfNeeded()
})
// 导出组件
defineExpose({})
</script>
<style lang="scss" scoped>
.file-preview-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.file-preview-body {
height: calc(80vh - 80px);
display: flex;
align-items: center;
justify-content: center;
}
.viewer-wrap {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
}
.viewer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.viewer img {
max-width: 100%;
max-height: 100%;
display: block;
}
.text-viewer pre {
white-space: pre-wrap;
word-wrap: break-word;
text-align: left;
padding: 12px;
height: 100%;
overflow: auto;
}
.unknown-viewer .unknown {
text-align: center;
}
.no-file {
color: #999;
}
.footer-info {
color: #666;
}
.loading {
color: #999;
}
.error {
color: #f56c6c;
}
</style>
-320
View File
@@ -1,320 +0,0 @@
import { computed } from 'vue'
import { useMenuBadgeStore } from '@/store/modules/menuBadge'
import { useMenuStore } from '@/store/modules/menu'
import mittBus from '@/utils/sys/mittBus'
// 全局初始化状态
let globalInitialized = false
// 重置全局初始化状态(用于退出登录后重新初始化)
export const resetGlobalInitialized = () => {
globalInitialized = false
// console.log('徽标系统全局初始化状态已重置')
}
/**
* 菜单徽标管理 Composable
* 提供菜单徽标的统一管理接口
*/
export function useMenuBadge() {
const menuBadgeStore = useMenuBadgeStore()
const menuStore = useMenuStore()
// 计算属性:获取菜单列表(带徽标状态)
const menuListWithBadges = computed(() => {
const menuList = menuStore.menuList
if (!menuList.length) return menuList
// 同步徽标状态到菜单列表
menuBadgeStore.syncBadgeWithMenuList(menuList)
return menuList
})
// 更新菜单徽标
const updateMenuBadge = (path: string, count: number, type: 'dot' | 'text' = 'text') => {
menuBadgeStore.updateBadgeData(path, count, type)
}
// 批量更新菜单徽标
const batchUpdateMenuBadges = (
updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>
) => {
menuBadgeStore.batchUpdateBadgeData(updates)
}
// 清除菜单徽标
const clearMenuBadge = (path: string) => {
menuBadgeStore.clearBadge(path)
}
// 清除所有菜单徽标
const clearAllMenuBadges = () => {
menuBadgeStore.clearAllBadges()
}
// 获取菜单徽标数量
const getMenuBadgeCount = (path: string) => {
return menuBadgeStore.getBadgeCount(path)
}
// 检查菜单是否显示徽标
const isMenuBadgeVisible = (path: string) => {
return menuBadgeStore.shouldShowBadge(path)
}
// 获取菜单文本徽标
const getMenuTextBadge = (path: string) => {
return menuBadgeStore.shouldShowTextBadge(path)
}
// 处理WebSocket徽标更新消息
const handleWebSocketBadgeUpdate = (data: any) => {
// 防止重复处理
if (data._badgeHandled) {
return
}
data._badgeHandled = true
// 首先检查是否是数组格式的徽标数据
if (Array.isArray(data) || (data.data && Array.isArray(data.data))) {
handleMenuBadgeDataArray(data)
return
}
try {
// 根据消息类型处理不同的徽标更新
if (data.event === 'menu.badge.update') {
const { path, count, type } = data.data
if (path && typeof count === 'number') {
updateMenuBadge(path, count, type || 'text')
}
} else if (data.event === 'menu.badge.batch_update') {
const { updates } = data.data
if (Array.isArray(updates)) {
batchUpdateMenuBadges(updates)
}
} else if (data.event === 'menu.badge.clear') {
const { path } = data.data
if (path) {
clearMenuBadge(path)
} else {
clearAllMenuBadges()
}
}
} catch (error) {
console.error('处理WebSocket徽标更新消息时出错:', error, data)
}
}
// 处理菜单徽标数据数组格式
const handleMenuBadgeDataArray = (data: any) => {
// 防止重复处理
if (data._arrayHandled) {
return
}
data._arrayHandled = true
// 支持多种数据格式:
// 1. { event: '...', data: [{ url: '...', badge: 3 }] }
// 2. [{ url: '...', badge: 3 }] (直接是数组)
let badgeArray: any[] = []
if (Array.isArray(data)) {
// 如果 data 本身就是数组
badgeArray = data
} else if (Array.isArray(data.data)) {
// 如果 data.data 是数组
badgeArray = data.data
} else {
console.warn('未找到数组格式的徽标数据:', data)
return
}
if (badgeArray.length > 0) {
const updates = badgeArray
.filter((item: any) => {
const isValid = item.url && typeof item.badge === 'number'
if (!isValid) {
console.warn('跳过无效的徽标数据项:', item)
}
return isValid
})
.map((item: any) => ({
path: item.url,
count: item.badge,
type: 'text' as const
}))
if (updates.length > 0) {
batchUpdateMenuBadges(updates)
} else {
console.warn('没有有效的徽标更新项')
}
} else {
console.warn('徽标数据数组为空')
}
}
// 处理通知相关的徽标更新
const handleNotificationBadgeUpdate = (data: any) => {
// console.log('收到通知徽标更新消息:', data)
// 根据通知类型更新对应菜单的徽标
if (data.event === 'notify.receive') {
// 通知接收,更新通知菜单徽标
const currentCount = getMenuBadgeCount('/notification')
updateMenuBadge('/notification', currentCount + 1, 'text')
} else if (data.event === 'notify.read') {
// 通知已读,减少通知菜单徽标
const currentCount = getMenuBadgeCount('/notification')
updateMenuBadge('/notification', Math.max(0, currentCount - 1), 'text')
} else if (data.event === 'notify.clear') {
// 清除所有通知,清除通知菜单徽标
clearMenuBadge('/notification')
}
}
// 处理审批相关的徽标更新
const handleApprovalBadgeUpdate = (data: any) => {
// 首先检查是否是数组格式的徽标数据
if (Array.isArray(data) || Array.isArray(data.data)) {
handleMenuBadgeDataArray(data)
return
}
if (data.event === 'approval.new') {
// 新审批,更新审批菜单徽标
const currentCount = getMenuBadgeCount('/admin/approval')
updateMenuBadge('/admin/approval', currentCount + 1, 'text')
} else if (data.event === 'approval.processed') {
// 审批已处理,减少审批菜单徽标
const currentCount = getMenuBadgeCount('/admin/approval')
updateMenuBadge('/admin/approval', Math.max(0, currentCount - 1), 'text')
}
}
// 处理库存相关的徽标更新
const handleStockBadgeUpdate = (data: any) => {
// console.log('收到库存徽标更新消息:', data)
if (data.event === 'stock.low') {
// 库存不足,更新库存菜单徽标
const currentCount = getMenuBadgeCount('/admin/stock')
updateMenuBadge('/admin/stock', currentCount + 1, 'dot')
} else if (data.event === 'stock.normal') {
// 库存正常,减少库存菜单徽标
const currentCount = getMenuBadgeCount('/admin/stock')
updateMenuBadge('/admin/stock', Math.max(0, currentCount - 1), 'dot')
}
}
// 统一的WebSocket消息处理
const handleWebSocketMessage = (data: any) => {
if (!data) return
// 如果已经被特定事件处理过(如 menu-badge-update),则跳过
// 注意:_badgeProcessed 标记是在 wsClient.ts 中设置的
if (data._badgeProcessed) {
return
}
// 首先检查是否是数组格式的徽标数据(无论是否有 event 字段)
if (Array.isArray(data) || (data.data && Array.isArray(data.data))) {
handleMenuBadgeDataArray(data)
return
}
// 如果没有 event 字段,无法继续处理
if (!data.event) return
// 处理菜单徽标数据数组格式(如 approval.todo.counts)
if (data.event.includes('.todo.counts') || data.event.includes('.badge.counts')) {
handleMenuBadgeDataArray(data)
} else if (data.event.includes('menu.badge')) {
handleWebSocketBadgeUpdate(data)
} else if (data.event.includes('notify')) {
handleNotificationBadgeUpdate(data)
} else if (data.event.includes('approval')) {
handleApprovalBadgeUpdate(data)
} else if (data.event.includes('stock')) {
handleStockBadgeUpdate(data)
}
}
// 初始化徽标系统
const initBadgeSystem = () => {
if (globalInitialized) {
return
}
globalInitialized = true
// 标记页面已加载完成
menuBadgeStore.setPageReady(true)
// 先清理可能存在的旧监听器,避免重复注册
mittBus.off('ws:message', handleWebSocketMessage)
mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate)
mittBus.off('approval-badge-update', handleApprovalBadgeUpdate)
mittBus.off('stock-badge-update', handleStockBadgeUpdate)
mittBus.off('notification-badge-update', handleNotificationBadgeUpdate)
mittBus.off('menu:updated')
// 监听WebSocket消息
mittBus.on('ws:message', handleWebSocketMessage)
// 监听特定的徽标更新事件
mittBus.on('menu-badge-update', handleWebSocketBadgeUpdate)
mittBus.on('approval-badge-update', handleApprovalBadgeUpdate)
mittBus.on('stock-badge-update', handleStockBadgeUpdate)
mittBus.on('notification-badge-update', handleNotificationBadgeUpdate)
// 监听菜单数据变化,同步徽标状态
mittBus.on('menu:updated', () => {
menuBadgeStore.syncBadgeWithMenuList(menuStore.menuList)
})
}
// 清理徽标系统
const cleanupBadgeSystem = () => {
if (!globalInitialized) {
// console.log('菜单徽标系统未全局初始化,跳过清理')
return
}
// console.log('清理菜单徽标系统')
globalInitialized = false
// 移除事件监听
mittBus.off('ws:message', handleWebSocketMessage)
mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate)
mittBus.off('approval-badge-update', handleApprovalBadgeUpdate)
mittBus.off('stock-badge-update', handleStockBadgeUpdate)
mittBus.off('notification-badge-update', handleNotificationBadgeUpdate)
mittBus.off('menu:updated')
// 重置页面加载状态
menuBadgeStore.setPageReady(false)
}
// 注意:不在组件中自动初始化,避免重复初始化
// 初始化应该在App.vue中全局调用一次
return {
// 计算属性
menuListWithBadges,
// 方法
updateMenuBadge,
batchUpdateMenuBadges,
clearMenuBadge,
clearAllMenuBadges,
getMenuBadgeCount,
isMenuBadgeVisible,
getMenuTextBadge,
// 系统方法
initBadgeSystem,
cleanupBadgeSystem,
handleWebSocketMessage
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -11,4 +11,4 @@ export function useUploadHeaders() {
'Fdesk-Auth-Token': 'Bearer ' + userStore.accessToken
}))
return { uploadHeaders }
}
}
-16
View File
@@ -11,22 +11,6 @@ import { AppRouteRecord } from '@/types/router'
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
*/
export const asyncRoutes: AppRouteRecord[] = [
{
path: '/dashboard/console/finance',
name: 'Consolefinance',
component: RoutesAlias.consolefinance,
meta: {
title: '仪表盘'
}
},
{
path: '/dashboard/console/backoffice',
name: 'Backoffice',
component: RoutesAlias.backoffice,
meta: {
title: '仪表盘'
}
},
{
path: '/admin/inquiry_system',
name: 'Inquiry_system',
+1 -1
View File
@@ -9,7 +9,7 @@ import Home from '@views/index/index.vue'
export const staticRoutes: AppRouteRecordRaw[] = [
{
path: '/',
redirect: localStorage.getItem('home_page') || HOME_PAGE
redirect: HOME_PAGE
},
{
path: '/console',
+1 -3
View File
@@ -68,9 +68,7 @@ export enum RoutesAlias {
StockingAdd = '/crmpc/stocking/Add', // 创建备货单
StockingShow = '/crmpc/stocking/show', // 备货单详情
StockingFinalAdd = '/crmpc/stocking/final_add', // 最终备货单添加
StockingFinalShow = '/crmpc/stocking/final_show', // 最终备货单详情
consolefinance = '/dashboard/console/finance', //财务仪表盘
backoffice = '/dashboard/console/backoffice' // 内勤仪表盘
StockingFinalShow = '/crmpc/stocking/final_show' // 最终备货单详情
}
// 主页路由
-295
View File
@@ -1,295 +0,0 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { AppRouteRecord } from '@/types/router'
import { useUserStore } from './user'
// 菜单徽标数据类型
export interface MenuBadgeData {
/** 菜单路径 */
path: string
/** 徽标数量 */
count: number
/** 徽标类型:dot(红点) | text(数字) */
type: 'dot' | 'text'
/** 最后更新时间 */
lastUpdated: number
}
// 菜单徽标Store
export const useMenuBadgeStore = defineStore('menuBadgeStore', () => {
// 徽标数据映射 - 使用普通对象而不是Map,避免持久化问题
const badgeData = ref<Record<string, MenuBadgeData>>({})
// 页面是否已加载完成
const isPageReady = ref(false)
// 待处理的徽标更新队列(页面未加载完成时使用)
const pendingUpdates = ref<Array<{ path: string; count: number; type: 'dot' | 'text' }>>([])
// 获取当前用户ID
const getCurrentUserId = (): string => {
try {
const userStore = useUserStore()
return userStore.info?.userId?.toString() || 'anonymous'
} catch {
return 'anonymous'
}
}
// 获取用户特定的存储key
const getUserStorageKey = (): string => {
const userId = getCurrentUserId()
return `menu-badge-${userId}`
}
// 从localStorage加载用户特定的徽标数据
const loadUserBadgeData = () => {
try {
const storageKey = getUserStorageKey()
const stored = localStorage.getItem(storageKey)
if (stored) {
const parsed = JSON.parse(stored)
badgeData.value = parsed.badgeData || {}
// 注意:不恢复 isPageReady,因为页面状态应该在运行时由代码控制
// isPageReady.value 应该在 setPageReady() 中设置,而不是从存储中恢复
pendingUpdates.value = parsed.pendingUpdates || []
}
} catch (error) {
console.warn('加载用户徽标数据失败:', error)
}
}
// 保存用户特定的徽标数据到localStorage
const saveUserBadgeData = () => {
try {
const storageKey = getUserStorageKey()
const data = {
badgeData: badgeData.value,
// 注意:不保存 isPageReady,因为页面状态应该在运行时由代码控制
// 页面每次加载时都应该重新设置为 true
pendingUpdates: pendingUpdates.value
}
localStorage.setItem(storageKey, JSON.stringify(data))
} catch (error) {
console.warn('保存用户徽标数据失败:', error)
}
}
// 清理其他用户的徽标数据
const cleanupOtherUserData = () => {
try {
const currentUserId = getCurrentUserId()
const keys = Object.keys(localStorage)
keys.forEach((key) => {
if (key.startsWith('menu-badge-') && !key.endsWith(`-${currentUserId}`)) {
localStorage.removeItem(key)
}
})
} catch (error) {
console.warn('清理其他用户数据失败:', error)
}
}
// 计算属性:获取所有徽标数据
const allBadgeData = computed(() => {
return { ...badgeData.value }
})
// 获取指定菜单的徽标数据
const getBadgeData = (path: string): MenuBadgeData | null => {
return badgeData.value[path] || null
}
// 获取指定菜单的徽标数量
const getBadgeCount = (path: string): number => {
const data = badgeData.value[path]
return data?.count || 0
}
// 检查菜单是否应该显示徽标
const shouldShowBadge = (path: string): boolean => {
const data = badgeData.value[path]
return Boolean(data && data.count > 0)
}
// 检查菜单是否应该显示文本徽标
const shouldShowTextBadge = (path: string): string | null => {
const data = badgeData.value[path]
if (data && data.count > 0 && data.type === 'text') {
return data.count > 99 ? '99+' : data.count.toString()
}
return null
}
// 设置页面加载状态
const setPageReady = (ready: boolean) => {
isPageReady.value = ready
// 页面加载完成时,加载用户特定的徽标数据
if (ready) {
loadUserBadgeData()
// 确保 isPageReady 保持为 true(防止被其他逻辑覆盖)
isPageReady.value = true
// 处理待处理的更新
if (pendingUpdates.value.length > 0) {
pendingUpdates.value.forEach((update) => {
updateBadgeData(update.path, update.count, update.type)
})
pendingUpdates.value = []
// 再次确认页面状态
isPageReady.value = true
}
}
}
// 更新徽标数据
const updateBadgeData = (path: string, count: number, type: 'dot' | 'text' = 'text') => {
// 如果页面未加载完成,将更新加入队列
if (!isPageReady.value) {
// console.log('页面未加载完成,将徽标更新加入队列:', { path, count, type })
// 检查是否已存在相同路径的待处理更新,避免重复
const existingIndex = pendingUpdates.value.findIndex((item) => item.path === path)
if (existingIndex >= 0) {
pendingUpdates.value[existingIndex] = { path, count, type }
} else {
pendingUpdates.value.push({ path, count, type })
}
console.log('检查菜单是否应该显示徽标:', path, badgeData.value)
// 保存到用户特定存储
saveUserBadgeData()
return
}
if (count <= 0) {
// 如果数量为0或负数,移除徽标
delete badgeData.value[path]
} else {
// 更新或创建徽标数据
badgeData.value[path] = {
path,
count,
type,
lastUpdated: Date.now()
}
}
// 保存到用户特定存储
saveUserBadgeData()
}
// 批量更新徽标数据
const batchUpdateBadgeData = (
updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>
) => {
if (!isPageReady.value) {
updates.forEach((update) => {
const existingIndex = pendingUpdates.value.findIndex((item) => item.path === update.path)
if (existingIndex >= 0) {
pendingUpdates.value[existingIndex] = {
path: update.path,
count: update.count,
type: update.type || 'text'
}
} else {
pendingUpdates.value.push({
path: update.path,
count: update.count,
type: update.type || 'text'
})
}
})
// 保存到用户特定存储
saveUserBadgeData()
return
}
updates.forEach((update) => {
updateBadgeData(update.path, update.count, update.type || 'text')
})
// 批量更新后保存
saveUserBadgeData()
}
// 清除指定菜单的徽标
const clearBadge = (path: string) => {
updateBadgeData(path, 0)
}
// 清除所有徽标
const clearAllBadges = () => {
badgeData.value = {}
pendingUpdates.value = []
// 保存到用户特定存储
saveUserBadgeData()
}
// 从菜单列表同步徽标状态
const syncBadgeWithMenuList = (menuList: AppRouteRecord[]) => {
if (!isPageReady.value) {
// console.log('页面未加载完成,跳过菜单徽标同步')
return
}
const updateMenuBadge = (menus: AppRouteRecord[]) => {
menus.forEach((menu) => {
const badgeData = getBadgeData(menu.path)
if (badgeData) {
// 更新菜单的徽标状态
menu.meta.showBadge = badgeData.type === 'dot' && badgeData.count > 0
menu.meta.showTextBadge =
badgeData.type === 'text' && badgeData.count > 0
? badgeData.count > 99
? '99+'
: badgeData.count.toString()
: undefined
} else {
// 清除菜单的徽标状态
menu.meta.showBadge = false
menu.meta.showTextBadge = undefined
}
// 递归处理子菜单
if (menu.children) {
updateMenuBadge(menu.children)
}
})
}
updateMenuBadge(menuList)
}
// 获取待处理更新的数量
const getPendingUpdatesCount = () => pendingUpdates.value.length
// 获取待处理的更新列表
const getPendingUpdates = () => [...pendingUpdates.value]
return {
// 状态
badgeData,
isPageReady,
pendingUpdates,
// 计算属性
allBadgeData,
// 方法
getBadgeData,
getBadgeCount,
shouldShowBadge,
shouldShowTextBadge,
setPageReady,
updateBadgeData,
batchUpdateBadgeData,
clearBadge,
clearAllBadges,
syncBadgeWithMenuList,
getPendingUpdatesCount,
getPendingUpdates,
// 用户隔离相关方法
loadUserBadgeData,
saveUserBadgeData,
cleanupOtherUserData,
getCurrentUserId
}
})
-14
View File
@@ -140,20 +140,6 @@ export const useUserStore = defineStore(
}
const logOut = () => {
// 清理徽标系统
import('@/composables/useMenuBadge').then(({ useMenuBadge, resetGlobalInitialized }) => {
const { cleanupBadgeSystem, clearAllMenuBadges } = useMenuBadge()
clearAllMenuBadges()
cleanupBadgeSystem()
resetGlobalInitialized()
})
// 清理徽标store中的其他用户数据
import('@/store/modules/menuBadge').then(({ useMenuBadgeStore }) => {
const menuBadgeStore = useMenuBadgeStore()
menuBadgeStore.cleanupOtherUserData()
})
// 清空所有本地缓存
localStorage.clear()
sessionStorage.clear()
-8
View File
@@ -7,7 +7,6 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
'副本': typeof import('./../components/core/layouts/art-page-content/index - 副本.vue')['default']
ApprovalTimeline: typeof import('./../components/custom/ApprovalTimeline.vue')['default']
ArtBackToTop: typeof import('./../components/core/base/art-back-to-top/index.vue')['default']
ArtBarChart: typeof import('./../components/core/charts/ArtBarChart.vue')['default']
@@ -103,7 +102,6 @@ declare module 'vue' {
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElLoading: typeof import('element-plus/es')['ElLoading']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
@@ -134,17 +132,11 @@ declare module 'vue' {
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
ElUpload: typeof import('element-plus/es')['ElUpload']
ElWatermark: typeof import('element-plus/es')['ElWatermark']
FilePreview: typeof import('./../components/custom/FilePreview.vue')['default']
HorizontalSubmenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default']
'Index - 副本': typeof import('./../components/core/layouts/art-page-content/index - 副本.vue')['default']
InternalStaffView: typeof import('./../components/custom/InternalStaffView.vue')['default']
InternalStaffViews: typeof import('./../components/custom/InternalStaffViews.vue')['default']
LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default']
MenuLayoutSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default']
MenuStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default']
MultiFileUpload: typeof import('./../components/core/forms/MultiFileUpload.vue')['default']
OrderDetailContent: typeof import('./../components/custom/OrderDetailContent.vue')['default']
PrintPreview: typeof import('./../components/custom/PrintPreview.vue')['default']
ProcurementDetailView: typeof import('./../components/custom/ProcurementDetailView.vue')['default']
'ProcurementDetailView copy': typeof import('./../components/custom/ProcurementDetailView copy.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
+1 -1
View File
@@ -50,7 +50,7 @@ axiosInstance.interceptors.request.use(
const { systemsabbr } = useUserStore()
request.headers.set('Fdesk-Language', language)
request.headers.set('Fdesk-Corp-Abbr', companyabbr)
request.headers.set('Fdesk-System-Code', 'fdesk')
request.headers.set('Fdesk-System-Code', systemsabbr)
request.headers.set('Fdesk-Login-Type', loginType)
// 设置 token 和 请求头
if (accessToken) {
-186
View File
@@ -1,186 +0,0 @@
/**
* 菜单徽标管理工具
* 用于管理菜单徽标功能
*/
import { useMenuBadgeStore } from '@/store/modules/menuBadge'
/**
* 菜单徽标管理类
*/
export class MenuBadgeTest {
private menuBadgeStore = useMenuBadgeStore()
/**
* 直接更新徽标
*/
updateBadge(path: string, count: number, type: 'dot' | 'text' = 'text') {
// console.log(`直接更新徽标: ${path} = ${count} (${type})`)
this.menuBadgeStore.updateBadgeData(path, count, type)
}
/**
* 批量更新徽标
*/
batchUpdateBadges(updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>) {
// console.log('批量更新徽标:', updates)
this.menuBadgeStore.batchUpdateBadgeData(updates)
}
/**
* 清除徽标
*/
clearBadge(path: string) {
// console.log(`清除徽标: ${path}`)
this.menuBadgeStore.clearBadge(path)
}
/**
* 清除所有徽标
*/
clearAllBadges() {
// console.log('清除所有徽标')
this.menuBadgeStore.clearAllBadges()
}
/**
* 获取徽标状态
*/
getBadgeStatus(path: string) {
const data = this.menuBadgeStore.getBadgeData(path)
// console.log(`徽标状态 ${path}:`, data)
return data
}
/**
* 获取所有徽标状态
*/
getAllBadgeStatus() {
const allData = this.menuBadgeStore.allBadgeData
// console.log('所有徽标状态:', allData)
return allData
}
/**
* 获取待处理更新数量
*/
getPendingUpdatesCount() {
const count = this.menuBadgeStore.getPendingUpdatesCount()
// console.log(`待处理更新数量: ${count}`)
return count
}
/**
* 设置页面加载状态
*/
setPageReady(ready: boolean) {
// console.log(`设置页面加载状态: ${ready}`)
this.menuBadgeStore.setPageReady(ready)
}
/**
* 检查实际菜单路径
*/
checkActualMenuPaths() {
// console.log('检查实际菜单路径...')
try {
// 获取菜单store
import('@/store/modules/menu')
.then(({ useMenuStore }) => {
const menuStore = useMenuStore()
this.printMenuPaths(menuStore.menuList)
})
.catch((error) => {
console.error('动态导入菜单store失败:', error)
})
} catch (error) {
console.error('检查菜单路径失败:', error)
}
}
private printMenuPaths(menuList: any[]) {
// console.log('当前菜单列表:', menuList)
// 递归打印所有菜单路径
const printMenuPaths = (menus: any[], level = 0) => {
menus.forEach((menu) => {
// const indent = ' '.repeat(level)
// console.log(`${indent}${menu.path} - ${menu.meta?.title}`)
if (menu.children && menu.children.length > 0) {
printMenuPaths(menu.children, level + 1)
}
})
}
if (menuList && menuList.length > 0) {
printMenuPaths(menuList)
} else {
console.log('菜单列表为空,可能还未加载完成')
}
}
/**
* 运行基础功能测试
*/
runBasicTest() {
// console.log('开始运行基础功能测试...')
// 1. 测试页面未加载完成时的更新
// console.log('\n=== 测试页面未加载完成时的更新 ===')
this.setPageReady(false)
this.updateBadge('/admin/approval', 3, 'text')
this.updateBadge('/admin/stock', 1, 'dot')
// 2. 设置页面加载完成
// console.log('\n=== 设置页面加载完成 ===')
this.setPageReady(true)
// 3. 测试批量更新
// console.log('\n=== 测试批量更新 ===')
this.batchUpdateBadges([
{ path: '/admin/approval', count: 3, type: 'text' },
{ path: '/admin/stock', count: 1, type: 'dot' }
])
// 4. 显示最终状态
// console.log('\n=== 最终状态 ===')
this.getAllBadgeStatus()
// console.log('\n基础功能测试完成!')
}
/**
* 检查WebSocket连接状态
*/
checkWebSocketStatus() {
// console.log('检查WebSocket连接状态...')
// 检查全局WebSocket状态函数
if (typeof window !== 'undefined' && (window as any).getWebSocketStatus) {
const status = (window as any).getWebSocketStatus()
console.log('WebSocket状态:', status)
} else {
console.log('WebSocket状态函数不可用')
}
// 检查菜单徽标系统初始化状态
const pendingCount = this.menuBadgeStore.getPendingUpdatesCount()
console.log('待处理的徽标更新数量:', pendingCount)
if (pendingCount > 0) {
console.log('待处理的更新:', this.menuBadgeStore.getPendingUpdates())
}
}
}
// 创建全局测试实例
const menuBadgeTest = new MenuBadgeTest()
// 将测试工具挂载到全局对象,方便在控制台使用
if (typeof window !== 'undefined') {
// @ts-expect-error - 挂载到全局对象用于调试
window.menuBadgeTest = menuBadgeTest
}
export default menuBadgeTest
+10 -241
View File
@@ -1,8 +1,6 @@
import mittBus from './mittBus'
import mittBus from './mittBus'
import { useUserStore } from '@/store/modules/user'
import { ElNotification, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { ref } from 'vue'
import { ElNotification } from 'element-plus'
type JsonValue = Record<string, any> | any[]
@@ -20,7 +18,6 @@ let isConnecting = false
let lastInitToken: string | null = null
let lastInitAt = 0
const INIT_DEBOUNCE_MS = 1200
let isAlertShowing = false
// 生成唯一ID
const generateTraceId = (): string => {
@@ -98,144 +95,9 @@ const handleMessage = (event: MessageEvent) => {
try {
data = JSON.parse(event.data)
} catch {
// console.warn('WebSocket消息解析失败:', error, '原始数据:', event.data)
// 对于无法解析的消息,仍然尝试处理原始数据
data = { event: 'unknown', data: event.data }
// 忽略解析错误
}
// console.log('WebSocket message:', data)
// 判断系统初始化检查
if (data.event == 'system.init.check') {
const { systemsid } = useUserStore()
// 检查页面是否已加载完成
if (document.readyState !== 'complete') {
console.warn('页面未完全加载,延迟处理系统检查消息')
// 延迟处理,等待页面加载完成
setTimeout(() => {
handleMessage(event)
}, 1000)
return
}
// 检查 router 是否可用
let router: any = null
let currentPath = ''
let isOnMobileSignature = false
let isOnUserInfo = false
let isOnUserCenter = false
try {
router = useRouter()
// 检查 router.currentRoute 是否可用
if (router.currentRoute) {
currentPath = router.currentRoute.value?.path || ''
isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile')
isOnUserInfo = currentPath.startsWith('/system/user-center')
isOnUserCenter = currentPath.startsWith('/admin/user/user_info')
} else {
// Router currentRoute 未初始化,使用 window.location 作为备选方案
currentPath = window.location.pathname || ''
isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile')
isOnUserInfo = currentPath.startsWith('/system/user-center')
isOnUserCenter = currentPath.startsWith('/admin/user/user_info')
// console.warn('Router currentRoute 未初始化,使用 window.location 作为备选方案')
}
} catch {
// Router 完全未初始化,使用 window.location 作为备选方案
currentPath = window.location.pathname || ''
isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile')
isOnUserInfo = currentPath.startsWith('/system/user-center')
isOnUserCenter = currentPath.startsWith('/admin/user/user_info')
// console.warn('Router 未初始化,使用 window.location 作为备选方案:', error)
}
// 排除移动端电子签名页,不弹出提示框
if (isOnMobileSignature || isOnUserInfo || isOnUserCenter) {
return
}
// 接收到消息就弹出提示框(排除移动端电子签名页)
if (data.data) {
const { is_sign } = data.data
const msgs = ref('尚未设置【签名】,请前往个人中心完成操作!')
// 根据检查结果确定提示信息
let alertMessage = ''
let shouldShowAlert = false
// 构建状态标识
// const statusKey = `${is_report_to}-${is_office_clerk}-${is_sign}`
// switch (statusKey) {
// case 'false-false-false':
// alertMessage = msgs.value
// shouldShowAlert = true
// break
// case 'true-false-true':
// alertMessage = '尚未操作【绑定直属上级、签名】,请前往个人中心完成操作!'
// shouldShowAlert = true
// break
// case 'true-false-false':
// alertMessage = '尚未操作【绑定内勤、签名】】,请前往个人中心完成操作!'
// shouldShowAlert = true
// break
// case 'true-true-false':
// alertMessage = '尚未操作【签名】,请前往个人中心完成操作!'
// shouldShowAlert = true
// break
// case 'false-true-true':
// alertMessage = '尚未操作【直属上级】,请前往个人中心完成操作!'
// shouldShowAlert = true
// break
// case 'false-true-false':
// alertMessage = '尚未操作【直属上级,签名】,请前往个人中心完成操作!'
// shouldShowAlert = true
// break
// case 'false-false-true':
// alertMessage = '尚未操作【直属上级,内勤】,请前往个人中心完成操作!'
// shouldShowAlert = true
// break
// default:
// // 其他情况不弹出提示
// shouldShowAlert = false
// break
// }
if (is_sign == false) {
alertMessage = msgs.value
shouldShowAlert = true
}
// 只有在需要显示提示且当前没有弹出框的情况下才弹出提示框
if (shouldShowAlert && !isAlertShowing) {
isAlertShowing = true
ElMessageBox.alert(alertMessage, '提醒', {
confirmButtonText: '确定',
callback: () => {
isAlertShowing = false
// 如果 router 可用,使用 router 跳转
if (router && router.push) {
if (systemsid == '1') {
router.push('/admin/user/user_info')
} else {
router.push('/system/user-center')
}
} else {
// Router 不可用,使用 window.location 跳转
const targetPath = systemsid == '1' ? '/admin/user/user_info' : '/system/user-center'
window.location.href = targetPath
}
}
}).catch(() => {
// 如果弹出框被意外关闭,重置状态
isAlertShowing = false
})
} else if (shouldShowAlert && isAlertShowing) {
// 已有弹窗显示,避免重复弹出
}
}
}
// 处理通知消息
console.log('WebSocket message:', data)
if (data.event == 'notify.receive') {
ElNotification({
title: data.data.notice_data.message.title || '通知',
@@ -244,97 +106,22 @@ const handleMessage = (event: MessageEvent) => {
})
mittBus.emit('notification-data-changed', 1)
}
// 处理菜单徽标更新消息
if (data.event && data.event.includes('menu.badge')) {
// console.log('收到菜单徽标更新消息:', data)
// 直接触发徽标更新事件
mittBus.emit('menu-badge-update', data)
}
// 处理审批相关消息
if (data.event && data.event.includes('approval')) {
// 检查是否是数组格式的徽标数据或待办数量消息
if (
Array.isArray(data.data) ||
Array.isArray(data) ||
data.event.includes('.todo.counts') ||
data.event.includes('.badge.counts')
) {
// 数组格式的徽标数据或待办数量消息,触发菜单徽标更新事件
mittBus.emit('menu-badge-update', data)
// 标记已处理,避免在后续通用处理中重复处理
data._badgeProcessed = true
} else {
// 其他审批消息触发审批徽标更新事件
mittBus.emit('approval-badge-update', data)
}
}
// 处理库存相关消息
if (data.event && data.event.includes('stock')) {
// console.log('收到库存相关消息:', data)
// 触发库存徽标更新事件
mittBus.emit('stock-badge-update', data)
}
// 处理通知相关消息
if (data.event && data.event.includes('notify')) {
// console.log('收到通知相关消息:', data)
// 触发通知徽标更新事件
mittBus.emit('notification-badge-update', data)
}
// 处理其他可能的徽标相关消息
// 检查是否是数组格式的徽标数据(无论是否有 event 字段)
// 如果已经被处理过(如审批消息),则跳过
if (!data._badgeProcessed) {
if (Array.isArray(data) || (data.data && Array.isArray(data.data))) {
mittBus.emit('menu-badge-update', data)
} else if (data.event && (data.event.includes('badge') || data.event.includes('count'))) {
// 尝试作为通用徽标更新处理
mittBus.emit('menu-badge-update', data)
}
}
// 发送原始消息到事件总线
mittBus.emit('ws:message', data)
// 检查WebSocket连接状态,如果连接异常则记录
if (socket && socket.readyState !== WebSocket.OPEN) {
// console.warn('WebSocket连接状态异常,当前状态:', socket.readyState, '消息可能丢失')
}
}
// 设置WebSocket事件处理器
const setupWebSocketHandlers = (ws: WebSocket) => {
ws.onopen = () => {
console.log('WebSocket连接已建立')
isInitialConnection = false
isConnecting = false
startHeartbeat()
mittBus.emit('ws:open')
sendPing()
// 连接建立成功后推送一个连接确认消息
const connectMessage = {
event: 'system.connect',
data: {
timestamp: Date.now(),
message: 'WebSocket连接已建立'
},
trace_id: generateTraceId()
}
try {
ws.send(JSON.stringify(connectMessage))
} catch (error) {
console.error('发送连接确认消息失败:', error)
}
}
ws.onmessage = handleMessage
ws.onerror = (err) => {
console.error('WebSocket连接错误:', err)
mittBus.emit('ws:error', err)
isConnecting = false
try {
@@ -347,7 +134,6 @@ const setupWebSocketHandlers = (ws: WebSocket) => {
}
ws.onclose = () => {
console.log('WebSocket连接已关闭')
mittBus.emit('ws:close')
isConnecting = false
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
@@ -369,6 +155,7 @@ const scheduleReconnect = () => {
cleanup()
const delay = getReconnectDelay()
console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`)
reconnectTimer = setTimeout(() => {
const { accessToken } = useUserStore()
if (accessToken && !isConnecting) {
@@ -400,50 +187,32 @@ const scheduleReconnect = () => {
// 初始化WebSocket连接
export const initWebSocket = (token: string) => {
try {
if (!token) {
console.log('WebSocket初始化失败:没有token')
return
}
if (!token) return
const now = Date.now()
// 简单防抖:在短时间内重复触发则跳过
if (now - lastInitAt < INIT_DEBOUNCE_MS) {
console.log('WebSocket初始化被防抖跳过')
return
}
// 避免并发初始化:已在连接中或已连接且 token 未变化则跳过
if (isConnecting) {
console.log('WebSocket正在连接中,跳过重复初始化')
return
}
if (
socket &&
(socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)
) {
if (isConnecting) return
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
if (lastInitToken === token) {
console.log('WebSocket已连接且token未变化,跳过初始化')
return
}
// token 变更:需要切换连接
console.log('WebSocket token变更,关闭旧连接')
try {
socket.close()
} catch {
// 忽略关闭错误
}
try { socket.close() } catch {}
socket = null
}
cleanup()
const url = `${WS_BASE}${token}`
console.log('正在初始化WebSocket连接:', url)
isConnecting = true
lastInitToken = token
lastInitAt = now
socket = new WebSocket(url)
setupWebSocketHandlers(socket)
} catch (e) {
console.error('WebSocket初始化失败:', e)
mittBus.emit('ws:error', e)
isConnecting = false
scheduleReconnect()
@@ -501,4 +270,4 @@ export const getWebSocketStatus = () => ({
isInitialConnection,
isConnecting,
lastInitToken
})
})
@@ -1,810 +0,0 @@
<template>
<div class="borrow-approval-page">
<div class="borrow-toolbar boxshadow">
<div class="toolbar-right">
<el-select
v-model="searchForm.status"
placeholder="选择状态"
clearable
style="width: 150px; margin-right: 12px"
@change="handleSearch"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键字"
clearable
style="width: 260px"
@keyup.enter="handleSearch"
>
<template #suffix>
<el-icon class="clickable" @click="handleSearch"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="申请单号" prop="order_no" min-width="160" />
<ElTableColumn label="申请人" prop="applicant_name" min-width="120" />
<ElTableColumn label="借用物品" prop="item" min-width="200">
<template #default="scope">
<span>{{ getItemNames(scope.row.item) }}</span>
</template>
</ElTableColumn>
<ElTableColumn
label="借用原因"
prop="borrow_reason"
min-width="180"
show-overflow-tooltip
/>
<ElTableColumn label="使用开始日期" prop="use_start_date" min-width="120" />
<ElTableColumn label="使用结束日期" prop="use_end_date" min-width="120" />
<ElTableColumn label="状态" prop="status" min-width="120">
<template #default="scope">
<ElTag :type="getStatusType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn label="操作" fixed="right" min-width="180" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
type="primary"
underline="never"
@click="handleViewDetail(scope.row)"
style="margin-right: 8px"
v-if="hasAuth('borrow_approval@show')"
>
<i class="iconfont-sys">&#xe76c;</i> 查看详情
</el-link>
<el-link
v-if="canApprove(scope.row) && hasAuth('borrow_approval@approve')"
type="success"
underline="never"
@click="handleApprove(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 审批
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="借用申请详情"
width="800px"
:close-on-click-modal="false"
>
<div v-if="detailData.id" class="detail-content">
<!-- 基本信息 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>基本信息</span>
</div>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>申请单号</label>
<span>{{ detailData.order_no }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>申请人</label>
<span>{{ detailData.applicant_name }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>使用开始日期</label>
<span>{{ detailData.use_start_date }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>使用结束日期</label>
<span>{{ detailData.use_end_date }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row">
<ElCol :span="24">
<div class="info-item">
<label>借用原因</label>
<span>{{ detailData.borrow_reason }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row" v-if="detailData.remark">
<ElCol :span="24">
<div class="info-item">
<label>备注</label>
<span>{{ detailData.remark }}</span>
</div>
</ElCol>
</ElRow>
</div>
<!-- 借用物品 -->
<div class="info-section" v-if="detailData.item && detailData.item.length > 0">
<div class="section-title">
<div class="title-bar"></div>
<span>借用物品</span>
</div>
<div class="item-list">
<div
v-for="(item, index) in detailData.item"
:key="index"
class="item-card"
>
<div class="item-row">
<span class="item-label">物品名称</span>
<span class="item-value">{{ item.item_name_snap }}</span>
</div>
<div class="item-row">
<span class="item-label">申请数量</span>
<span class="item-value">{{ item.request_qty }}</span>
</div>
</div>
</div>
</div>
<!-- 附件 -->
<div class="info-section" v-if="detailData.attachment && detailData.attachment.length > 0">
<div class="section-title">
<div class="title-bar"></div>
<span>附件</span>
</div>
<div class="image-attachment-list">
<div
v-for="(file, index) in detailData.attachment"
:key="index"
class="image-attachment-item"
>
<el-image
:src="file.file_path_pull"
:preview-src-list="getImagePreviewUrls(detailData.attachment)"
:initial-index="Number(index)"
fit="cover"
class="attachment-image"
>
<template #error>
<div class="image-error">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
<div class="image-name">{{ file.file_name }}</div>
</div>
</div>
</div>
<!-- 审批记录 -->
<div class="info-section" v-if="detailData.approve && detailData.approve.length > 0">
<div class="section-title">
<div class="title-bar"></div>
<span>审批记录</span>
</div>
<el-timeline>
<el-timeline-item
v-for="(record, index) in detailData.approve"
:key="index"
:timestamp="record.action_at != 0 ? record.action_at : ''"
placement="top"
>
<div class="timeline-content">
<div class="timeline-header">
<span class="approver">{{ record.approver_name }}</span>
<ElTag :type="getActionType(record.action)" size="small">
{{ getActionLabel(record.action) }}
</ElTag>
</div>
<div v-if="record.comment" class="timeline-comment">
{{ record.comment }}
</div>
</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
</el-dialog>
<!-- 审批弹窗 -->
<el-dialog
v-model="approvalDialogVisible"
title="借用申请审批"
width="800px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div v-if="approvalDetail.id" class="approval-content">
<!-- 申请详情展示 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>申请信息</span>
</div>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>申请单号</label>
<span>{{ approvalDetail.order_no }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>申请人</label>
<span>{{ approvalDetail.applicant_name }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>使用开始日期</label>
<span>{{ approvalDetail.use_start_date }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>使用结束日期</label>
<span>{{ approvalDetail.use_end_date }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row">
<ElCol :span="24">
<div class="info-item">
<label>借用原因</label>
<span>{{ approvalDetail.borrow_reason }}</span>
</div>
</ElCol>
</ElRow>
</div>
<!-- 借用物品 -->
<div class="info-section" v-if="approvalDetail.item && approvalDetail.item.length > 0">
<div class="section-title">
<div class="title-bar"></div>
<span>借用物品</span>
</div>
<div class="item-list">
<div v-for="(item, index) in approvalDetail.item" :key="index" class="item-card">
<div class="item-row">
<span class="item-label">物品名称</span>
<span class="item-value">{{ item.item_name_snap }}</span>
</div>
<div class="item-row">
<span class="item-label">申请数量</span>
<span class="item-value">{{ item.request_qty }}</span>
</div>
</div>
</div>
</div>
<!-- 审批意见 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>审批意见</span>
</div>
<ElInput
v-model="approvalForm.comment"
type="textarea"
:rows="3"
placeholder="请输入审批意见"
/>
</div>
<!-- 上传附件 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>附件(可选)</span>
</div>
<el-upload
v-model:file-list="approvalFileList"
:action="uploadAction"
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:on-remove="handleUploadRemove"
:limit="5"
multiple
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">支持图片、PDF等格式,最多上传5个文件</div>
</template>
</el-upload>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton
type="danger"
plain
size="large"
class="submit-btn"
@click="handleSubmitApprove('reject')"
>
拒绝
</ElButton>
<ElButton
type="primary"
size="large"
class="submit-btn"
@click="handleSubmitApprove('approve')"
>
同意
</ElButton>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import type { UploadUserFile, UploadProps } from 'element-plus'
import { invitemService } from '@/api/admin/Itemrequisition'
import { useAuth } from '@/composables/useAuth'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
defineOptions({ name: 'BorrowApprovalPage' })
const { hasAuth } = useAuth()
const { uploadHeaders } = useUploadHeaders()
// 上传地址
const uploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=borrow_approval`
// 状态选项
const statusOptions = [
{ label: '待审核', value: 10 },
{ label: '审批中', value: 11 },
{ label: '已审批(待归还)', value: 12 },
{ label: '拒绝', value: 13 }
]
// 获取状态标签颜色
const getStatusType = (status: number): 'success' | 'danger' | 'info' | 'warning' | 'primary' => {
const typeMap: Record<number, 'success' | 'danger' | 'info' | 'warning' | 'primary'> = {
10: 'warning', // 待审核
11: 'primary', // 审批中
12: 'success', // 已审批(待归还)
13: 'danger', // 拒绝
14: 'info', // 申请归还
15: 'primary', // 归还审核中
16: 'success', // 部分归还
17: 'success', // 已全部归还
18: 'danger' // 已拒绝
}
return typeMap[status] || 'info'
}
// 获取状态文本
const getStatusLabel = (status: number) => {
const labelMap: Record<number, string> = {
10: '待审核',
11: '审批中',
12: '已审批(待归还)',
13: '拒绝',
14: '申请归还',
15: '归还审核中',
16: '部分归还',
17: '已全部归还',
18: '已拒绝'
}
return labelMap[status] || '未知'
}
// 判断是否可以审批
const canApprove = (row: any) => {
return row.status === 10 || row.status === 11 || row.status === 14 || row.status === 15
}
// 获取审批动作标签颜色
const getActionType = (action: number): 'success' | 'danger' | 'info' | 'warning' | 'primary' => {
const typeMap: Record<number, 'success' | 'danger' | 'info' | 'warning' | 'primary'> = {
0: 'info', // 待审核
1: 'success', // 同意
2: 'danger' // 拒绝
}
return typeMap[action] || 'info'
}
// 获取审批动作文本
const getActionLabel = (action: number) => {
const labelMap: Record<number, string> = {
0: '待审核',
1: '同意',
2: '拒绝'
}
return labelMap[action] || '未知'
}
// 获取物品名称列表
const getItemNames = (items: any[]) => {
if (!items || items.length === 0) return '-'
return items.map((item) => item.item_name_snap).join('、')
}
// 获取图片预览URL列表
const getImagePreviewUrls = (attachments: any[]) => {
if (!attachments || attachments.length === 0) return []
return attachments.map((item) => item.file_path_pull)
}
const loading = ref(false)
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// 搜索表单
const searchForm = reactive({
keyword: '',
status: undefined as number | undefined
})
// 详情弹窗数据
const detailDialogVisible = ref(false)
const detailData = ref<any>({})
// 审批弹窗数据
const approvalDialogVisible = ref(false)
const currentRowId = ref<number | null>(null)
const approvalDetail = ref<any>({})
const approvalForm = reactive({
comment: '',
attachment: [] as any[]
})
const approvalFileList = ref<UploadUserFile[]>([])
// 上传成功处理
const handleUploadSuccess: UploadProps['onSuccess'] = (response) => {
if (response.code === 200) {
approvalForm.attachment.push({
original_name: response.data.original_name || '文件',
file_path: response.data.file_path || response.data.file_url,
file_url: response.data.file_url
})
ElMessage.success('文件上传成功')
}
}
// 删除文件处理
const handleUploadRemove: UploadProps['onRemove'] = (file) => {
const index = approvalForm.attachment.findIndex(
(item) => item.file_url === file.url || item.original_name === file.name
)
if (index > -1) {
approvalForm.attachment.splice(index, 1)
}
}
onMounted(() => {
getList()
})
// 获取列表数据
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize
}
if (searchForm.keyword) {
params.keyword = searchForm.keyword
}
if (searchForm.status !== undefined) {
params.status = searchForm.status
}
const { records, current, size, total } = await invitemService.getBorrowList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.currentPage = 1
getList()
}
// 分页变化
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
// 查看详情
const handleViewDetail = async (row: any) => {
try {
const detail = await invitemService.getBorrowDetail({ id: row.id })
detailData.value = detail
detailDialogVisible.value = true
} catch (error) {
console.error('获取详情失败:', error)
}
}
// 打开审批弹窗 - 获取完整详情
const handleApprove = async (row: any) => {
try {
const detail = await invitemService.getBorrowDetail({ id: row.id })
currentRowId.value = row.id
approvalDetail.value = detail
approvalForm.comment = ''
approvalForm.attachment = []
approvalFileList.value = []
approvalDialogVisible.value = true
} catch (error) {
console.error('获取详情失败:', error)
}
}
// 提交审批
const handleSubmitApprove = async (action: 'approve' | 'reject') => {
if (!approvalForm.comment) {
ElMessage.warning('请输入审批意见')
return
}
try {
const { message } = await invitemService.borrowApproval({
id: currentRowId.value!,
action,
comment: approvalForm.comment,
attachment: approvalForm.attachment.map((item) => ({
original_name: item.original_name,
file_path: item.file_path
}))
})
ElMessage.success(message)
approvalDialogVisible.value = false
approvalForm.comment = ''
approvalForm.attachment = []
currentRowId.value = null
approvalDetail.value = {}
approvalFileList.value = []
getList()
} catch (error) {
console.error('审批失败:', error)
}
}
</script>
<style scoped lang="scss">
.borrow-approval-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.borrow-toolbar {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
.toolbar-right {
display: flex;
align-items: center;
}
}
.clickable {
cursor: pointer;
}
// 详情和审批弹窗样式
.detail-content,
.approval-content {
max-height: 60vh;
overflow-y: auto;
}
.info-section {
margin: 20px 0;
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
.title-bar {
width: 4px;
height: 20px;
background: #409eff;
margin-right: 8px;
border-radius: 2px;
}
span {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
}
.info-row {
margin-bottom: 12px;
}
.info-item {
display: flex;
flex-direction: column;
label {
font-size: 13px;
color: #909399;
margin-bottom: 6px;
}
span {
font-size: 14px;
color: #303133;
}
}
// 物品列表
.item-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.item-card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
background: #fafafa;
.item-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.item-label {
color: #909399;
}
.item-value {
color: #303133;
font-weight: 500;
}
}
}
// 图片附件列表
.image-attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.image-attachment-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100px;
}
.attachment-image {
width: 100px;
height: 100px;
border-radius: 8px;
border: 1px solid #e4e7ed;
overflow: hidden;
background: #f5f7fa;
:deep(.el-image__inner) {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.image-error {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 32px;
}
.image-name {
font-size: 12px;
color: #606266;
text-align: center;
word-break: break-all;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 审批记录时间轴
.timeline-content {
.timeline-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
.approver {
font-weight: 600;
color: #303133;
}
}
.timeline-comment {
font-size: 14px;
color: #606266;
}
}
// 弹窗底部按钮
.dialog-footer {
display: flex;
justify-content: center;
gap: 20px;
.submit-btn {
min-width: 120px;
}
}
</style>
@@ -1,929 +0,0 @@
<template>
<div class="customer-page">
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-if="hasAuth('item_list@add')" v-ripple
>新增</ElButton
>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="ID" prop="id" width="80" />
<ElTableColumn label="公司" prop="company_name" />
<ElTableColumn label="名称" prop="item_name" />
<ElTableColumn label="物品管理员" prop="manage_user_name" />
<ElTableColumn label="备注" prop="remark" />
<ElTableColumn label="库存数量" prop="stock_qty" />
<ElTableColumn label="扫码领取" prop="allow_qr_claim">
<template #default="scope">
<ElTag :type="scope.row.allow_qr_claim ? 'primary' : 'info'">
{{ scope.row.allow_qr_claim ? '是' : '否' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="领取限制数量" prop="qr_code_num">
<template #default="scope">
{{ scope.row.qr_code_num === 0 ? '不限制' : scope.row.qr_code_num }}
</template>
</ElTableColumn>
<ElTableColumn label="是否耗材" prop="is_consumable" width="180">
<template #default="scope">
<ElTag :type="scope.row.is_consumable ? 'primary' : 'info'">
{{ scope.row.is_consumable ? '是' : '否' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="状态" prop="status">
<template #default="scope">
<ElTag :type="scope.row.status ? 'primary' : 'info'">
{{ scope.row.status ? '启用' : '禁用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" />
<ElTableColumn fixed="right" label="操作" width="160" align="center">
<template #default="scope">
<ElButton link type="primary" @click="showDialog('edit', scope.row)" v-if="hasAuth('item_list@edit')">
编辑
</ElButton>
<ElDropdown trigger="click">
<ElButton link type="primary">
更多
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem @click="generateQrCode(scope.row)">
<el-icon><Document /></el-icon>
{{ scope.row.qr_code_pull ? '查看二维码' : '生成二维码' }}
</ElDropdownItem>
<ElDropdownItem @click="showProxyClaimDialog(scope.row)">
<el-icon><User /></el-icon>
管理员代领
</ElDropdownItem>
<ElDropdownItem @click="showStockLogDialog(scope.row)">
<el-icon><List /></el-icon>
库存记录
</ElDropdownItem>
<ElDropdownItem @click="showQrClaimsDialog(scope.row)">
<el-icon><Tickets /></el-icon>
领取记录
</ElDropdownItem>
<ElDropdownItem v-if="scope.row.qr_code_pull" @click="deleteQrCode(scope.row)">
<el-icon><Delete /></el-icon>
删除二维码
</ElDropdownItem>
<ElDropdownItem divided @click="deleteRole(scope.row)" v-if="hasAuth('item_list@del')">
<el-icon style="color: #f56c6c"><Delete /></el-icon>
<span style="color: #f56c6c">删除物品</span>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" align-center>
<ElForm
ref="formRef"
:model="form"
:rules="rules"
style="padding: 5px 20px"
label-width="100px"
>
<ElFormItem label="公司" prop="company_id">
<ElSelect
v-model="form.company_id"
placeholder="请选择公司"
filterable
style="width: 100%"
>
<ElOption
v-for="item in companyList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="名称" prop="item_name">
<ElInput v-model="form.item_name" placeholder="请输入名称" />
</ElFormItem>
<ElFormItem label="物品管理员" prop="manage_user_id">
<ElSelect
v-model="form.manage_user_id"
placeholder="请选择负责人"
filterable
style="width: 100%"
>
<ElOption
v-for="item in manageUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<!-- <ElFormItem label="规格型号" prop="spec_model">
<ElInput v-model="form.spec_model" placeholder="请输入规格型号" />
</ElFormItem>
<ElFormItem label="计量单位" prop="unit">
<ElInput v-model="form.unit" placeholder="请输入计量单位" />
</ElFormItem> -->
<ElFormItem label="是否耗材" prop="is_consumable">
<el-radio-group v-model="form.is_consumable">
<el-radio :label="1">是</el-radio>
<el-radio :label="0">否</el-radio>
</el-radio-group>
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput v-model="form.remark" placeholder="请输入备注" />
</ElFormItem>
<ElFormItem label="库存数量" prop="stock_qty">
<ElInputNumber
v-model="form.stock_qty"
:min="0"
placeholder="请输入库存数量"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="扫码领取" prop="allow_qr_claim">
<el-radio-group v-model="form.allow_qr_claim">
<el-radio :label="0">否</el-radio>
<el-radio :label="1">是</el-radio>
</el-radio-group>
</ElFormItem>
<ElFormItem v-if="form.allow_qr_claim === 1" label="单人领取限制" prop="qr_code_num">
<ElInputNumber
v-model="form.qr_code_num"
:min="0"
placeholder="0为不限制"
style="width: 100%"
/>
<span style="margin-left: 8px; color: var(--el-text-color-secondary); font-size: 12px">
单人扫码领取数量限制,0为不限制
</span>
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
<!-- 二维码弹窗 -->
<ElDialog v-model="qrCodeDialogVisible" title="物品二维码" width="400px" align-center>
<div class="qr-code-container">
<div v-if="qrCodeData" class="qr-code-wrapper">
<img :src="qrCodeData" alt="二维码" class="qr-code-image" />
<p class="qr-code-desc">扫描二维码领取物品</p>
</div>
<div v-else-if="qrCodeLoading" class="qr-code-loading">
<el-icon class="is-loading" :size="40">
<Loading />
</el-icon>
<p>生成中...</p>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="closeQrCodeDialog">{{ qrCodeData ? '关闭' : '取消' }}</ElButton>
<ElButton v-if="qrCodeData" type="primary" @click="downloadQrCode">下载二维码</ElButton>
</div>
</template>
</ElDialog>
<!-- 管理员代领弹窗 -->
<ElDialog v-model="proxyClaimDialogVisible" title="管理员代领" width="500px" align-center>
<ElForm ref="proxyClaimFormRef" :model="proxyClaimForm" label-width="100px">
<ElFormItem label="选择用户" prop="target_user_ids">
<ElSelect
v-model="proxyClaimForm.target_user_ids"
placeholder="请选择用户"
filterable
multiple
style="width: 100%"
>
<ElOption v-for="item in userList" :key="item.id" :label="item.name" :value="item.id" />
</ElSelect>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="proxyClaimDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleProxyClaimSubmit">确定</ElButton>
</div>
</template>
</ElDialog>
<!-- 库存记录弹窗 -->
<ElDialog v-model="stockLogDialogVisible" title="库存变化记录" width="80%" align-center>
<!-- 搜索表单 -->
<ElForm :model="stockLogSearchForm" inline style="margin-bottom: 16px">
<ElFormItem label="变动类型">
<ElSelect
v-model="stockLogSearchForm.change_type"
placeholder="请选择变动类型"
clearable
style="width: 150px"
>
<ElOption label="入库" :value="1" />
<ElOption label="出库(领用)" :value="2" />
<ElOption label="出库(损耗)" :value="3" />
<ElOption label="入库(归还)" :value="4" />
</ElSelect>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="searchStockLog">查询</ElButton>
<ElButton @click="resetStockLogSearch">重置</ElButton>
</ElFormItem>
</ElForm>
<ElTable :data="stockLogList" :loading="stockLogLoading" max-height="400">
<ElTableColumn label="物品名称" prop="item_name" />
<ElTableColumn label="变化类型" prop="change_type_text" />
<ElTableColumn label="变化数量" prop="change_qty">
<template #default="scope">
<span :style="{ color: scope.row.change_qty > 0 ? '#67C23A' : '#F56C6C' }">
{{ scope.row.change_qty > 0 ? '+' : '' }}{{ scope.row.change_qty }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="变化前" prop="before_qty"/>
<ElTableColumn label="变化后" prop="after_qty" />
<ElTableColumn label="业务类型" prop="biz_type_text" />
<ElTableColumn label="操作人" prop="operator_name" />
<ElTableColumn label="备注" prop="remark" min-width="120" />
<ElTableColumn label="创建时间" prop="created_at"/>
</ElTable>
<!-- 分页 -->
<div style="margin-top: 16px; display: flex; justify-content: flex-end">
<ElPagination
v-model:current-page="stockLogPagination.currentPage"
v-model:page-size="stockLogPagination.pageSize"
:total="stockLogPagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleStockLogSizeChange"
@current-change="handleStockLogCurrentChange"
/>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="stockLogDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
<!-- 领取记录弹窗 -->
<ElDialog v-model="qrClaimsDialogVisible" title="领取记录" width="80%" align-center>
<!-- 搜索表单 -->
<ElForm :model="qrClaimsSearchForm" inline style="margin-bottom: 16px">
<ElFormItem label="领取方式">
<ElSelect
v-model="qrClaimsSearchForm.claim_type"
placeholder="请选择领取方式"
clearable
style="width: 150px"
>
<ElOption label="扫码领取" :value="2" />
<ElOption label="管理员代领" :value="3" />
</ElSelect>
</ElFormItem>
<ElFormItem label="开始时间">
<ElDatePicker
v-model="qrClaimsSearchForm.start_time"
type="datetime"
placeholder="请选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 180px"
/>
</ElFormItem>
<ElFormItem label="结束时间">
<ElDatePicker
v-model="qrClaimsSearchForm.end_time"
type="datetime"
placeholder="请选择结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 180px"
/>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="searchQrClaims">查询</ElButton>
<ElButton @click="resetQrClaimsSearch">重置</ElButton>
</ElFormItem>
</ElForm>
<ElTable :data="qrClaimsList" :loading="qrClaimsLoading" max-height="400">
<ElTableColumn label="物品">
<template #default="scope">
<span v-if="scope.row.items && scope.row.items.length > 0">
<template v-for="(item, index) in scope.row.items" :key="index">
{{ item.item_name_snap }}<span v-if="Number(index) < scope.row.items.length - 1">, </span>
</template>
</span>
<span v-else>-</span>
</template>
</ElTableColumn>
<ElTableColumn label="数量">
<template #default="scope">
<span v-if="scope.row.items && scope.row.items.length > 0">
<template v-for="(item, index) in scope.row.items" :key="index">
{{ item.approved_qty }}<span v-if="Number(index) < scope.row.items.length - 1">, </span>
</template>
</span>
<span v-else>-</span>
</template>
</ElTableColumn>
<ElTableColumn label="领取人" prop="applicant_name" />
<ElTableColumn label="领取类型" prop="claim_type_text" />
<ElTableColumn label="领取时间" prop="created_at" />
<ElTableColumn label="代领人" prop="proxy_claim_user_name">
<template #default="scope">
{{ scope.row.proxy_claim_user_name || '-' }}
</template>
</ElTableColumn>
<ElTableColumn label="领取方式" prop="borrow_reason" />
<ElTableColumn label="备注" prop="remark" />
</ElTable>
<!-- 分页 -->
<div style="margin-top: 16px; display: flex; justify-content: flex-end">
<ElPagination
v-model:current-page="qrClaimsPagination.currentPage"
v-model:page-size="qrClaimsPagination.pageSize"
:total="qrClaimsPagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleQrClaimsSizeChange"
@current-change="handleQrClaimsCurrentChange"
/>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="qrClaimsDialogVisible = false">关闭</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { Loading, ArrowDown, Document, User, List, Tickets, Delete } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { invitemService } from '@/api/admin/Itemrequisition'
import { manageService } from '@/api/adminApi'
import { UserService } from '@/api/usersApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'ItemList' })
const keyword = ref<string>('')
const loading = ref(false)
const dialogVisible = ref(false)
const qrCodeDialogVisible = ref(false)
const qrCodeData = ref('')
const qrCodeLoading = ref(false)
const proxyClaimDialogVisible = ref(false)
const stockLogDialogVisible = ref(false)
const stockLogList = ref<any[]>([])
const stockLogLoading = ref(false)
const currentStockLogItemId = ref<number | null>(null)
const stockLogSearchForm = reactive({
change_type: undefined as number | undefined
})
const stockLogPagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const qrClaimsDialogVisible = ref(false)
const qrClaimsList = ref<any[]>([])
const qrClaimsLoading = ref(false)
const currentQrClaimsItemId = ref<number | null>(null)
const qrClaimsSearchForm = reactive({
claim_type: undefined as number | undefined,
start_time: '',
end_time: ''
})
const qrClaimsPagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const proxyClaimFormRef = ref<FormInstance>()
const formRef = ref<FormInstance>()
const rules = reactive<FormRules>({
company_id: [{ required: true, message: '请选择公司', trigger: 'change' }],
item_name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
manage_user_id: [{ required: true, message: '请选择负责人', trigger: 'change' }]
})
const form = reactive({
company_id: '',
manage_user_id: '',
remark: '',
item_name: '',
stock_qty: 0,
allow_qr_claim: 0,
is_consumable: 0,
qr_code_num: 0,
status: 1,
id: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const companyList = ref<any[]>([])
const getcompanyList = async () => {
const response = await UserService.getCompanyList()
const list = (response as any)?.data ?? (response as any)?.records ?? response
companyList.value = Array.isArray(list) ? list : []
}
const manageUserList = ref<any>()
// 获取用户列表
const getCompanyUserList = async () => {
const response = await manageService.presetusers()
manageUserList.value = response || []
}
// 管理员代领相关
const proxyClaimForm = reactive({
item_id: '',
target_user_ids: [] as number[]
})
const userList = ref<any[]>([])
// 获取所有用户列表
const getAllUserList = async () => {
try {
const response = await manageService.presetusers()
userList.value = response || []
} catch (error) {
console.error('获取用户列表失败:', error)
}
}
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
getcompanyList()
getCompanyUserList()
getAllUserList()
})
watch([keyword], () => {
pagination.currentPage = 1
getWorkList()
})
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize
}
if (keyword.value) {
params.keyword = keyword.value
}
const { records, current, size, total } = await invitemService.getinvitemlist(params)
tableData.value = records.map((item: any, index: number) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
const dialogType = ref('add')
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
// 编辑模式,填充所有字段
Object.assign(form, {
company_id: row.company_id,
item_name: row.item_name,
manage_user_id: row.manage_user_id,
remark: row.remark,
stock_qty: row.stock_qty ?? 0,
is_consumable: row.is_consumable ?? 0,
allow_qr_claim: row.allow_qr_claim ?? 0,
qr_code_num: row.qr_code_num ?? 0,
status: row.status,
id: row.id
})
} else {
// 新增模式,重置所有字段
Object.assign(form, {
company_id: '',
item_name: '',
manage_user_id: '',
remark: '',
stock_qty: 0,
is_consumable: 0,
allow_qr_claim: 0,
qr_code_num: 0,
status: 1,
id: ''
})
}
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await invitemService.invitemdelete({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
// 删除二维码
const deleteQrCode = (row: any) => {
ElMessageBox.confirm('确定删除该物品的二维码吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const { message } = await invitemService.deleteQrCode({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (valid) {
if (dialogType.value === 'add') {
const { message } = await invitemService.invitemadd({
company_id: form.company_id,
item_name: form.item_name,
manage_user_id: form.manage_user_id,
remark: form.remark,
stock_qty: form.stock_qty,
is_consumable: form.is_consumable,
allow_qr_claim: form.allow_qr_claim,
qr_code_num: form.qr_code_num || 0,
status: form.status
})
ElMessage.success(message)
} else {
const { message } = await invitemService.invitemupdate({
id: form.id,
company_id: form.company_id,
item_name: form.item_name,
manage_user_id: form.manage_user_id,
remark: form.remark,
stock_qty: form.stock_qty,
is_consumable: form.is_consumable,
allow_qr_claim: form.allow_qr_claim,
qr_code_num: form.qr_code_num || 0,
status: form.status
})
ElMessage.success(message)
}
dialogVisible.value = false
getWorkList()
}
})
}
// 生成/查看二维码
const generateQrCode = async (row: any) => {
qrCodeDialogVisible.value = true
// 如果已有二维码URL,直接使用
if (row.qr_code_pull) {
qrCodeData.value = row.qr_code_pull
qrCodeLoading.value = false
return
}
// 没有二维码URL,调用接口生成
qrCodeLoading.value = true
qrCodeData.value = ''
try {
const response = await invitemService.getQrCode({ id: row.id })
qrCodeData.value = (response as any)?.data?.qr_code_url || ''
ElMessage.success('二维码生成成功')
// 刷新列表以更新 qr_code_pull
getWorkList()
} catch (error) {
console.error('生成二维码失败:', error)
ElMessage.error('生成二维码失败')
} finally {
qrCodeLoading.value = false
}
}
// 关闭二维码弹窗
const closeQrCodeDialog = () => {
qrCodeDialogVisible.value = false
qrCodeData.value = ''
qrCodeLoading.value = false
}
// 下载二维码
const downloadQrCode = () => {
if (!qrCodeData.value) {
ElMessage.warning('暂无二维码可下载')
return
}
const link = document.createElement('a')
link.href = qrCodeData.value
link.download = `item-qr-code-${Date.now()}.png`
link.click()
ElMessage.success('二维码下载成功')
}
// 显示管理员代领弹窗
const showProxyClaimDialog = (row: any) => {
proxyClaimDialogVisible.value = true
proxyClaimForm.item_id = row.id
proxyClaimForm.target_user_ids = []
}
// 提交管理员代领
const handleProxyClaimSubmit = async () => {
if (!proxyClaimForm.target_user_ids || proxyClaimForm.target_user_ids.length === 0) {
ElMessage.warning('请选择用户')
return
}
try {
const { message } = await invitemService.proxyClaim({
item_id: proxyClaimForm.item_id,
target_user_id: proxyClaimForm.target_user_ids.join(',')
})
ElMessage.success(message)
proxyClaimDialogVisible.value = false
getWorkList()
} catch (error) {
console.error('管理员代领失败:', error)
ElMessage.error('操作失败')
}
}
// 显示库存记录弹窗
const showStockLogDialog = async (row: any) => {
currentStockLogItemId.value = row.id
stockLogDialogVisible.value = true
// 重置搜索条件和分页
stockLogSearchForm.change_type = undefined
stockLogPagination.currentPage = 1
stockLogPagination.pageSize = 10
await getStockLogList()
}
// 获取库存记录列表
const getStockLogList = async () => {
if (!currentStockLogItemId.value) return
stockLogLoading.value = true
stockLogList.value = []
try {
const params: any = {
item_id: currentStockLogItemId.value,
current: stockLogPagination.currentPage,
size: stockLogPagination.pageSize
}
if (stockLogSearchForm.change_type) {
params.change_type = stockLogSearchForm.change_type
}
const response = await invitemService.getStockLogList(params)
const records = (response as any)?.data || (response as any)?.records || []
stockLogList.value = Array.isArray(records) ? records : []
stockLogPagination.total = (response as any)?.total || 0
} catch (error) {
console.error('获取库存记录失败:', error)
ElMessage.error('获取库存记录失败')
} finally {
stockLogLoading.value = false
}
}
// 搜索库存记录
const searchStockLog = () => {
stockLogPagination.currentPage = 1
getStockLogList()
}
// 重置搜索条件
const resetStockLogSearch = () => {
stockLogSearchForm.change_type = undefined
stockLogPagination.currentPage = 1
getStockLogList()
}
// 分页大小变化
const handleStockLogSizeChange = (newPageSize: number) => {
stockLogPagination.pageSize = newPageSize
getStockLogList()
}
// 当前页变化
const handleStockLogCurrentChange = (newCurrentPage: number) => {
stockLogPagination.currentPage = newCurrentPage
getStockLogList()
}
// 显示扫码记录弹窗
const showQrClaimsDialog = async (row: any) => {
currentQrClaimsItemId.value = row.id
qrClaimsDialogVisible.value = true
// 重置搜索条件和分页
qrClaimsSearchForm.claim_type = undefined
qrClaimsSearchForm.start_time = ''
qrClaimsSearchForm.end_time = ''
qrClaimsPagination.currentPage = 1
qrClaimsPagination.pageSize = 10
await getQrClaimsList()
}
// 获取扫码记录列表
const getQrClaimsList = async () => {
if (!currentQrClaimsItemId.value) return
qrClaimsLoading.value = true
qrClaimsList.value = []
try {
const params: any = {
item_id: currentQrClaimsItemId.value,
current: qrClaimsPagination.currentPage,
size: qrClaimsPagination.pageSize
}
if (qrClaimsSearchForm.claim_type) {
params.claim_type = qrClaimsSearchForm.claim_type
}
if (qrClaimsSearchForm.start_time) {
params.start_time = qrClaimsSearchForm.start_time
}
if (qrClaimsSearchForm.end_time) {
params.end_time = qrClaimsSearchForm.end_time
}
const response = await invitemService.getQrClaimsList(params)
const records = (response as any)?.data || (response as any)?.records || []
qrClaimsList.value = Array.isArray(records) ? records : []
qrClaimsPagination.total = (response as any)?.total || 0
} catch (error) {
console.error('获取扫码记录失败:', error)
ElMessage.error('获取扫码记录失败')
} finally {
qrClaimsLoading.value = false
}
}
// 搜索扫码记录
const searchQrClaims = () => {
qrClaimsPagination.currentPage = 1
getQrClaimsList()
}
// 重置搜索条件
const resetQrClaimsSearch = () => {
qrClaimsSearchForm.claim_type = undefined
qrClaimsSearchForm.start_time = ''
qrClaimsSearchForm.end_time = ''
qrClaimsPagination.currentPage = 1
getQrClaimsList()
}
// 分页大小变化
const handleQrClaimsSizeChange = (newPageSize: number) => {
qrClaimsPagination.pageSize = newPageSize
getQrClaimsList()
}
// 当前页变化
const handleQrClaimsCurrentChange = (newCurrentPage: number) => {
qrClaimsPagination.currentPage = newCurrentPage
getQrClaimsList()
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.customer-search-group {
display: flex;
gap: 12px;
}
.qr-code-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
.qr-code-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.qr-code-image {
width: 280px;
height: 280px;
object-fit: contain;
}
.qr-code-desc {
margin: 0;
font-size: 14px;
color: var(--el-text-color-secondary);
}
.qr-code-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color: var(--el-text-color-secondary);
p {
margin: 0;
}
}
</style>
@@ -1,742 +0,0 @@
<template>
<div class="return-approval-page">
<div class="return-toolbar boxshadow">
<div class="toolbar-right">
<el-select
v-model="searchForm.status"
placeholder="选择状态"
clearable
style="width: 150px; margin-right: 12px"
@change="handleSearch"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键字"
clearable
style="width: 260px"
@keyup.enter="handleSearch"
>
<template #suffix>
<el-icon class="clickable" @click="handleSearch"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="归还单号" prop="order_no" min-width="160" />
<ElTableColumn label="申请人" prop="applicant_name" min-width="120" />
<ElTableColumn label="归还物品" prop="return_item" min-width="200">
<template #default="scope">
<span>{{ getReturnItemNames(scope.row.return_item) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="归还时间" prop="return_at" min-width="120" />
<ElTableColumn label="状态" prop="status" min-width="120">
<template #default="scope">
<ElTag :type="getStatusType(scope.row.status)">
{{ getStatusLabel(scope.row.status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn label="操作" fixed="right" min-width="180" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
type="primary"
underline="never"
@click="handleViewDetail(scope.row)"
style="margin-right: 8px"
v-if="hasAuth('return_approval@show')"
>
<i class="iconfont-sys">&#xe76c;</i> 查看详情
</el-link>
<el-link
v-if="canApprove(scope.row) && hasAuth('return_approval@approve')"
type="success"
underline="never"
@click="handleApprove(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 审批
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="归还申请详情"
width="800px"
:close-on-click-modal="false"
>
<div v-if="detailData.id" class="detail-content">
<!-- 基本信息 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>基本信息</span>
</div>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>归还单号</label>
<span>{{ detailData.order_no }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>申请人</label>
<span>{{ detailData.applicant_name }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>归还时间</label>
<span>{{ detailData.return_at }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>状态</label>
<span>{{ getStatusLabel(detailData.status) }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row" v-if="detailData.apply_remark">
<ElCol :span="24">
<div class="info-item">
<label>申请备注</label>
<span>{{ detailData.apply_remark }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row" v-if="detailData.handle_remark">
<ElCol :span="24">
<div class="info-item">
<label>处理备注</label>
<span>{{ detailData.handle_remark }}</span>
</div>
</ElCol>
</ElRow>
</div>
<!-- 归还物品 -->
<div class="info-section" v-if="detailData.item_return && detailData.item_return.length > 0">
<div class="section-title">
<div class="title-bar"></div>
<span>归还物品</span>
</div>
<div class="item-list">
<div
v-for="(item, index) in detailData.item_return"
:key="index"
class="item-card"
>
<div class="item-row">
<span class="item-label">物品名称</span>
<span class="item-value">{{ item.requisition_item.item_name_snap }}</span>
</div>
<div class="item-row">
<span class="item-label">申请数量</span>
<span class="item-value">{{ item.requisition_item.request_qty }}</span>
</div>
<div class="item-row">
<span class="item-label">已归还数量</span>
<span class="item-value">{{ item.requisition_item.returned_qty }}</span>
</div>
<div class="item-row">
<span class="item-label">本次归还</span>
<span class="item-value">{{ item.return_qty }}</span>
</div>
</div>
</div>
</div>
<!-- 附件 -->
<div class="info-section" v-if="detailData.return_attachment && detailData.return_attachment.length > 0">
<div class="section-title">
<div class="title-bar"></div>
<span>附件</span>
</div>
<div class="image-attachment-list">
<div
v-for="(file, index) in detailData.return_attachment"
:key="index"
class="image-attachment-item"
>
<el-image
:src="file.file_path_pull"
:preview-src-list="getImagePreviewUrls(detailData.return_attachment)"
:initial-index="Number(index)"
fit="cover"
class="attachment-image"
>
<template #error>
<div class="image-error">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
<div class="image-name">{{ file.file_name }}</div>
</div>
</div>
</div>
</div>
</el-dialog>
<!-- 审批弹窗 -->
<el-dialog
v-model="approvalDialogVisible"
title="归还申请审批"
width="800px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div v-if="approvalDetail.id" class="approval-content">
<!-- 申请详情展示 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>申请信息</span>
</div>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>归还单号</label>
<span>{{ approvalDetail.order_no }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>申请人</label>
<span>{{ approvalDetail.applicant_name }}</span>
</div>
</ElCol>
</ElRow>
<ElRow :gutter="16" class="info-row">
<ElCol :span="12">
<div class="info-item">
<label>归还时间</label>
<span>{{ approvalDetail.return_at }}</span>
</div>
</ElCol>
<ElCol :span="12">
<div class="info-item">
<label>申请备注</label>
<span>{{ approvalDetail.apply_remark || '-' }}</span>
</div>
</ElCol>
</ElRow>
</div>
<!-- 归还物品 -->
<div class="info-section" v-if="approvalDetail.item_return && approvalDetail.item_return.length > 0">
<div class="section-title">
<div class="title-bar"></div>
<span>归还物品</span>
</div>
<div class="item-list">
<div
v-for="(item, index) in approvalDetail.item_return"
:key="index"
class="item-card"
>
<div class="item-row">
<span class="item-label">物品名称</span>
<span class="item-value">{{ item.requisition_item.item_name_snap }}</span>
</div>
<div class="item-row">
<span class="item-label">申请数量</span>
<span class="item-value">{{ item.requisition_item.request_qty }}</span>
</div>
<div class="item-row">
<span class="item-label">已归还数量</span>
<span class="item-value">{{ item.requisition_item.returned_qty }}</span>
</div>
<div class="item-row">
<span class="item-label">本次归还</span>
<span class="item-value">{{ item.return_qty }}</span>
</div>
</div>
</div>
</div>
<!-- 审批意见 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>审批意见</span>
</div>
<ElInput
v-model="approvalForm.comment"
type="textarea"
:rows="3"
placeholder="请输入审批意见"
/>
</div>
<!-- 上传附件 -->
<div class="info-section">
<div class="section-title">
<div class="title-bar"></div>
<span>附件(可选)</span>
</div>
<el-upload
v-model:file-list="approvalFileList"
:action="uploadAction"
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:on-remove="handleUploadRemove"
:limit="5"
multiple
>
<el-button type="primary">点击上传</el-button>
<template #tip>
<div class="el-upload__tip">支持图片、PDF等格式,最多上传5个文件</div>
</template>
</el-upload>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton
type="danger"
plain
size="large"
class="submit-btn"
@click="handleSubmitApprove('reject')"
>
拒绝
</ElButton>
<ElButton
type="primary"
size="large"
class="submit-btn"
@click="handleSubmitApprove('approve')"
>
同意
</ElButton>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import type { UploadUserFile, UploadProps } from 'element-plus'
import { invitemService } from '@/api/admin/Itemrequisition'
import { useAuth } from '@/composables/useAuth'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
defineOptions({ name: 'ReturnApprovalPage' })
const { hasAuth } = useAuth()
const { uploadHeaders } = useUploadHeaders()
// 上传地址
const uploadAction = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=return_approval`
// 状态选项
const statusOptions = [
{ label: '待审核', value: 0 },
{ label: '已通过', value: 1 },
{ label: '未通过', value: 2 }
]
// 获取状态标签颜色
const getStatusType = (status: number): 'success' | 'danger' | 'info' | 'warning' | 'primary' => {
const typeMap: Record<number, 'success' | 'danger' | 'info' | 'warning' | 'primary'> = {
0: 'warning', // 待审核
1: 'success', // 已通过
2: 'danger' // 未通过
}
return typeMap[status] || 'info'
}
// 获取状态文本
const getStatusLabel = (status: number) => {
const labelMap: Record<number, string> = {
0: '待审核',
1: '已通过',
2: '未通过'
}
return labelMap[status] || '未知'
}
// 判断是否可以审批
const canApprove = (row: any) => {
return row.status === 0
}
// 获取归还物品名称列表
const getReturnItemNames = (returnItems: any[]) => {
if (!returnItems || returnItems.length === 0) return '-'
const names = returnItems
.map((item) => {
if (item.item && item.item.length > 0) {
return item.item[0].item_name
}
return ''
})
.filter(Boolean)
return names.join('、')
}
// 获取图片预览URL列表
const getImagePreviewUrls = (attachments: any[]) => {
if (!attachments || attachments.length === 0) return []
return attachments.map((item) => item.file_path_pull)
}
const loading = ref(false)
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// 搜索表单
const searchForm = reactive({
keyword: '',
status: undefined as number | undefined
})
// 详情弹窗数据
const detailDialogVisible = ref(false)
const detailData = ref<any>({})
// 审批弹窗数据
const approvalDialogVisible = ref(false)
const currentRowId = ref<number | null>(null)
const approvalDetail = ref<any>({})
const approvalForm = reactive({
comment: '',
attachment: [] as any[]
})
const approvalFileList = ref<UploadUserFile[]>([])
// 上传成功处理
const handleUploadSuccess: UploadProps['onSuccess'] = (response) => {
if (response.code === 200) {
approvalForm.attachment.push({
original_name: response.data.original_name || '文件',
file_path: response.data.file_path || response.data.file_url,
file_url: response.data.file_url
})
ElMessage.success('文件上传成功')
}
}
// 删除文件处理
const handleUploadRemove: UploadProps['onRemove'] = (file) => {
const index = approvalForm.attachment.findIndex(
(item) => item.file_url === file.url || item.original_name === file.name
)
if (index > -1) {
approvalForm.attachment.splice(index, 1)
}
}
onMounted(() => {
getList()
})
// 获取列表数据
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
current: currentPage,
size: pageSize
}
if (searchForm.keyword) {
params.keyword = searchForm.keyword
}
if (searchForm.status !== undefined) {
params.status = searchForm.status
}
const { records, current, size, total } = await invitemService.getReturnList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.currentPage = 1
getList()
}
// 分页变化
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
// 查看详情
const handleViewDetail = async (row: any) => {
try {
const detail = await invitemService.getReturnDetail({ id: row.id })
detailData.value = detail
detailDialogVisible.value = true
} catch (error) {
console.error('获取详情失败:', error)
}
}
// 打开审批弹窗 - 获取完整详情
const handleApprove = async (row: any) => {
try {
const detail = await invitemService.getReturnDetail({ id: row.id })
currentRowId.value = row.id
approvalDetail.value = detail
approvalForm.comment = ''
approvalForm.attachment = []
approvalFileList.value = []
approvalDialogVisible.value = true
} catch (error) {
console.error('获取详情失败:', error)
}
}
// 提交审批
const handleSubmitApprove = async (action: 'approve' | 'reject') => {
if (!approvalForm.comment) {
ElMessage.warning('请输入审批意见')
return
}
try {
const { message } = await invitemService.returnconfirm({
id: currentRowId.value!,
action,
comment: approvalForm.comment,
attachment: approvalForm.attachment.map((item) => ({
original_name: item.original_name,
file_path: item.file_path
}))
})
ElMessage.success(message)
approvalDialogVisible.value = false
approvalForm.comment = ''
approvalForm.attachment = []
currentRowId.value = null
approvalDetail.value = {}
approvalFileList.value = []
getList()
} catch (error) {
console.error('审批失败:', error)
}
}
</script>
<style scoped lang="scss">
.return-approval-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.return-toolbar {
display: flex;
justify-content: flex-end;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
.toolbar-right {
display: flex;
align-items: center;
}
}
.clickable {
cursor: pointer;
}
// 详情和审批弹窗样式
.detail-content,
.approval-content {
max-height: 60vh;
overflow-y: auto;
}
.info-section {
margin: 20px 0;
.section-title {
display: flex;
align-items: center;
margin-bottom: 16px;
.title-bar {
width: 4px;
height: 20px;
background: #409eff;
margin-right: 8px;
border-radius: 2px;
}
span {
font-size: 16px;
font-weight: 600;
color: #303133;
}
}
}
.info-row {
margin-bottom: 12px;
}
.info-item {
display: flex;
flex-direction: column;
label {
font-size: 13px;
color: #909399;
margin-bottom: 6px;
}
span {
font-size: 14px;
color: #303133;
}
}
// 物品列表
.item-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.item-card {
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
background: #fafafa;
.item-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
&:last-child {
margin-bottom: 0;
}
.item-label {
color: #909399;
}
.item-value {
color: #303133;
font-weight: 500;
}
}
}
// 图片附件列表
.image-attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.image-attachment-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
width: 100px;
}
.attachment-image {
width: 100px;
height: 100px;
border-radius: 8px;
border: 1px solid #e4e7ed;
overflow: hidden;
background: #f5f7fa;
:deep(.el-image__inner) {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.image-error {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
font-size: 32px;
}
.image-name {
font-size: 12px;
color: #606266;
text-align: center;
word-break: break-all;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// 弹窗底部按钮
.dialog-footer {
display: flex;
justify-content: center;
gap: 20px;
.submit-btn {
min-width: 120px;
}
}
</style>
+1 -36
View File
@@ -176,13 +176,7 @@
</el-select>
</el-form-item>
<el-form-item label="选项" size="small">
<el-input
v-if="group.type == 'field_user'"
placeholder="请输入"
v-model="group.users"
></el-input>
<el-select
v-else
v-model="group.users"
multiple
placeholder="请选择"
@@ -257,13 +251,7 @@
</el-select>
</el-form-item>
<el-form-item label="选项" size="small">
<el-input
v-if="group.type == 'field_user'"
placeholder="请输入"
v-model="group.users"
></el-input>
<el-select
v-else
v-model="group.users"
multiple
placeholder="请选择"
@@ -437,13 +425,7 @@
// const businessUsers = ref([])
// 选项缓存,避免重复请求
const optionsCache = reactive({
role: [],
user: [],
department: [],
grouplist: [],
group_members: []
})
const optionsCache = reactive({ role: [], user: [], department: [] })
// 懒加载并缓存不同类型的选项
const ensureOptionsLoaded = async (group) => {
@@ -467,21 +449,6 @@
optionsCache.user = response || []
}
group.options = optionsCache.user
} else if (type === 'scenario_leader_of_initiator') {
if (!optionsCache.grouplist || optionsCache.grouplist.length === 0) {
const response = await commonService.getUserGroupScene()
optionsCache.grouplist = response || []
}
group.options = optionsCache.grouplist
} else if (type === 'group_members') {
if (!optionsCache.group_members || optionsCache.group_members.length === 0) {
const response = await commonService.getUserGroupSelect()
response.forEach((group) => {
group.name = group.title
})
optionsCache.group_members = response || []
}
group.options = optionsCache.group_members
} else {
group.options = []
}
@@ -672,8 +639,6 @@
node.approverGroups.forEach((group) => {
if (group && group.type === 'initiator_leader') {
approvers.push({ type: 'initiator_leader' })
} else if (group && group.type === 'field_user') {
approvers.push({ type: group.type, value: group.users })
} else if (group && group.type && Array.isArray(group.users) && group.users.length > 0) {
approvers.push({ type: group.type, value: group.users.join(',') })
}
+7 -66
View File
@@ -176,13 +176,7 @@
</el-select>
</el-form-item>
<el-form-item label="选项" size="small">
<el-input
v-if="group.type == 'field_user'"
placeholder="请输入"
v-model="group.users"
></el-input>
<el-select
v-else
v-model="group.users"
multiple
placeholder="请选择"
@@ -257,13 +251,7 @@
</el-select>
</el-form-item>
<el-form-item label="选项" size="small">
<el-input
v-if="group.type == 'field_user'"
placeholder="请输入"
v-model="group.users"
></el-input>
<el-select
v-else
v-model="group.users"
multiple
placeholder="请选择"
@@ -465,21 +453,6 @@
optionsCache.user = response || []
}
group.options = optionsCache.user
} else if (type === 'scenario_leader_of_initiator') {
if (!optionsCache.grouplist || optionsCache.grouplist.length === 0) {
const response = await commonService.getUserGroupScene()
optionsCache.grouplist = response || []
}
group.options = optionsCache.grouplist
} else if (type === 'group_members') {
if (!optionsCache.group_members || optionsCache.group_members.length === 0) {
const response = await commonService.getUserGroupSelect()
response.forEach((group) => {
group.name = group.title
})
optionsCache.group_members = response || []
}
group.options = optionsCache.group_members
} else {
group.options = []
}
@@ -616,24 +589,11 @@
await ensureOptionsLoaded(group)
if (ap.type === 'initiator_leader') {
group.users = []
} else if (ap.type === 'field_user') {
group.users = ap.value.split(',').map((v) => v.trim())
} else if (ap.value) {
const values = String(ap.value)
group.users = String(ap.value)
.split(',')
.map((v) => (v || '').trim())
.filter(Boolean)
// 判断是否为纯数字ID
const isNumericIds = values.every(
(v) => !Number.isNaN(Number(v)) && Number.isInteger(Number(v))
)
if (isNumericIds) {
// 如果是数字ID,转换为数字数组
group.users = values.map((v) => Number(v))
} else {
// 如果是字符串名称,保持为字符串数组
group.users = values
}
.map((v) => Number((v || '').trim()))
.filter((n) => !Number.isNaN(n))
}
node.approverGroups.push(group)
}
@@ -651,28 +611,11 @@
await ensureOptionsLoaded(group)
if (c.type === 'initiator_leader') {
group.users = []
} else if (c.type === 'field_user') {
group.users = c.value.split(',').map((v) => v.trim())
} else if (c.value) {
// group.users = String(c.value)
// .split(',')
// .map((v) => Number((v || '').trim()))
// .filter((n) => !Number.isNaN(n))
const values = String(c.value)
group.users = String(c.value)
.split(',')
.map((v) => (v || '').trim())
.filter(Boolean)
// 判断是否为纯数字ID
const isNumericIds = values.every(
(v) => !Number.isNaN(Number(v)) && Number.isInteger(Number(v))
)
if (isNumericIds) {
// 如果是数字ID,转换为数字数组
group.users = values.map((v) => Number(v))
} else {
// 如果是字符串名称,保持为字符串数组
group.users = values
}
.map((v) => Number((v || '').trim()))
.filter((n) => !Number.isNaN(n))
}
node.ccGroups.push(group)
}
@@ -789,8 +732,6 @@
node.approverGroups.forEach((group) => {
if (group && group.type === 'initiator_leader') {
approvers.push({ type: 'initiator_leader' })
} else if (group && group.type === 'field_user') {
approvers.push({ type: group.type, value: group.users })
} else if (group && group.type && Array.isArray(group.users) && group.users.length > 0) {
approvers.push({ type: group.type, value: group.users.join(',') })
}
@@ -807,7 +748,7 @@
if (group && group.type === 'initiator_leader') {
cc.push({ type: 'initiator_leader' })
} else if (group && group.type && Array.isArray(group.users) && group.users.length > 0) {
cc.push({ type: group.type, value: group.users })
cc.push({ type: group.type, value: group.users.join(',') })
}
})
}
+1 -6
View File
@@ -47,12 +47,7 @@
<ElTableColumn fixed="right" label="操作" width="140px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('approval_list@edit')"
>编辑</el-button
>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button
type="danger"
link
+23 -70
View File
@@ -29,13 +29,7 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
<template #default="scope">
@@ -56,12 +50,11 @@
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
v-if="hasAuth('stocking_auth@show')"
>
<i class="iconfont-sys">&#xe76c;</i> 查看审批
</el-link>
<el-link
v-if="scope.row.status == 'pending' && hasAuth('stocking_auth@approve')"
v-if="scope.row.status == 'pending'"
type="success"
underline="never"
@click="handleCreateRepair(scope.row)"
@@ -86,9 +79,7 @@
>
<div class="approval-content">
<!-- 头部信息 -->
<div
v-if="orderDialog.type == 'stock_orders' || orderDialog.type == 'stock_orders_nullify'"
>
<div v-if="orderDialog.type == 'stock_orders'">
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<div v-if="orderDialog.type == 'purchase_orders'">
@@ -123,12 +114,7 @@
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
>同意</ElButton
>
<ElButton
v-if="orderDialog.type == 'purchase_orders'"
type="warning"
size="large"
class="submit-btn"
@click="handleTransfer()"
<ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
>转交</ElButton
>
</div>
@@ -192,26 +178,6 @@
userOptions.value = res || []
// return res || []
}
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
const zjForm = reactive({
to_uid: '',
instance_id: '',
@@ -292,38 +258,25 @@
id: row.id,
flow_type: row.flow_type
})
console.log(res)
switch (row.flow_type) {
case 'stock_orders': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
break
}
case 'stock_orders_nullify': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '备货单-作废'
break
}
case 'purchase_orders': {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-采购单(采购)'
break
}
if (row.flow_type == 'stock_orders') {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
} else if (row.flow_type == 'purchase_orders') {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-采购单(采购)'
}
approvalDialogVisible.value = true
}
@@ -1,7 +1,7 @@
<template>
<div class="stocking-show-page">
<div class="boxshadow">
<div v-if="flow_type == 'stock_orders' || flow_type == 'stock_orders_nullify'">
<div v-if="flow_type == 'stock_orders'">
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<div v-if="flow_type == 'purchase_orders'">
@@ -10,17 +10,43 @@
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<FilePreview title="合同文件" :files="contractFiles" empty-text="暂无合同文件" />
</div>
<!-- PI文件 -->
<div class="boxshadow">
<FilePreview title="PI文件" :files="piFiles" empty-text="暂无PI文件" />
</div>
<!-- 水单文件 -->
<div class="boxshadow">
<FilePreview title="水单文件" :files="waterBillFiles" empty-text="暂无水单文件" />
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<div class="boxshadow">
<div class="form-title">
@@ -72,23 +98,13 @@
<div class="form-card" v-if="status == 'pending'">
<!-- 底部信息 -->
<div class="submit-wrap">
<ElButton
type="primary"
plain
size="large"
class="submit-btn"
@click="handleSubmit('reject')"
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleSubmit('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
>同意</ElButton
>
<ElButton
v-if="flow_type == 'purchase_orders'"
type="warning"
size="large"
class="submit-btn"
@click="handleTransfer()"
<ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
>转交</ElButton
>
</div>
@@ -152,7 +168,6 @@
import { usePdfPreview } from '@/composables/usePdfPreview'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
import FilePreview from '@/components/custom/FilePreview.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
import { router } from '@/router'
defineOptions({ name: 'AdminStockingAuthDetil' })
@@ -233,20 +248,7 @@
}
// 合同文件
// 文件数据
const contractFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const piFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const waterBillFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
// 处理文件数据
const processFiles = (files: any[], type: string) => {
if (!files || !Array.isArray(files)) return []
return files.map((file: any) => ({
name: file.original_name,
url: file.file_full_url || file.file_url,
file_name: file.file_url
}))
}
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
function extractFileName(pathOrUrl: string | undefined | null): string {
if (!pathOrUrl) return '合同文件'
try {
@@ -263,7 +265,7 @@
id: approval_id,
flow_type: flow_type
})
if (flow_type == 'stock_orders' || flow_type == 'stock_orders_nullify') {
if (flow_type == 'stock_orders') {
const orderData: OrderData = res
orderDialog.orderData = orderData
} else if (flow_type == 'purchase_orders') {
@@ -279,26 +281,13 @@
orderData.contract_file_url = list
orderDialog.orderData = orderData
}
// 处理合同文件
const file_list = res?.file_list
if (file_list?.contract) {
contractFiles.value = processFiles(file_list.contract, '合同')
} else {
contractFiles.value = []
}
// 处理PI文件
if (file_list?.pi_doc) {
piFiles.value = processFiles(file_list.pi_doc, 'PI')
} else {
piFiles.value = []
}
// 处理水单文件
if (file_list?.transaction_slip) {
waterBillFiles.value = processFiles(file_list.transaction_slip, '水单')
} else {
waterBillFiles.value = []
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
// 审批记录(兼容多种字段)
const resstockapprovallogs = await manageService.stockapprovallogs({ instance_id: instance_id })
@@ -32,13 +32,7 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
@@ -61,12 +55,11 @@
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
v-if="hasAuth('stocking_auth_history@show')"
>
<i class="iconfont-sys">&#xe76c;</i> 查看审批
</el-link>
<el-link
v-if="scope.row.status == 'pending' && hasAuth('stocking_auth_history@approve')"
v-if="scope.row.status == 'pending'"
type="success"
underline="never"
@click="handleCreateRepair(scope.row)"
@@ -154,8 +147,8 @@
channel: '',
customer_name: '',
client_country: '',
cif_total: '',
cif_total_currency: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
@@ -165,25 +158,7 @@
onMounted(() => {
getList()
})
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
+1 -11
View File
@@ -85,8 +85,7 @@
</div>
</div>
<el-dialog v-model="detailDialogVisible" title="详情">
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<el-dialog v-model="audetDialogVisible" title="审核" width="700px">
<!-- 客户标记无效 原因与附件回显 -->
@@ -328,18 +327,9 @@
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.cursor = 'zoom-in'
})
return doc.body.innerHTML
}
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
const descViewerUrl = ref('')
function onDescImgClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.tagName === 'IMG') {
descViewerUrl.value = (target as HTMLImageElement).src
}
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
+1 -11
View File
@@ -145,8 +145,7 @@
</div>
</div>
<div class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<!-- 分配跟单对话框 -->
<el-dialog v-model="zyDialogVisible" title="分配" width="360px">
@@ -349,18 +348,9 @@
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.cursor = 'zoom-in'
})
return doc.body.innerHTML
}
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
const descViewerUrl = ref('')
function onDescImgClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.tagName === 'IMG') {
descViewerUrl.value = (target as HTMLImageElement).src
}
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
+1 -11
View File
@@ -238,8 +238,7 @@
</div>
</div>
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<!-- 转移跟单对话框 -->
<el-dialog v-model="zyDialogVisible" title="转移" width="360px">
@@ -529,18 +528,9 @@
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.cursor = 'zoom-in'
})
return doc.body.innerHTML
}
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
const descViewerUrl = ref('')
function onDescImgClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.tagName === 'IMG') {
descViewerUrl.value = (target as HTMLImageElement).src
}
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
+14 -43
View File
@@ -57,13 +57,8 @@
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
<el-table-column prop="user_name" label="跟单人"></el-table-column>
<el-table-column prop="del_source" label="来源"></el-table-column>
<el-table-column
prop="status_desc"
label="删除原因"
show-overflow-tooltip
></el-table-column>
<!-- <el-table-column prop="follow_last" label="上次跟进"></el-table-column> -->
<!-- <el-table-column prop="follow_next" label="下次跟进"></el-table-column> -->
<el-table-column prop="follow_last" label="上次跟进"></el-table-column>
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
<el-table-column
prop="code"
label="编号"
@@ -144,8 +139,7 @@
</div>
</div>
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
<!-- 图片预览对话框 -->
<el-image-viewer
@@ -238,18 +232,9 @@
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.cursor = 'zoom-in'
})
return doc.body.innerHTML
}
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
const descViewerUrl = ref('')
function onDescImgClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.tagName === 'IMG') {
descViewerUrl.value = (target as HTMLImageElement).src
}
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
@@ -272,34 +257,20 @@
})
}
const delectfn = (row: any) => {
ElMessageBox.prompt('确定要删除此客户吗?请填写入删除原因!', '提示', {
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^(?!\s*$).+/,
inputErrorMessage: '请填写入删除原因'
}).then(async ({ value }) => {
const res = await manageService.crmCustomerDelete({
customer_id: row.id,
scene: 'force',
status_desc: value
})
ElMessage.success(res?.message || '提交成功!')
gettableList()
type: 'warning'
})
// ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// })
// .then(async () => {
// const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'force' })
// ElMessage.success(res?.message || '提交成功!')
// gettableList()
// // 你可以在这里调用接口或做后续处理
// })
// .catch(() => {
// // 用户点击了取消
// })
.then(async () => {
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'force' })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
const closeImageViewer = () => {
imageViewerVisible.value = false
-301
View File
@@ -1,301 +0,0 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<div class="toolbar-left"></div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<el-tabs v-model="activeTab">
<el-tab-pane label="未认领" name="not"></el-tab-pane>
<el-tab-pane label="已认领" name="already"></el-tab-pane>
<el-tab-pane label="超时未认领" name="timeout"></el-tab-pane>
</el-tabs>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:row-class-name="tableRowClassName"
>
<template #default>
<ElTableColumn label="创建日期" prop="created_at" min-width="160" />
<!-- <ElTableColumn label="付款来源" prop="country_name" min-width="120" /> -->
<ElTableColumn label="汇款人" prop="remitter" min-width="120" />
<ElTableColumn label="账户" prop="account_name" min-width="120" />
<ElTableColumn label="账币" prop="currency_name" min-width="120" />
<ElTableColumn label="金额" prop="amount" min-width="120" />
<ElTableColumn label="计时" prop="amount" min-width="120">
<template #default="scope">
<span
v-if="!scope.row.payment_apply_id"
:class="getCountdownClass(scope.row.claim_timeout_time)"
>
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
<i class="iconfont-sys">&#xe7b1;</i>
</span>
{{ getCountdown(scope.row.claim_timeout_time) }}
</span>
<span v-else class="countdown-success">已认领</span>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
type="primary"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
v-if="hasAuth('claim@show')"
>
<i class="iconfont-sys">&#xe76c;</i> 查看
</el-link>
<el-link
v-if="!scope.row.payment_apply_id && hasAuth('claim@claim')"
type="success"
underline="never"
@click="handleClaim(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 认领
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 付款记录选择对话框 -->
<PaymentRecordDialog
v-model="paymentDialogVisible"
:receipt-id="currentReceiptId"
@confirm="handlePaymentRecordConfirm"
/>
<!-- 付款记录详情查看对话框 -->
<ClaimRecordDetailDialog v-model="detailDialogVisible" :payment-id="currentPaymentId" />
</div>
</template>
<script setup lang="ts">
import { manageService } from '@/api/adminApi'
import { financeService } from '@/api/admin/finance'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useAuth } from '@/composables/useAuth'
import PaymentRecordDialog from '@/components/custom/PaymentRecordDialog.vue'
import ClaimRecordDetailDialog from '@/components/custom/ClaimRecordDetailDialog.vue'
import dayjs from 'dayjs'
const { hasAuth } = useAuth()
defineOptions({ name: 'Adminclaim' })
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const activeTab = ref('not')
const detailDialogVisible = ref(false)
// 付款记录对话框相关
const paymentDialogVisible = ref(false)
const currentReceiptId = ref<string | number>('')
watch([keyword], () => {
pagination.currentPage = 1
getList()
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
watch([activeTab], () => {
pagination.currentPage = 1
getList()
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
payment_apply: activeTab.value,
keyword: keyword.value
}
const { records, current, size, total } = await financeService.getReceiptList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const currentPaymentId = ref<string | number>('')
const handleView = async (params: any) => {
detailDialogVisible.value = true
currentPaymentId.value = params.id
await financeService.getReceiptDetail(params)
}
// 处理认领操作
const handleClaim = (row: any) => {
currentReceiptId.value = row.id
paymentDialogVisible.value = true
}
// 处理付款记录确认绑定
const handlePaymentRecordConfirm = async (paymentRecord: any) => {
try {
const params = {
id: currentReceiptId.value,
payment_apply_id: paymentRecord.id
}
await financeService.bindPaymentRecord(params)
ElMessage.success('认领成功')
getList() // 刷新列表
} catch (error) {
console.error('认领失败:', error)
ElMessage.error('认领失败,请稍后重试')
}
}
const handleDelete = () => {}
const timer = ref(0)
const refreshKey = ref(0)
onMounted(() => {
getList()
timer.value = window.setInterval(() => {
refreshKey.value++
}, 1000)
})
function getCountdown(targetDate: string) {
// 让 refreshKey 成为依赖,确保每秒刷新
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
refreshKey.value
const now = dayjs()
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
if (!target.isValid()) return '时间格式错误'
const diff = target.diff(now)
if (diff <= 0) return '已超时'
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
const minutes = Math.floor((diff / (1000 * 60)) % 60)
const seconds = Math.floor((diff / 1000) % 60)
return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
}
function getCountdownClass(targetDate: string) {
const now = dayjs()
const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
if (!target.isValid() || target.diff(now) <= 0) {
return 'countdown-danger'
}
return 'countdown-success'
}
const tableRowClassName = ({ row }: { row: any }) => {
const flag = Number(row?.claim_timeout)
return flag === 1 ? 'row-claim-timeout' : ''
}
</script>
<style scoped lang="scss">
:deep(.row-claim-timeout) {
background-color: #ffc9c9 !important;
}
:deep(.el-tabs__nav-wrap:after){
background: #fff;
}
.countdown-danger {
color: #ff0000 !important;
}
.countdown-success {
color: #00a43f !important;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 6px;
margin-right: 10px;
}
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
// 创建收款记录 - 弹窗样式对齐设计图
.receipt-dialog {
.receipt-form {
.el-form-item {
margin-bottom: 20px;
}
}
.upload-demo {
width: 100%;
display: flex;
align-items: center;
// justify-content: flex-end;
// border: 1px dashed #dcdfe6;
border-radius: 6px;
cursor: pointer;
}
.avatarimg {
width: 60px;
cursor: pointer;
}
.el-button {
width: 200px;
height: 40px;
font-size: 16px;
}
}
:deep(.receipt-dialog .el-dialog__footer) {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
</style>
File diff suppressed because it is too large Load Diff
-838
View File
@@ -1,838 +0,0 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<div class="toolbar-left">
<ElButton
type="primary"
v-ripple
@click="handleCreate"
v-if="hasAuth('receipt_records@add')"
>
创建收款记录
</ElButton>
</div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:key="tableData.length"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:row-class-name="tableRowClassName"
>
<template #default>
<ElTableColumn label="创建日期" prop="created_at" min-width="140" />
<!-- <ElTableColumn label="付款来源" prop="country_name" min-width="160" /> -->
<ElTableColumn label="汇款人" prop="remitter" min-width="120" />
<ElTableColumn label="账户" prop="account_short_desc" min-width="120" />
<ElTableColumn label="币种" prop="currency_name" min-width="120" />
<ElTableColumn label="金额" prop="amount" min-width="120" />
<ElTableColumn label="折让金额(含手续费)" prop="discount_amount" min-width="120" />
<ElTableColumn label="认领状态" prop="payment_apply_id" min-width="120">
<template #default="scope">
<span
v-if="scope.row.payment_apply_id == 0 || scope.row.payment_apply_id == null"
class="text-danger"
>
未认领
</span>
<span v-else class="text-success">已认领</span>
</template>
</ElTableColumn>
<ElTableColumn label="入账" prop="id" min-width="120">
<template #default="scope">
<span v-if="scope.row.received_at">{{ scope.row.received_at }}</span>
<el-button
v-else-if="hasAuth('receipt_records@confirmentry')"
type="primary"
size="small"
@click="handlebtn(scope.row)"
>
确认入账
</el-button>
<span v-else></span>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="180" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-link
v-if="hasAuth('receipt_records@show')"
type="primary"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看
</el-link>
<el-link
v-if="hasAuth('receipt_records@update')"
type="success"
underline="never"
@click="handleEdit(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe6bf;</i> 修改
</el-link>
<!-- <el-link
v-if="hasAuth('receipt_records@destroy')"
type="danger"
underline="never"
@click="handleDelete()"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe6d4;</i> 作废
</el-link> -->
<el-link
v-if="hasAuth('receipt_records@history')"
type="warning"
underline="never"
@click="handleHistory(scope.row)"
>
<i class="iconfont-sys">&#xe64e;</i> 历史记录
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新建/编辑收款记录 - 通用弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑收款记录' : '创建收款记录'"
width="900px"
class="receipt-dialog"
>
<el-form
ref="createFormRef"
:model="form"
:rules="createRules"
label-width="180px"
class="receipt-form"
>
<ElRow :gutter="12" class="item-row-wrap">
<!-- <ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="付款来源">
<el-select v-model="form.country_id" placeholder="请选择付款国家">
<el-option
v-for="item in countryOptions"
:key="item.id"
:label="item.cn_addrs"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
</ElCol> -->
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="汇款人">
<el-input v-model="form.remitter" placeholder="请填写汇款人" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="收款账户">
<el-select v-model="form.account_id" placeholder="请选择收款账户">
<el-option
v-for="item in accountOptions"
:key="item.id"
:label="item.short_desc"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="币种">
<el-select v-model="form.currency_id" placeholder="请选择币种">
<el-option
v-for="item in currencyOptions"
:key="item.id"
:label="item.currency_name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="金额">
<el-input v-model="form.amount" placeholder="请填写金额" />
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="到账时间">
<el-date-picker
v-model="form.arrived_at"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
type="date"
placeholder="请选择到账时间"
style="width: 100%"
/>
</el-form-item>
</ElCol>
<!-- <ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="折让金额">
<el-input v-model="form.discount_amount" placeholder="请填写折让金额" />
</el-form-item>
</ElCol> -->
<ElCol :sm="24" :md="12" :lg="12" v-if="isEdit">
<el-form-item label="收汇凭证号">
<el-input v-model="form.voucher_no" placeholder="请填写收汇凭证号" />
</el-form-item>
</ElCol>
<!-- <ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="实际汇率(出纳)">
<el-input v-model="form.exchange_rate" placeholder="请填写实际汇率" />
</el-form-item>
</ElCol> -->
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="总金额(人民币)实际汇率">
<el-input
v-model="form.total_amount_cny"
placeholder="请填写总金额(人民币)实际汇率"
/>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="12" :lg="12">
<el-form-item label="是否转回内地">
<el-radio-group v-model="form.is_domestic_return">
<el-radio :label="0">否</el-radio>
<el-radio :label="1">是</el-radio>
</el-radio-group>
</el-form-item>
</ElCol>
<ElCol :sm="24" :md="24" :lg="24">
<el-form-item label="外汇到账回单(出纳)">
<el-upload
class="upload-demo"
:action="actionImageUplode"
:headers="uploadHeaders"
:on-success="handleUploadSuccess1"
:show-file-list="false"
:multiple="true"
>
<img :src="imageUrl1" alt="" class="avatarimg" />
<input type="hidden" v-model="form.fx_receipt_file" />
</el-upload>
<div
v-if="Array.isArray(form.fx_receipt_file) && form.fx_receipt_file.length"
style="margin-top: 8px"
>
<div
v-for="(u, i) in form.fx_receipt_file"
:key="i"
style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px"
>
<img
v-if="
/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test(
(u.file_full || '').split('?')[0]
)
"
:src="u.file_full"
style="
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--el-border-color);
"
/>
<a
v-else
:href="u.file_full"
target="_blank"
rel="noopener"
style="
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
"
>{{ u.file_name }}</a
>
<el-button link type="danger" @click="removeVoucher(i)" style="width: 80px"
>移除</el-button
>
</div>
</div>
</el-form-item>
</ElCol>
</ElRow>
</el-form>
<template #footer>
<el-button type="primary" @click="handleSubmit">提交</el-button>
</template>
</el-dialog>
<el-dialog
v-model="dialogVisibleshow"
title="查看收款记录"
width="900px"
class="receipt-dialog"
>
<div class="info-grid">
<div class="info-item">
<label>创建日期:</label>
<span>{{ formshow.created_at }}</span>
</div>
<!-- <div class="info-item">
<label>付款来源:</label>
<span>{{ formshow.country_name || '—' }}</span>
</div> -->
<div class="info-item">
<label>汇款人:</label>
<span>{{ formshow.remitter || '—' }}</span>
</div>
<div class="info-item">
<label>账户:</label>
<span>{{ formshow.account_name || '—' }}</span>
</div>
<div class="info-item">
<label>币种:</label>
<span>{{ formshow.currency_name || '—' }}</span>
</div>
<div class="info-item">
<label>金额:</label>
<span class="amount">{{ formshow.amount }}</span>
</div>
<div class="info-item">
<label>到账时间:</label>
<span>{{ formshow.arrived_at || '—' }}</span>
</div>
<!-- <div class="info-item">
<label>折让金额:</label>
<span>{{ formshow.discount_amount || '—' }}</span>
</div> -->
<div class="info-item">
<label>收汇凭证号:</label>
<span>{{ formshow.voucher_no || '—' }}</span>
</div>
<!-- <div class="info-item">
<label>实际汇率(出纳):</label>
<span>{{ formshow.exchange_rate || '—' }}</span>
</div> -->
<div class="info-item">
<label>总金额(人民币)实际汇率:</label>
<span>{{ formshow.total_amount_cny || '—' }}</span>
</div>
<div class="info-item">
<label>是否转回内地:</label>
<span>{{ formshow.is_domestic_return == 1 ? '是' : '否' }}</span>
</div>
<div class="info-item">
<label>外汇到账回单:</label>
<div>
<p v-for="(item, index) in formshow.fx_receipt_file" :key="index">
<el-image
v-if="
/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test((item.file_full || '').split('?')[0])
"
:src="item.file_full"
:preview-src-list="item.file_full ? [item.file_full] : []"
style="width: 100px; height: 100px"
/>
<a
v-else
:href="item.file_full"
target="_blank"
rel="noopener"
style="
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 200px;
"
>{{ item.file_name }}</a
>
</p>
</div>
</div>
</div>
</el-dialog>
<!-- 历史记录 -->
<el-dialog
v-model="timelineDialogVisible"
title="历史记录"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="approval-dialog"
>
<div class="approval-content">
<!-- 头部信息 -->
<Timeline :records="approvalRecords" />
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { commonService } from '@/api/commonApi'
import { financeService } from '@/api/admin/finance'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useAuth } from '@/composables/useAuth'
import imageUrl1 from '@/assets/img/crm/upimg.png'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { defineAsyncComponent } from 'vue'
const Timeline = defineAsyncComponent(() => import('@/components/custom/Timeline.vue'))
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=receipt_records`
defineOptions({ name: 'AdminReceiptRecords' })
const approvalRecords = ref<any[]>([])
const { hasAuth } = useAuth()
const loading = ref(false)
const dialogVisibleshow = ref(false)
const timelineDialogVisible = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
//国家
const countryOptions = ref<any[]>([])
const fetchCountryOptions = async () => {
try {
const res = await commonService.getCountrySelect()
res.forEach((item: any) => {
item.cn_addrs = item.cn_addr + '(' + item.en_addr + ')'
})
countryOptions.value = res || []
} catch {
countryOptions.value = []
}
}
//账户
const accountOptions = ref<any[]>([])
const fetchAccountOptions = async () => {
try {
const res = await commonService.getAccountSelect()
accountOptions.value = res || []
} catch {
accountOptions.value = []
}
}
//币种
const currencyOptions = ref<any[]>([])
const fetchCurrencyOptions = async () => {
try {
const res = await commonService.getCurrencySelect()
currencyOptions.value = res || []
} catch {
currencyOptions.value = []
}
}
// 审批弹出框相关
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
getList()
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await financeService.getReceiptList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const tableRowClassName = ({ row }: { row: any }) => {
const flag = Number(row?.claim_timeout)
return flag === 1 ? 'row-claim-timeout' : ''
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
// 新建/编辑弹窗通用状态
const dialogVisible = ref(false)
const createLoading = ref(false)
const isEdit = ref(false)
const currentId = ref<string | number | null>(null)
const createFormRef = ref<FormInstance>()
interface FileInfo {
file_name?: string
file_full?: string
file_src?: string
}
const form = ref({
remitter: '',
account_id: '',
currency_id: '',
amount: '',
arrived_at: new Date().toISOString().split('T')[0],
voucher_no: '',
total_amount_cny: '',
is_domestic_return: 0,
fx_receipt_file: [] as FileInfo[],
fx_receipt_file_list: [] as object[]
})
const createRules: FormRules = {
remitter: [{ required: true, message: '请输入汇款人', trigger: 'blur' }],
account_id: [{ required: true, message: '请选择账户', trigger: 'change' }],
currency_id: [{ required: true, message: '请选择币种', trigger: 'change' }]
}
const formshow = ref({
created_at: '',
country_name: '',
remitter: '',
account_name: '',
currency_name: '',
amount: '',
arrived_at: '',
voucher_no: '',
total_amount_cny: '',
is_domestic_return: 0,
fx_receipt_file: [] as FileInfo[],
fx_receipt_file_full: '',
fx_receipt_file_list: [] as object[]
})
const resetCreateForm = () => {
Object.assign(form.value, {
remitter: '',
account_id: '',
currency_id: '',
amount: '',
arrived_at: new Date().toISOString().split('T')[0],
voucher_no: '',
total_amount_cny: '',
is_domestic_return: 0,
fx_receipt_file: [] as FileInfo[],
fx_receipt_file_list: []
})
currentId.value = null
createFormRef.value?.clearValidate?.()
}
const handleCreate = async () => {
resetCreateForm()
isEdit.value = false
fetchAccountOptions()
fetchCurrencyOptions()
fetchCountryOptions()
dialogVisible.value = true
}
const handleSubmit = () => {
createFormRef.value?.validate?.(async (valid: boolean) => {
if (!valid) return
try {
createLoading.value = true
// TODO: 根据新增/编辑调用对应接口
if (isEdit.value) {
const formData = { ...form.value }
// if ('fx_receipt_file_list' in formData) {
// delete formData.fx_receipt_file_list
// }
const forms = { ...formData, id: currentId.value }
const { message } = await financeService.updateReceipt({
remitter: forms.remitter,
account_id: forms.account_id,
currency_id: forms.currency_id,
amount: forms.amount,
arrived_at: forms.arrived_at,
voucher_no: forms.voucher_no,
total_amount_cny: forms.total_amount_cny,
is_domestic_return: forms.is_domestic_return,
fx_receipt_file: forms.fx_receipt_file,
id: forms.id
})
ElMessage.success(message)
} else {
const formData = { ...form.value }
const { message } = await financeService.addReceipt({
remitter: formData.remitter,
account_id: formData.account_id,
currency_id: formData.currency_id,
amount: formData.amount,
arrived_at: formData.arrived_at,
voucher_no: formData.voucher_no,
total_amount_cny: formData.total_amount_cny,
is_domestic_return: formData.is_domestic_return,
fx_receipt_file: formData.fx_receipt_file
})
ElMessage.success(message)
}
dialogVisible.value = false
getList()
} finally {
createLoading.value = false
}
})
}
const handleView = (row?: any) => {
// const fx_receipt_file_full = []
// if (row.fx_receipt_file_full) {
// fx_receipt_file_full.push(row.fx_receipt_file_full)
// }
formshow.value = row
// formshow.value.fx_receipt_file_list = fx_receipt_file_full
dialogVisibleshow.value = true
}
const handleEdit = (row?: any) => {
resetCreateForm()
isEdit.value = true
currentId.value = row?.id ?? null
if (row && typeof row === 'object') {
// form.value.country_id = row.country_id ?? form.value.country_id
form.value.remitter = row.remitter ?? form.value.remitter
form.value.account_id = row.account_id ?? form.value.account_id
form.value.currency_id = row.currency_id ?? form.value.currency_id
form.value.amount = row.amount ?? form.value.amount
form.value.arrived_at = row.arrived_at ?? form.value.arrived_at
// form.value.discount_amount = row.discount_amount ?? form.value.discount_amount
form.value.voucher_no = row.voucher_no ?? form.value.voucher_no
form.value.total_amount_cny = row.total_amount_cny ?? form.value.total_amount_cny
// form.value.exchange_rate = row.exchange_rate ?? form.value.exchange_rate
form.value.is_domestic_return = row.is_domestic_return ?? form.value.is_domestic_return
form.value.fx_receipt_file = row.fx_receipt_file ?? form.value.fx_receipt_file
form.value.fx_receipt_file_list = row.fx_receipt_file ?? form.value.fx_receipt_file
// const fx_receipt_file_full = []
// if (row.fx_receipt_file_full) {
// fx_receipt_file_full.push(row.fx_receipt_file_full)
// }
// form.value.fx_receipt_file_list = Array.isArray(fx_receipt_file_full)
// ? fx_receipt_file_full
// : form.value.fx_receipt_file_list
}
fetchAccountOptions()
fetchCurrencyOptions()
fetchCountryOptions()
dialogVisible.value = true
}
const handlebtn = (row?: any) => {
ElMessageBox.prompt('你是否确认入账?确认入账请填写入账实时汇率!', '入账', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^(?!\s*$).+/,
inputErrorMessage: '请填写入账实时汇率'
}).then(({ value }) => {
console.log(value)
financeService.confirmReceipt({ id: row.id, exchange_rate: value }).then((res) => {
ElMessage.success(res.message)
getList()
})
})
}
// const handleDelete = () => {
// ElMessageBox.confirm('确定删除吗?', '提示', {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
// financeService.deleteReceipt({ id: currentId.value }).then((res) => {
// ElMessage.success(res.message)
// getList()
// })
// })
// }
const handleHistory = (row: any) => {
financeService.getReceiptHistory({ receipt_id: row.id }).then((res) => {
timelineDialogVisible.value = true
approvalRecords.value = res.records || []
// ElMessage.success(res.message)
// getList()
})
// history.push('/admin/finance/receipt_records_history')
}
// function handleUploadSuccess1(response: any) {
// const fileName = response?.data?.file_name || ''
// const fileUrl = response?.data?.file_url || response?.data?.full_url || response?.url || ''
// form.value.fx_receipt_file = [form.value.fx_receipt_file, fileName].filter(Boolean).join(',')
// const list = Array.isArray(form.value.fx_receipt_file_list)
// ? form.value.fx_receipt_file_list
// : []
// list.push(fileUrl)
// form.value.fx_receipt_file_list = [...list]
// ElMessage.success(response.message)
// }
function handleUploadSuccess1(response: any) {
const fileName = response?.data?.file_name || ''
const fileUrl = response?.data?.file_url || response?.data?.full_url || response?.url || ''
const names = Array.isArray(form.value.fx_receipt_file) ? [...form.value.fx_receipt_file] : []
const files = ref({
file_src: '',
file_name: '',
file_full: ''
})
files.value.file_src = fileName
files.value.file_name = response?.data?.original_name || ''
files.value.file_full = response?.data?.file_url || ''
if (fileName) names.push(files.value)
form.value.fx_receipt_file = names
const list = Array.isArray(form.value.fx_receipt_file_list)
? form.value.fx_receipt_file_list
: []
list.push(fileUrl)
form.value.fx_receipt_file_list = [...list]
ElMessage.success(response.message)
}
// 预览项移除(同步预览数组与提交字段)
function removeVoucher(index: number) {
// const list = Array.isArray(form.value.fx_receipt_file_list)
// ? [...form.value.fx_receipt_file_list]
// : []
// if (index < 0 || index >= list.length) return
// list.splice(index, 1)
// form.value.fx_receipt_file_list = list
// const names = (form.value.fx_receipt_file || '').split(',').filter(Boolean)
// if (index < names.length) names.splice(index, 1)
// form.value.fx_receipt_file = names.join(',')
const list = Array.isArray(form.value.fx_receipt_file_list)
? [...form.value.fx_receipt_file_list]
: []
if (index < 0 || index >= list.length) return
list.splice(index, 1)
form.value.fx_receipt_file_list = list
const names = Array.isArray(form.value.fx_receipt_file) ? [...form.value.fx_receipt_file] : []
if (index < names.length) names.splice(index, 1)
form.value.fx_receipt_file = names
}
</script>
<style scoped lang="scss">
:deep(.row-claim-timeout) {
background-color: #ffc9c9 !important;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 6px;
margin-right: 10px;
}
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
// 创建收款记录 - 弹窗样式对齐设计图
.receipt-dialog {
.receipt-form {
.el-form-item {
margin-bottom: 20px;
}
}
.upload-demo {
width: 100%;
display: flex;
align-items: center;
// justify-content: flex-end;
// border: 1px dashed #dcdfe6;
border-radius: 6px;
cursor: pointer;
}
.avatarimg {
width: 60px;
cursor: pointer;
}
.el-button {
width: 200px;
height: 40px;
font-size: 16px;
}
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
.info-item {
display: flex;
align-items: center;
// padding: 8px 0;
label {
font-weight: 500;
color: #666;
min-width: 100px;
margin-right: 8px;
}
span {
color: #333;
flex: 1;
&.amount {
font-weight: 600;
color: #3d7bff;
font-size: 16px;
}
}
}
}
:deep(.receipt-dialog .el-dialog__footer) {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
</style>
+7 -7
View File
@@ -68,7 +68,7 @@
</el-form>
</section>
<div class="table">
<el-button class="reset-button" @click="addfn()" v-if="hasAuth('inquiry@store')"><i class="iconfont-sys">&#xe602;</i> 录单</el-button>
<el-button class="reset-button" @click="addfn()" v-if="hasAuth('inquiry#store')"><i class="iconfont-sys">&#xe602;</i> 录单</el-button>
<ArtTable
ref="tableRef"
row-key="id"
@@ -121,7 +121,7 @@
</el-table-column>
<el-table-column prop="recipient" label="跟单人">
<template #default="scope">
<span style="color:#008477;cursor: pointer;" v-if="scope.row.recipientInfo && hasAuth('inquiry@reallocation')" @click="onFenpei(scope.row,'edit')">
<span style="color:#008477;cursor: pointer;" v-if="scope.row.recipientInfo && hasAuth('inquiry#reallocation')" @click="onFenpei(scope.row,'edit')">
{{scope.row.recipientInfo? scope.row.recipientInfo.name : ''}}
<i class="iconfont-sys">&#xe626;</i>
</span>
@@ -147,31 +147,31 @@
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="260px">
<template #default="scope">
<div class="follow-action fenpei" @click="onFenpei(scope.row,'add')" v-if="scope.row.status!= 2&&hasAuth('inquiry@allocation')">
<div class="follow-action fenpei" @click="onFenpei(scope.row,'add')" v-if="scope.row.status!= 2&&hasAuth('inquiry#allocation')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe852;</i></span
>
分配
</div>
<div class="follow-action detil" @click="onDetail(scope.row)" v-if="hasAuth('inquiry@show')">
<div class="follow-action detil" @click="onDetail(scope.row)" v-if="hasAuth('inquiry#show')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe70f;</i></span
>
详情
</div>
<div class="follow-action" @click="onFollow(scope.row)" v-if="scope.row.status==0&&hasAuth('inquiry@release')">
<div class="follow-action" @click="onFollow(scope.row)" v-if="scope.row.status==0&&hasAuth('inquiry#release')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe712;</i></span
>
发布
</div>
<div class="follow-action edit" @click="onEdit(scope.row)" v-if="hasAuth('inquiry@update')">
<div class="follow-action edit" @click="onEdit(scope.row)" v-if="hasAuth('inquiry#update')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe6bf;</i></span
>
编辑
</div>
<div class="follow-action delete" @click="onDelete(scope.row)" v-if="hasAuth('inquiry@destroy')">
<div class="follow-action delete" @click="onDelete(scope.row)" v-if="hasAuth('inquiry#destroy')">
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
><i class="iconfont-sys">&#xe6d4;</i></span
>
-288
View File
@@ -1,288 +0,0 @@
<template>
<div class="customer-page">
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('changelog@add')"
>新增</ElButton
>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="编号" prop="id" width="100px" />
<el-table-column prop="id" label="内容" min-width="300px">
<template #default="scope">
<span v-html="scope.row.content" style="white-space: pre-line"></span>
</template>
</el-table-column>
<ElTableColumn label="发布时间" prop="update_time" width="180px" />
<ElTableColumn label="备注" prop="remark" />
<ElTableColumn fixed="right" label="操作" width="120px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('changelog@edit')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('changelog@del')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog v-model="dialogVisible" :title="dialogType === 'add' ? '新增' : '编辑'" align-center>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
<ElFormItem label="发布日期" prop="update_time">
<ElDatePicker
v-model="form.update_time"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择发布日期"
style="width: 100%"
/>
</ElFormItem>
<ElFormItem label="更新内容" prop="content">
<ElInput v-model="form.content" type="textarea" :rows="3" placeholder="请输入通知内容" />
</ElFormItem>
<ElFormItem label="备注" prop="remark">
<ElInput v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { logService } from '@/api/admin/log'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Changelog' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const { width } = useWindowSize()
const keyword = ref<string>('')
const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
abbr: [{ required: true, message: '请输入公司简称', trigger: 'blur' }]
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
})
const form = reactive<any>({
id: '',
content: '',
update_time: '',
remark: ''
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
watch([keyword], () => {
pagination.currentPage = 1
getWorkList()
})
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await logService.getChangelogList({
current: currentPage,
size: pageSize,
keyword: keyword.value
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const dialogType = ref('add')
const showDialog = (type: string, row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.content = row.content
form.update_time = row.update_time
form.remark = row.remark
form.id = row.id
// form.tenant_key = row.tenant_key
} else {
form.id = ''
form.content = ''
form.update_time = ''
form.remark = ''
}
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await logService.deleteChangelog({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
if (dialogType.value === 'add') {
const { message } = await logService.addChangelog({
content: form.content,
update_time: form.update_time,
remark: form.remark
})
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
} else {
const { message } = await logService.updateChangelog(form)
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
}
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.customer-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.customer-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.customer-search-group {
display: flex;
gap: 12px;
}
.message-text {
padding: 10px 14px;
line-height: 1.7;
white-space: pre-line;
word-break: break-word;
}
.detail-dialog {
display: flex;
flex-direction: column;
gap: 16px;
}
.section {
background: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 12px 14px;
}
.info-grid {
display: grid;
grid-template-columns: 96px 1fr;
gap: 6px 12px;
align-items: center;
padding: 6px 0;
}
.label {
color: var(--el-text-color-secondary);
font-size: 13px;
}
.value {
color: var(--el-text-color-primary);
font-size: 14px;
}
.content-box {
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
max-height: 320px;
overflow: auto;
}
</style>
+10 -52
View File
@@ -2,13 +2,7 @@
<div class="port-page">
<div class="port-toolbar boxshadow">
<div class="toolbar-left">
<ElButton
@click="showDialog('add')"
v-ripple
type="primary"
v-if="hasAuth('purchase_order@add')"
>创建</ElButton
>
<ElButton @click="showDialog('add')" v-ripple type="primary">创建</ElButton>
</div>
<div class="toolbar-right">
<el-input
@@ -62,38 +56,19 @@
<ElTableColumn fixed="right" label="操作" width="240px">
<template #default="scope">
<ElRow>
<el-button
type="success"
link
@click="showDialogdetil(scope.row)"
v-if="hasAuth('purchase_order@show')"
>
<el-button type="success" link @click="showDialogdetil(scope.row)">
详情
</el-button>
<el-button
type="warning"
link
@click="showDialogtimeline(scope.row)"
v-if="hasAuth('purchase_order@approval')"
>
<el-button type="warning" link @click="showDialogtimeline(scope.row)">
查看审批
</el-button>
<el-button
link
v-if="
scope.row.status == 0 ||
(scope.row.status == 3 && hasAuth('purchase_order@edit'))
"
v-if="scope.row.status == 0 || scope.row.status == 3"
@click="showDialog('edit', scope.row)"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deleteprocurement(scope.row)"
v-if="hasAuth('purchase_order@del')"
>删除</el-button
>
<el-button type="danger" link @click="deleteprocurement(scope.row)">删除</el-button>
</ElRow>
</template>
</ElTableColumn>
@@ -108,15 +83,10 @@
width="900px"
class="purchase-dialog"
>
<ElForm ref="formRef" :model="poForm" :rules="poRules" label-width="90px" class="form">
<ElForm ref="formRef" :model="poForm" :rules="poRules" label-width="120px" class="form">
<ElFormItem label="PI号" prop="pi_no">
<ElSelect v-model="poForm.pi_no" placeholder="请选择PI号" style="width: 100%">
<ElOption
v-for="pi in piOptions"
:key="pi.pi_no"
:value="pi.pi_no"
:label="pi.pi_nosnapshot"
/>
<ElOption v-for="pi in piOptions" :key="pi.pi_no" :value="pi.pi_no" />
</ElSelect>
</ElFormItem>
@@ -168,7 +138,7 @@
type="primary"
@click="openVehiclePicker(gIdx)"
:disabled="!group.id"
>修改</ElButton
>车辆</ElButton
>
</template>
<template v-else>
@@ -281,10 +251,10 @@
@selection-change="onVehicleSelectionChange"
style="width: 100%"
>
<el-table-column type="selection" width="48" />
<el-table-column label="车辆" prop="model_snapshot" min-width="160" />
<el-table-column label="内饰" prop="color_exterior_snapshot" />
<el-table-column label="外饰" prop="color_interior_snapshot" />
<el-table-column type="selection" width="48" />
</el-table>
<template #footer>
<ElButton @click="vehiclePicker.visible = false">取消</ElButton>
@@ -378,8 +348,6 @@
import imageUrl1 from '@/assets/img/crm/upimg.png'
import ApprovalTimeline from '@/components/custom/ApprovalTimeline.vue'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const approvalRecords = ref<any[]>([])
const { uploadHeaders } = useUploadHeaders()
const timelineDialogVisible = ref(false)
@@ -398,15 +366,7 @@
const loadOptions = async () => {
try {
const res = await purchaseService.purchaseorderpredata({ scene: 'pi_no' })
const list = Array.isArray(res) ? res : []
if (Array.isArray(list)) {
list.forEach((item: any) => {
item.pi_nosnapshot = item.pi_no + '(' + item.salesperson_snapshot + ')'
})
piOptions.value = list
} else {
piOptions.value = res
}
piOptions.value = res || []
} catch {
piOptions.value = []
}
@@ -964,8 +924,6 @@
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted',
comment: h?.comment || h?.remark || ''
}))
} else {
approvalRecords.value = []
}
timelineDialogVisible.value = true
}
+163 -191
View File
@@ -41,17 +41,11 @@
<ElTableColumn label="状态" prop="approval_status">
<template #default="scope">
<div v-if="scope.row.appro_status">
<div>
<span :class="statusClass(scope.row.appro_status.status)">
{{
scope.row.appro_status.status_desc
? scope.row.appro_status.status_desc
: ''
}}
</span>
<span v-if="scope.row.appro_status.name != ''">
【{{ scope.row.appro_status.name }}】
</span>
<div
><span :class="statusClass(scope.row.appro_status.status)">{{ scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : '' }}</span>
<span v-if="scope.row.appro_status.name != ''"
>【{{ scope.row.appro_status.name }}】</span
>
</div>
</div>
<div v-else></div>
@@ -69,38 +63,12 @@
>
<i class="iconfont-sys">&#xe782;</i> 打印预览
</el-link>
<el-link
type="primary"
underline="never"
@click="handleView1(scope.row)"
style="margin-right: 8px"
>
<el-link type="primary" underline="never" @click="handleView1(scope.row)">
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
<el-link
type="warning"
underline="never"
@click="showDialogtimeline(scope.row)"
style="margin-right: 8px"
>
<el-link type="warning" underline="never" @click="showDialogtimeline(scope.row)">
<i class="iconfont-sys">&#xe728;</i> 查看审批
</el-link>
<el-link
type="danger"
underline="never"
@click="stockdeletefn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe783;</i> 删除
</el-link>
<!-- <el-link
type="primary"
underline="never"
@click="handleViewnq(scope.row)"
style="margin-right: 8px;color: #750AF8"
>
<i class="iconfont-sys">&#xe76c;</i> 内勤专用
</el-link> -->
</ElRow>
</template>
</ElTableColumn>
@@ -115,19 +83,44 @@
:close-on-click-modal="false"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
<!-- 合同文件 -->
<div class="boxshadow">
<FilePreview title="合同文件" :files="contractFiles" empty-text="暂无合同文件" />
</div>
<!-- PI文件 -->
<div class="boxshadow">
<FilePreview title="PI文件" :files="piFiles" empty-text="暂无PI文件" />
</div>
<!-- 水单文件 -->
<div class="boxshadow">
<FilePreview title="水单文件" :files="waterBillFiles" empty-text="暂无水单文件" />
<div class="form-title">
<!-- <span class="border"></span> -->
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
@@ -143,9 +136,9 @@
top="40px"
:close-on-click-modal="false"
>
<div style="max-height: 50vh; display: flex; flex-direction: column; gap: 12px">
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px">
<el-button @click="download" v-if="isFinites">下载</el-button>
<el-button @click="download">下载</el-button>
</div>
<div
v-if="pdf.images.length"
@@ -165,13 +158,6 @@
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
<div class="inter-view">
<InternalStaffViews
v-model:visible="pdf.visible"
:vehicle-data="nqData"
:default-currency="nqCurrency"
/>
</div>
</el-dialog>
<el-dialog
v-model="timelineDialogVisible"
@@ -186,40 +172,22 @@
<ApprovalTimeline :records="approvalRecords" />
</div>
</el-dialog>
<!-- 内勤查看弹窗 -->
<!-- <InternalStaffView
v-model:visible="nqDialogVisible"
:vehicle-data="nqData"
:default-currency="nqCurrency"
/> -->
</div>
</template>
<script setup lang="ts">
// import { router } from '@/router'
import { router } from '@/router'
// import { CrmService } from '@/api/crmApi'
import { manageService } from '@/api/adminApi'
// import { useAuth } from '@/composables/useAuth'
import { useAuth } from '@/composables/useAuth'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import ApprovalTimeline from '@/components/custom/ApprovalTimeline.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
import { usePdfPreview } from '@/composables/usePdfPreview'
import FilePreview from '@/components/custom/FilePreview.vue'
// import InternalStaffView from '@/components/custom/InternalStaffView.vue'
import InternalStaffViews from '@/components/custom/InternalStaffViews.vue'
const timelineDialogVisible = ref(false)
const approvalRecords = ref<any[]>([])
const isFinites = ref(false)
// PDF 预览/打印(公共)
const {
pdf,
frameRef,
openWithResponse,
download,
/* print,*/
preparePreview,
prepareImages
} = usePdfPreview()
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
defineOptions({ name: 'AdminFinalListPage' })
// 订单详情弹窗
const orderDialog = reactive({
@@ -228,18 +196,13 @@
orderData: {} as OrderData
})
// const { hasAuth } = useAuth()
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// 内勤查看:币种与数据
const nqDialogVisible = ref(false)
const nqCurrency = ref('')
const nqData = ref<any[]>([])
onMounted(() => {
getList()
})
@@ -285,12 +248,6 @@
}
//打印
const printfn = (row: any) => {
if (row.approval_status == 2) {
isFinites.value = true
} else {
isFinites.value = false
}
handleViewnq(row)
manageService
.printStock({ id: row.id })
.then((res) => {
@@ -304,78 +261,22 @@
console.log(err)
})
}
const handleViewnq = async (row: any) => {
const res = await manageService.checkStock({ id: row.id })
// 默认币种来自 currency_symbol
if (res?.currency_symbol) {
nqCurrency.value = res.currency_symbol
} else {
nqCurrency.value = 'USD'
}
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
nqData.value = list.map((it: any) => ({
model_snapshot: it.model_snapshot,
qty: 1,
clearance_price: it.clearance_price,
clearance_price_currency: it.clearance_price?.currency || it.clearance_price_currency,
fob_transport_fee: it.fob_transport_fee,
fob_fee_currency: it.fob_transport_fee?.currency || it.fob_fee_currency,
car_price: it.car_price,
car_price_currency: it.car_price?.currency || it.car_price_currency,
ocean_freight: it.ocean_freight,
ocean_freight_currency: it.ocean_freight?.currency || it.ocean_freight_currency,
commission: it.commission,
commission_currency: it.commission?.currency || it.commission_currency,
retention_price: it.retention_price,
retention_price_currency: it.retention_price?.currency || it.retention_price_currency,
port_misc_fee: it.port_misc_fee,
port_fee_currency: it.port_misc_fee?.currency || it.port_fee_currency,
model: it.model_snapshot
}))
// nqDialogVisible.value = true
}
// 文件数据
const contractFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const piFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const waterBillFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
// 处理文件数据
const processFiles = (files: any[], type: string) => {
if (!files || !Array.isArray(files)) return []
return files.map((file: any) => ({
name: file.original_name,
url: file.file_full_url || file.file_url,
file_name: file.file_url
}))
}
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const handleView1 = async (row: any) => {
const res = await manageService.checkStock({ id: row.object_id })
// 处理合同文件
const file_list = res?.file_list
if (file_list?.contract) {
contractFiles.value = processFiles(file_list.contract, '合同')
} else {
contractFiles.value = []
}
// 处理PI文件
if (file_list?.pi_doc) {
piFiles.value = processFiles(file_list.pi_doc, 'PI')
} else {
piFiles.value = []
}
// 处理水单文件
if (file_list?.transaction_slip) {
waterBillFiles.value = processFiles(file_list.transaction_slip, '水单')
} else {
waterBillFiles.value = []
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
if (res) {
// 获取订单详情成功
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(终版)'
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
// router.push({
@@ -384,6 +285,110 @@
// })
// TODO: 跳转或弹窗查看备货单
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
//审批进度
const showDialogtimeline = async (row: any) => {
// 审批记录(兼容多种字段)
@@ -403,34 +408,9 @@
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted',
comment: h?.comment || h?.remark || ''
}))
} else {
approvalRecords.value = []
}
timelineDialogVisible.value = true
}
//删除
const stockdeletefn = (row: any) => {
ElMessageBox.confirm('确定要删除此备货单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
manageService
.delectStock({ id: row.id })
.then((res) => {
ElMessage.success(res?.message || '操作成功')
getList()
})
.catch((err) => {
console.log(err)
})
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
</script>
<style scoped lang="scss">
@@ -455,9 +435,7 @@
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
.clickable { cursor: pointer; }
.status-following {
color: #00a43f;
font-weight: bold;
@@ -485,10 +463,4 @@
font-size: 48px;
color: #ddd;
}
.inter-view {
margin-top: 10px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 10px;
}
</style>
+6 -6
View File
@@ -177,8 +177,8 @@
<ElRow :gutter="12">
<ElCol :span="6">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.cif_total" disabled>
<template #append>{{ base.cif_total_currency }}</template>
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
@@ -332,8 +332,8 @@
channel: '',
customer_name: '',
client_country: '',
cif_total: '',
cif_total_currency: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
@@ -429,8 +429,8 @@
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.cif_total = res?.cif_total_amount?.amount ?? ''
base.cif_total_currency = res?.cif_total_amount?.currency || ''
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
+6 -6
View File
@@ -172,8 +172,8 @@
<ElRow :gutter="12">
<ElCol :span="6">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.cif_total" disabled>
<template #append>{{ base.cif_total_currency }}</template>
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
@@ -327,8 +327,8 @@
channel: '',
customer_name: '',
client_country: '',
cif_total: '',
cif_total_currency: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
@@ -421,8 +421,8 @@
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.cif_total = res?.cif_total_amount?.amount ?? ''
base.cif_total_currency = res?.cif_total_amount?.currency || ''
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
+178 -275
View File
@@ -42,15 +42,11 @@
<ElTableColumn label="状态" prop="approval_status">
<template #default="scope">
<div v-if="scope.row.appro_status">
<div>
<span :class="statusClass(scope.row.appro_status.status)">
{{
scope.row.appro_status.status_desc
? scope.row.appro_status.status_desc
: ''
}}
</span>
<span v-if="scope.row.appro_status.name != ''">【{{ scope.row.appro_status.name }}】</span>
<div
><span :class="statusClass(scope.row.appro_status.status)">{{ scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : '' }}</span>
<span v-if="scope.row.appro_status.name != ''"
>【{{ scope.row.appro_status.name }}】</span
>
</div>
</div>
<div v-else></div>
@@ -72,7 +68,7 @@
type="success"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px; color: #333333"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看
</el-link>
@@ -81,26 +77,12 @@
type="success"
underline="never"
@click="handleView1(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
<el-link
type="warning"
underline="never"
@click="showDialogtimeline(scope.row)"
style="margin-right: 8px"
>
<el-link type="warning" underline="never" @click="showDialogtimeline(scope.row)">
<i class="iconfont-sys">&#xe728;</i> 查看审批
</el-link>
<el-link
type="danger"
underline="never"
@click="stockdeletefn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe783;</i> 删除
</el-link>
</ElRow>
</template>
</ElTableColumn>
@@ -116,9 +98,9 @@
top="40px"
:close-on-click-modal="false"
>
<div style="max-height: 50vh; display: flex; flex-direction: column; gap: 12px">
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
<div style="display: flex; justify-content: flex-end; gap: 8px;">
<el-button @click="download" v-if="isFinites">下载</el-button>
<el-button @click="download">下载</el-button>
</div>
<div
v-if="pdf.images.length"
@@ -138,13 +120,6 @@
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
<div class="inter-view">
<InternalStaffViews
v-model:visible="pdf.visible"
:vehicle-data="nqData"
:default-currency="nqCurrency"
/>
</div>
</el-dialog>
<!-- 订单详情弹窗(使用公共内容组件) -->
<el-dialog
@@ -154,19 +129,44 @@
:close-on-click-modal="false"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
<!-- 合同文件 -->
<div class="boxshadow">
<FilePreview title="合同文件" :files="contractFiles" empty-text="暂无合同文件" />
</div>
<!-- PI文件 -->
<div class="boxshadow">
<FilePreview title="PI文件" :files="piFiles" empty-text="暂无PI文件" />
</div>
<!-- 水单文件 -->
<div class="boxshadow">
<FilePreview title="水单文件" :files="waterBillFiles" empty-text="暂无水单文件" />
<div class="form-title">
<!-- <span class="border"></span> -->
<span class="title">合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<template #footer>
<div style="display: flex; justify-content: flex-end; gap: 8px">
@@ -187,12 +187,6 @@
<ApprovalTimeline :records="approvalRecords" />
</div>
</el-dialog>
<!-- 内勤查看弹窗 -->
<!-- <InternalStaffView
v-model:visible="nqDialogVisible"
:vehicle-data="nqData"
:default-currency="nqCurrency"
/> -->
</div>
</template>
@@ -202,15 +196,8 @@
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import ApprovalTimeline from '@/components/custom/ApprovalTimeline.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
import FilePreview from '@/components/custom/FilePreview.vue'
// import InternalStaffView from '@/components/custom/InternalStaffView.vue'
import InternalStaffViews from '@/components/custom/InternalStaffViews.vue'
const timelineDialogVisible = ref(false)
const approvalRecords = ref<any[]>([])
// const nqDialogVisible = ref(false)
const nqCurrency = ref('')
const nqData = ref<any[]>([])
const isFinites = ref(false)
defineOptions({ name: 'StockingListAdmin' })
// import { useAuth } from '@/composables/useAuth'
// const { hasAuth } = useAuth()
@@ -228,14 +215,13 @@
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// PDF 预览/打印(公共)
const { pdf, frameRef, openWithResponse, download, /* print,*/ preparePreview, prepareImages } = usePdfPreview()
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
onMounted(() => {
getList()
})
function statusClass(status: any) {
if (status === 'rejected' || status === 'voided') return 'status-lost'
if (status === 'rejected') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
@@ -274,48 +260,18 @@
getList()
}
// 文件数据
const contractFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const piFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const waterBillFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
// 处理文件数据
const processFiles = (files: any[], type: string) => {
if (!files || !Array.isArray(files)) return []
return files.map((file: any) => ({
name: file.original_name,
url: file.file_full_url || file.file_url,
file_name: file.file_url
}))
}
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const handleView = async (row: any) => {
try {
// 获取订单详情数据
const res = await manageService.checkStock({ id: row.id })
// 处理合同文件
const file_list = res?.file_list
if (file_list?.contract) {
contractFiles.value = processFiles(file_list.contract, '合同')
} else {
contractFiles.value = []
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
// 处理PI文件
if (file_list?.pi_doc) {
piFiles.value = processFiles(file_list.pi_doc, 'PI')
} else {
piFiles.value = []
}
// 处理水单文件
if (file_list?.transaction_slip) {
waterBillFiles.value = processFiles(file_list.transaction_slip, '水单')
} else {
waterBillFiles.value = []
}
if (res) {
// 转换数据格式以适配 OrderDetailDialog
const orderData: OrderData = res
@@ -328,49 +284,141 @@
console.error('获取订单详情失败:', error)
ElMessage.error('获取订单详情失败')
}
// router.push({
// path: '/admin/stock/show_admin',
// query: { id: row.id } // 可带客户名等参数
// })
// TODO: 跳转或弹窗查看备货单
}
const handleView1 = async (row: any) => {
const res = await manageService.checkStock({ id: row.object_id })
// 处理合同文件
const file_list = res?.file_list
if (file_list?.contract) {
contractFiles.value = processFiles(file_list.contract, '合同')
} else {
contractFiles.value = []
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
// 处理PI文件
if (file_list?.pi_doc) {
piFiles.value = processFiles(file_list.pi_doc, 'PI')
} else {
piFiles.value = []
}
// 处理水单文件
if (file_list?.transaction_slip) {
waterBillFiles.value = processFiles(file_list.transaction_slip, '水单')
} else {
waterBillFiles.value = []
}
if (res) {
// 获取订单详情成功
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.title = '合塑实业-汽车事业部-备货单(终版)'
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
orderDialog.visible = true
}
// router.push({
// path: '/admin/stock/final_show_admin',
// query: { id: row.object_id } // 可带客户名等参数
// })
// router.push('/crmpc/stocking/final_show')
// TODO: 跳转或弹窗查看备货单
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
//打印
const printfn = (row: any) => {
if (row.approval_status == 2) {
isFinites.value = true
} else {
isFinites.value = false
}
handleViewnq(row)
manageService
.printStock({ id: row.id })
.then((res) => {
@@ -403,65 +451,9 @@
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted',
comment: h?.comment || h?.remark || ''
}))
} else {
approvalRecords.value = []
}
timelineDialogVisible.value = true
}
const handleViewnq = async (row: any) => {
const res = await manageService.checkStock({ id: row.id })
// 设置默认币种为数据中的 currency_symbol
if (res?.currency_symbol) {
nqCurrency.value = res.currency_symbol
} else {
nqCurrency.value = 'USD' // 兜底默认值
}
// 取车辆明细列表作为行
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
nqData.value = list.map((it: any) => ({
model_snapshot: it.model_snapshot,
qty: 1,
clearance_price: it.clearance_price,
clearance_price_currency: it.clearance_price?.currency || it.clearance_price_currency,
fob_transport_fee: it.fob_transport_fee,
fob_fee_currency: it.fob_transport_fee?.currency || it.fob_fee_currency,
car_price: it.car_price,
car_price_currency: it.car_price?.currency || it.car_price_currency,
ocean_freight: it.ocean_freight,
ocean_freight_currency: it.ocean_freight?.currency || it.ocean_freight_currency,
commission: it.commission,
commission_currency: it.commission?.currency || it.commission_currency,
retention_price: it.retention_price,
retention_price_currency: it.retention_price?.currency || it.retention_price_currency,
port_misc_fee: it.port_misc_fee,
port_fee_currency: it.port_misc_fee?.currency || it.port_fee_currency,
model: it.model_snapshot
}))
// nqDialogVisible.value = true
}
//删除
const stockdeletefn = (row: any) => {
ElMessageBox.confirm('确定要删除此备货单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
manageService
.delectStock({ id: row.id })
.then((res) => {
ElMessage.success(res?.message || '操作成功')
getList()
})
.catch((err) => {
console.log(err)
})
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
</script>
<style scoped lang="scss">
@@ -486,9 +478,7 @@
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
.clickable { cursor: pointer; }
.status-following {
color: #00a43f;
font-weight: bold;
@@ -516,92 +506,5 @@
font-size: 48px;
color: #ddd;
}
.files-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.file-item {
flex: 0 0 auto;
min-width: 200px;
max-width: 300px;
}
.file-preview {
position: relative;
width: 100%;
max-height: 120px;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
background: #fafafa;
padding: 10px;
}
.image-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.preview-image {
flex: 1;
max-height: 80px;
object-fit: cover;
cursor: pointer;
transition: transform 0.2s;
}
.preview-image:hover {
transform: scale(1.05);
}
.document-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.file-icon {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.file-icon i {
font-size: 48px;
color: #409eff;
}
.file-info {
padding: 8px;
background: rgba(255, 255, 255, 0.9);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.file-name {
font-size: 12px;
color: #606266;
text-align: center;
word-break: break-all;
max-width: 100%;
}
.inter-view {
margin-top: 10px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 10px;
}
</style>
+7 -3
View File
@@ -14,13 +14,18 @@
<!-- 表格头部 -->
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
<template #left>
<!-- <el-segmented
<el-segmented
v-model="yearVal"
size="large"
:options="segmentedOptions"
@change="searchArticleByYear"
>
</el-segmented> -->
<!-- <template #default="scope">
<div class="flex flex-col items-center gap-2 p-2">
<div>{{ scope.item.label }}</div>
</div>
</template> -->
</el-segmented>
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
<!-- <ElButton
v-auth="'system@menu_add'"
@@ -52,7 +57,6 @@
:data="filteredTableData"
:marginTop="10"
:stripe="false"
:pagination="false"
>
<template #default>
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
+3 -7
View File
@@ -183,15 +183,11 @@
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params: any = {
const { records, current, size, total } = await systemService.getNoticeList({
current: currentPage,
size: pageSize
}
if (keyword.value) {
params.keyword = keyword.value
}
const { records, current, size, total } = await systemService.getNoticeList(params)
})
tableData.value = records.map((item: any) => ({
...item
}))
-116
View File
@@ -1,116 +0,0 @@
<template>
<div class="page-content">
<div class="customer-toolbar boxshadow">
<div class="customer-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="ID" prop="id" />
<ElTableColumn label="操作人" prop="user_name" />
<ElTableColumn label="请求路由" prop="route" />
<ElTableColumn label="用户ip" prop="ip" />
<ElTableColumn label="用户ua" prop="ua" />
<ElTableColumn label="入参摘要" prop="params_brief" />
<ElTableColumn label="异常" prop="message" />
<ElTableColumn label="创建时间" prop="created_at" />
</template>
</ArtTable>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { logService } from '@/api/admin/log'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'Operationlog' })
const keyword = ref('')
const loading = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
watch([keyword], () => {
pagination.currentPage = 1
getWorkList()
})
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await logService.getOperationLogList({
current: currentPage,
size: pageSize,
keyword: keyword.value
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
.customer-search-group {
display: flex;
gap: 12px;
}
</style>
+3 -21
View File
@@ -97,7 +97,7 @@
<ElDialog v-model="permissionDialog" title="菜单权限" align-center>
<el-tabs v-model="activeName" class="demo-tabs">
<!-- <el-tab-pane
<el-tab-pane
v-for="sys in systemsList"
:key="sys.id"
:label="sys.name"
@@ -123,25 +123,7 @@
</template>
</ElTree>
</ElScrollbar>
</el-tab-pane> -->
<ElTree
:ref="(el:any) => setTreeRef(1, el)"
:data="processedMenuBySystem[1] || []"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="() => handleTreeCheck(1)"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
@@ -181,7 +163,7 @@
const treeRef = ref()
const treeRefwork = ref()
const treeRefcrm = ref()
const isExpandAll = ref(false)
const isExpandAll = ref(true)
const isSelectAll = ref(false)
const pagination = reactive({
currentPage: 1,
-281
View File
@@ -1,281 +0,0 @@
<template>
<div class="page-content">
<div v-if="!isMobile">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('group@add')">新增</ElButton>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="分组名称" prop="title" width="160px" />
<ElTableColumn label="负责人" prop="id" width="120px">
<template #default="scope">
<ElTag>{{ scope.row.leader_name }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="分组成员" prop="id">
<template #default="scope">
<span v-for="item in scope.row.user" :key="item.id" style="margin-right: 10px;"> {{ item.name }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" width="160px" />
<ElTableColumn fixed="right" label="操作" width="110px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('group@update')"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('group@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
width="40%"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
<ElFormItem label="分组名称" prop="title">
<ElInput v-model="form.title" />
</ElFormItem>
<ElFormItem label="分组成员" prop="user_ids">
<el-select
v-model="form.user_ids"
multiple
filterable
placeholder="请选择分组成员"
style="width: 100%"
>
<el-option
v-for="item in companyUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</ElFormItem>
<ElFormItem label="负责人" prop="leader_id">
<el-select
v-model="form.leader_id"
filterable
placeholder="请选择负责人"
style="width: 100%"
>
<el-option
v-for="item in companyUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</ElFormItem>
<ElFormItem label="业务类型" prop="scenario_code">
<ElInput v-model="form.scenario_code" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
<div v-else class="mobile-hide-tip">
<p>该页面暂不支持移动端访问</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { userConService } from '@/api/admin/user'
import { useAuth } from '@/composables/useAuth'
import { manageService } from '@/api/adminApi'
const { hasAuth } = useAuth()
defineOptions({ name: 'Company' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({
title: [{ required: true, message: '请输入分组名称', trigger: 'blur' }],
user_ids: [{ required: true, message: '请选择分组成员', trigger: 'blur' }]
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
})
const companyUserList = ref<any>()
// 获取用户列表
const getCompanyUserList = async () => {
const response = await manageService.presetusers()
companyUserList.value = response || []
}
const form = reactive<any>({
title: '',
user_ids: [],
leader_id: '',
scenario_code: '',
sort: 0,
status: 1
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await userConService.getGroupList({
current: currentPage,
size: pageSize
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const dialogType = ref('add')
const showDialog = (type: string, row?: any) => {
getCompanyUserList()
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.title = row.title
form.id = row.id
form.leader_id = row.leader_id
form.scenario_code = row.scenario_code
form.user_ids = row.user.map((item: any) => item.id)
form.sort = row.sort
console.log(row.user_ids)
console.log(form.user_ids)
form.status = row.status
// form.tenant_key = row.tenant_key
} else {
form.title = ''
form.id = ''
form.scenario_code = ''
form.sort = 0
form.leader_id = ''
form.user_ids = []
form.status = 1
// form.tenant_key = ''
}
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await userConService.deleteGroup({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
if (dialogType.value === 'add') {
const { message } = await userConService.addGroup({
title: form.title,
user_ids: form.user_ids,
leader_id: form.leader_id,
sort: form.sort,
scenario_code: form.scenario_code,
status: form.status
})
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
} else {
const { message } = await userConService.editGroup(form)
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
}
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
</style>
+3 -22
View File
@@ -60,7 +60,6 @@
<el-table-column prop="name" label="名字" align="center" />
<el-table-column prop="phone" label="电话" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column prop="report_to_name" label="直属上级" align="center" />
<el-table-column prop="corp_name" label="所属公司" align="center" />
<el-table-column prop="follow" label="状态" align="center">
<template #default="scope">
@@ -275,7 +274,7 @@
/>
<ElDialog v-model="permissionDialog" title="菜单权限" align-center>
<el-tabs v-model="activeName" class="demo-tabs">
<!-- <el-tab-pane
<el-tab-pane
v-for="sys in systemsList"
:key="sys.id"
:label="sys.name"
@@ -301,25 +300,7 @@
</template>
</ElTree>
</ElScrollbar>
</el-tab-pane> -->
<ElTree
:ref="(el: any) => setTreeRef(1, el)"
:data="processedMenuBySystem[1] || []"
show-checkbox
node-key="id"
:default-expand-all="isExpandAll"
:props="defaultProps"
@check="() => handleTreeCheck(1)"
>
<template #default="{ data }">
<div style="display: flex; align-items: center">
<span v-if="data.isAuth">
{{ data.label }}
</span>
<span v-else>{{ defaultProps.label(data) }}</span>
</div>
</template>
</ElTree>
</el-tab-pane>
</el-tabs>
<template #footer>
<div class="dialog-footer">
@@ -470,7 +451,7 @@
if (el) treeRefs[systemId] = el
}
const treeRef = ref()
const isExpandAll = ref(false)
const isExpandAll = ref(true)
const isSelectAll = ref(false)
const pagination = reactive({
currentPage: 1,
+1 -92
View File
@@ -27,14 +27,6 @@
<span class="info-label">公 司:</span>
<span class="info-value">{{ userInfo.corp_name }}</span>
</div>
<div class="info-row" v-if="isshownq">
<span class="info-label">上 级:</span>
<span class="info-value">{{ userInfo.report_to_info?.name || '' }}</span>
</div>
<div class="info-row" v-if="isshownq">
<span class="info-label">内 勤:</span>
<span class="info-value">{{ userInfo.office_clerk_info?.name || '' }}</span>
</div>
<!-- <div class="info-row">
<span class="info-label">国 家:</span>
<span class="info-value">{{ userInfo.country_txt }}</span>
@@ -218,54 +210,6 @@
</div>
</ElForm>
</div>
<div class="info box-style" style="margin-top: 20px" v-if="isshownq">
<h1 class="title"><span class="border" style="margin-right: 5px"></span>绑定信息</h1>
<ElForm
:model="formbd"
class="form"
ref="ruleFormbdRef"
:rules="rulesbd"
label-width="86px"
label-position="left"
>
<ElRow>
<ElFormItem label="直属上级" prop="report_to" required>
<ElSelect
v-model="formbd.report_to"
placeholder="请选择直属上级"
:disabled="!isEditbd"
>
<ElOption
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="所属内勤" prop="office_clerk_id" class="right-input" required>
<ElSelect
v-model="formbd.office_clerk_id"
placeholder="请选择所属内勤"
:disabled="!isEditbd"
>
<ElOption
v-for="item in usernqOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
</ElRow>
<div class="el-form-item-right">
<ElButton type="primary" style="width: 90px" v-ripple @click="editbd(ruleFormbdRef)">
<i v-if="!isEditbd" class="iconfont-sys"> &#xe6bf;</i>
{{ isEditbd ? '保存' : '编辑' }}
</ElButton>
</div>
</ElForm>
</div>
<div class="info box-style" style="margin-top: 20px">
<h1 class="title">更改密码</h1>
@@ -312,13 +256,12 @@
</template>
<script setup lang="ts">
import { ElForm, FormInstance, FormRules } from 'element-plus'
import { ElForm, FormInstance } from 'element-plus'
// import imageUrl from '@/assets/img/crm/upimg.png'
import { CrmService } from '@/api/crmApi'
import { UserService } from '@/api/usersApi'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { useUserStore } from '@/store/modules/user'
import { commonService } from '@/api/commonApi'
import { computed } from 'vue'
import QrcodeVue from 'qrcode.vue'
const { uploadHeaders } = useUploadHeaders()
@@ -334,8 +277,6 @@
// const socialize_pic_full = ref('')
// const countryOptions = ref<any>()
const form = ref<CustomerInfo>({})
const formbd = ref<CustomerInfo>({})
const isshownq = ref(false)
//获取公司 getCompanyList
// const companyList = ref<any>()
//国家
@@ -350,15 +291,11 @@
const userInfo = ref<CustomerInfo>({})
const getuserInfo = async () => {
const data = await UserService.getUserInfo()
isshownq.value = data.user.roles_id?.some((id: number) => [2, 4].includes(id)) || false
userInfo.value = data.user
form.value = data.user
imageUrlavatar.value = data.user.avatar
// socialize_pic_full.value = data.socialize_pic_full
sign_img_url.value = data.user.sign_img_url
formbd.value = data.user
formbd.value.office_clerk_id = data.user.office_clerk_info?.id || ''
formbd.value.report_to = data.user.report_to_info?.id || ''
// userInfo.value = getUserInfo.user
// form.value = getUserInfo.user
// imageUrlavatar.value = getUserInfo.user.avatar
@@ -368,7 +305,6 @@
const isEdit = ref(false)
const showReSignQR = ref(false)
const isEditPwd = ref(false)
const isEditbd = ref(false)
const pwdForm = reactive({
password: '',
password_new: '',
@@ -376,11 +312,6 @@
})
const ruleFormRef = ref<FormInstance>()
const ruleFormbdRef = ref<FormInstance>()
const rulesbd = reactive<FormRules>({
report_to: [{ required: true, message: '请选择直属上级', trigger: 'blur' }],
office_clerk_id: [{ required: true, message: '请选择所属内勤', trigger: 'blur' }]
})
const options = [
{
@@ -399,7 +330,6 @@
onMounted(async () => {
// await getarea()
await getuserInfo()
await getuserOptions()
})
const qrcodeUrl = computed(() => {
const userStore = useUserStore() as any
@@ -444,27 +374,6 @@
}
isEditPwd.value = !isEditPwd.value
}
//用户下拉
const userOptions = ref<any>()
const usernqOptions = ref<any>()
const getuserOptions = async () => {
const res = await commonService.getUserSelect()
const resparams = await commonService.getUserSelectparams({ role_id: '5' })
userOptions.value = res
usernqOptions.value = resparams
}
const editbd = async (formEl: FormInstance | undefined) => {
if (isEditbd.value) {
if (formbd.value.report_to == '' || formbd.value.office_clerk_id == '') {
ElMessage.error('直属上级或所属内勤不能为空')
return
}
const res = await CrmService.crmuseredit(formbd.value)
ElMessage.success(res?.message || '操作成功')
getuserInfo()
}
isEditbd.value = !isEditbd.value
}
//头像上传
function handleUploadavatar(response: any) {
// 假设返回图片地址字段为 response.url 或 response.data.url
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -10
View File
@@ -82,8 +82,8 @@
<span>{{ account.name }}</span>
</ElOption>
</ElSelect>
</ElFormItem>
<!-- <ElFormItem prop="company" label="系统">
</ElFormItem>
<ElFormItem prop="company" label="系统">
<ElSelect v-model="formData.systems" class="account-select" @change="changesystems">
<ElOption
v-for="account in systemsList"
@@ -94,7 +94,7 @@
<span>{{ account.name }}</span>
</ElOption>
</ElSelect>
</ElFormItem> -->
</ElFormItem>
<div class="drag-verify">
<div class="drag-verify-content" :class="{ error: !isPassing && isClickPass }">
<ArtDragVerify
@@ -151,7 +151,7 @@
</ElOption>
</ElSelect>
</ElFormItem>
<!-- <ElFormItem prop="company" label="系统">
<ElFormItem prop="company" label="系统">
<ElSelect v-model="formData.systems" class="account-select" @change="changesystems">
<ElOption
v-for="account in systemsList"
@@ -162,7 +162,7 @@
<span>{{ account.name }}</span>
</ElOption>
</ElSelect>
</ElFormItem> -->
</ElFormItem>
</ElForm>
<div class="qrcode-login" v-if="!isInFeishu">
<div id="login_container"></div>
@@ -256,7 +256,7 @@
const getfeishuAppList = async () => {
feishuresponse.value = await UserService.getfeishuAppList({
corp_id: useUserStore().companyid,
system_id: 1
system_id: useUserStore().systemsid
})
if (activeLoginType.value == 'qrcode') {
feishulogin()
@@ -278,11 +278,11 @@
companyid.value = company
iscompany.value = company
userStore.setCompany(company, companyt?.abbr)
if (activeLoginType.value == 'qrcode') {
if (companyid.value && systemsid.value) {
getfeishuAppList()
}
}
if (companyabbr != '' && activeLoginType.value == 'qrcode') {
if (companyabbr != '' && systemsabbr != '') {
iscompany.value = true
getfeishuAppList()
}
@@ -457,8 +457,7 @@
// formData.systems = Number(system_id)
// }
// 只有两个 id 都有值时才调用
userStore.setSystems('1', 'fdesk')
if (companyid && activeLoginType.value == 'qrcode') {
if (companyid && systemsid.value) {
getfeishuAppList()
}
})
+80 -169
View File
@@ -1,27 +1,48 @@
<template>
<div class="stocking-show-page">
<div class="boxshadow">
<div v-if="flow_type == 'stock_orders' || flow_type == 'stock_orders_nullify'">
<OrderDetailContent :orderData="orderDialog.orderData" />
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">合同</span>
</div>
<div v-if="flow_type == 'purchase_orders'">
<ProcurementDetailView :detailData="orderDialog.orderData" />
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<!-- 合同文件 -->
<div class="boxshadow">
<FilePreview title="合同文件" :files="contractFiles" empty-text="暂无合同文件" />
</div>
<!-- PI文件 -->
<div class="boxshadow">
<FilePreview title="PI文件" :files="piFiles" empty-text="暂无PI文件" />
</div>
<!-- 水单文件 -->
<div class="boxshadow">
<FilePreview title="水单文件" :files="waterBillFiles" empty-text="暂无水单文件" />
</div>
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
@@ -78,9 +99,6 @@
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
>同意</ElButton
>
<!-- <ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
>转交</ElButton
> -->
</div>
</div>
<!-- PDF 预览弹窗 -->
@@ -104,106 +122,37 @@
/>
</div>
</el-dialog>
<!-- 转交弹出框 -->
<el-dialog v-model="zjDialogVisible" title="转交" width="450px">
<el-form :model="zjForm" label-width="80px">
<el-form-item label="转交人" prop="to_uid">
<el-select
v-model="zjForm.to_uid"
filterable
placeholder="请选择转交人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<ElFormItem label="转交原因" prop="reason">
<ElInput v-model="zjForm.reason" type="textarea" :rows="3" placeholder="请输入转交原因" />
</ElFormItem>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="zjDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handlezjSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { commonService } from '@/api/commonApi'
import { manageService } from '@/api/adminApi'
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
// import { manageService } from '@/api/adminApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
import FilePreview from '@/components/custom/FilePreview.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
import { router } from '@/router'
defineOptions({ name: 'CrmApprovalDetail' })
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'CrmApprovalDetail' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { pdf, frameRef, download } = usePdfPreview()
const zjDialogVisible = ref(false)
const route = useRoute()
const approval_id = route.query.id
const stocking_id = route.query.biz_id
const instance_id = route.query.instance_id
const step_index = route.query.step_index
const status = route.query.status
const flow_type = route.query.flow_type
const approvalNotes = ref('')
const orderDialog = reactive({
orderData: {} as OrderData
})
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
const zjForm = reactive({
to_uid: '',
instance_id: '',
step_index: '',
reason: ''
})
// 转交操作
const handleTransfer = async () => {
getuserList()
zjDialogVisible.value = true
}
const handlezjSubmit = async () => {
if (!zjForm.to_uid) {
ElMessage.warning('请选择转交人')
return
}
if (!zjForm.reason) {
ElMessage.warning('请输入转交原因')
return
}
try {
const zjForms = {
to_uid: zjForm.to_uid,
instance_id: instance_id,
step_index: step_index,
reason: zjForm.reason
}
const { message } = await CrmService.stockapprovaltransfer(zjForms)
ElMessage.success(message)
router.push('/admin/approval/stocking_auth')
} catch (error) {
console.error('转交失败:', error)
// ElMessage.error('转交失败')
}
}
// const approval_id = route.query.id
const status = route.query.status
const stocking_id = route.query.biz_id
const instance_id = route.query.instance_id
const stockingInfo = ref<any>({})
const approvalNotes = ref('')
const approvalAction = ref('')
const handleSubmit = async (approvalAction: string) => {
try {
// 这里调用审批API
const { message } = await CrmService.stockapprovalpass({
instance_id: instance_id,
action: approvalAction,
@@ -224,71 +173,22 @@
}
// 合同文件
// 文件数据
const contractFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const piFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const waterBillFiles = ref<Array<{ name: string; url: string; file_name: string }>>([])
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
// 处理文件数据
const processFiles = (files: any[], type: string) => {
if (!files || !Array.isArray(files)) return []
return files.map((file: any) => ({
name: file.original_name,
url: file.file_full_url || file.file_url,
file_name: file.file_url
}))
}
function extractFileName(pathOrUrl: string | undefined | null): string {
if (!pathOrUrl) return '合同文件'
try {
const clean = String(pathOrUrl)
const afterSlash = clean.split(/[\\/]/).pop() || '合同文件'
return decodeURIComponent(afterSlash)
} catch {
return '合同文件'
}
}
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.stockapprovaldetailborder({
id: approval_id,
flow_type: flow_type
})
if (flow_type == 'stock_orders') {
const orderData: OrderData = res
orderDialog.orderData = orderData
} else if (flow_type == 'purchase_orders') {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
}
const res = await CrmService.checkStock({ id: stocking_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
stockingInfo.value = res
// 合同文件
// 处理合同文件
const file_list = res?.file_list
if (file_list?.contract) {
contractFiles.value = processFiles(file_list.contract, '合同')
} else {
contractFiles.value = []
}
// 处理PI文件
if (file_list?.pi_doc) {
piFiles.value = processFiles(file_list.pi_doc, 'PI')
} else {
piFiles.value = []
}
// 处理水单文件
if (file_list?.transaction_slip) {
waterBillFiles.value = processFiles(file_list.transaction_slip, '水单')
} else {
waterBillFiles.value = []
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
// 审批记录(兼容多种字段)
const resstockapprovallogs = await CrmService.stockapprovallogs({ instance_id: instance_id })
@@ -547,10 +447,8 @@
}
.submit-wrap {
display: flex;
justify-content: space-around;
justify-content: center;
padding: 24px 0 8px;
width: 500px;
margin: auto;
}
/* 审批记录 时间线 */
@@ -640,4 +538,17 @@
color: #999;
padding: 8px 0 4px 4px;
}
.submit-wrap {
display: flex;
justify-content: space-around;
padding: 24px 0 8px;
width: 500px;
margin: auto;
}
.submit-btn {
width: 100px;
height: 44px;
font-size: 16px;
}
</style>
+52 -213
View File
@@ -1,6 +1,9 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<!-- <div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div> -->
<div class="toolbar-right">
<el-input
v-model="keyword"
@@ -29,25 +32,14 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
<template #default="scope">
<div style="display: flex; align-items: center">
<img class="avatar" :src="scope.row.initiator_avatar" alt="" />
<p>{{ scope.row.initiator_name }}</p>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120" />
<ElTableColumn label="审核人" prop="approver_name" min-width="120" />
<ElTableColumn label="审批时间" prop="approved_at" min-width="120" />
<ElTableColumn label="状态" prop="status_desc" min-width="120" />
<ElTableColumn label="提交时间" prop="created_at" min-width="120" />
<ElTableColumn label="创建时间" prop="created_at" min-width="120" />
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
@@ -77,7 +69,7 @@
<!-- 快速审批弹出框 -->
<el-dialog
v-model="approvalDialogVisible"
:title="orderDialog.title"
title="合塑实业-汽车事业部-备货单(客户下定)"
width="80%"
:close-on-click-modal="false"
:close-on-press-escape="false"
@@ -85,25 +77,18 @@
>
<div class="approval-content">
<!-- 头部信息 -->
<div
v-if="orderDialog.type == 'stock_orders' || orderDialog.type == 'stock_orders_nullify'"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<div v-if="orderDialog.type == 'purchase_orders'">
<ProcurementDetailView :detailData="orderDialog.orderData" />
</div>
<OrderDetailContent :orderData="orderDialog.orderData" />
<!-- 审批备注 -->
<div class="info-section">
<ElRow :gutter="12">
<ElCol :span="24">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
</ElCol>
</ElRow>
</div>
@@ -111,107 +96,39 @@
<template #footer>
<div class="dialog-footer">
<ElButton
type="primary"
plain
size="large"
class="submit-btn"
@click="handleApprove('reject')"
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleApprove('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
>同意</ElButton
>
<!-- <ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
>转交</ElButton
> -->
</div>
</template>
</el-dialog>
<!-- 转交弹出框 -->
<el-dialog v-model="zjDialogVisible" title="转交" width="450px">
<el-form :model="zjForm" label-width="80px">
<el-form-item label="转交人" prop="to_uid">
<el-select
v-model="zjForm.to_uid"
filterable
placeholder="请选择转交人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<ElFormItem label="转交原因" prop="reason">
<ElInput v-model="zjForm.reason" type="textarea" :rows="3" placeholder="请输入转交原因" />
</ElFormItem>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="zjDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handlezjSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
import { manageService } from '@/api/adminApi'
// import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
defineOptions({ name: 'CrmStockingAuth' })
const zjDialogVisible = ref(false)
const orderDialog = reactive({
orderData: {} as OrderData,
title: '',
type: ''
})
defineOptions({ name: 'CrmStockingAuth' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { hasAuth } = useAuth()
const orderDialog = reactive({
orderData: {} as OrderData
})
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
const zjForm = reactive({
to_uid: '',
instance_id: '',
step_index: '',
reason: ''
})
// 审批弹出框相关
const approvalDialogVisible = ref(false)
@@ -257,68 +174,21 @@
const handleView = (row: any) => {
router.push({
path: '/crmpc/approval/auth_detil',
query: {
id: row.id,
biz_id: row.biz_id,
instance_id: row.instance_id,
status: row.status,
flow_type: row.flow_type,
step_index: row.step_index
} // 可带客户名等参数
query: { id: row.id, biz_id: row.biz_id, instance_id: row.instance_id, status: row.status } // 可带客户名等参数
})
}
const getDictItemSelectlist = ref<any[]>([])
const getDictItemSelect = async () => {
const res = await commonService.getDictItemSelect({ dict_id: '18' })
getDictItemSelectlist.value = res
}
const instance_id = ref('')
function extractFileName(pathOrUrl: string | undefined | null): string {
if (!pathOrUrl) return '合同文件'
try {
const clean = String(pathOrUrl)
const afterSlash = clean.split(/[\\/]/).pop() || '合同文件'
return decodeURIComponent(afterSlash)
} catch {
return '合同文件'
}
}
const activerow = ref()
const handleCreateRepair = async (row: any) => {
activerow.value = row
// getDictItemSelect()
instance_id.value = row.instance_id
const res = await CrmService.stockapprovaldetailborder({
id: row.id,
flow_type: row.flow_type
})
console.log(res)
switch (row.flow_type) {
case 'stock_orders': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
break
}
case 'stock_orders_nullify': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '备货单-作废'
break
}
case 'purchase_orders': {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-采购单(采购)'
break
}
}
const res = await CrmService.checkStock({ id: row.biz_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
approvalDialogVisible.value = true
}
@@ -343,46 +213,15 @@
ElMessage.error('审批失败')
}
}
// 转交操作
const handleTransfer = async () => {
zjForm.instance_id = activerow.value.instance_id
zjForm.step_index = activerow.value.step_index
getuserList()
zjDialogVisible.value = true
}
const handlezjSubmit = async () => {
if (!zjForm.to_uid) {
ElMessage.warning('请选择转交人')
return
}
if (!zjForm.reason) {
ElMessage.warning('请输入转交原因')
return
}
try {
const { message } = await CrmService.stockapprovaltransfer(zjForm)
ElMessage.success(message)
zjDialogVisible.value = false
approvalDialogVisible.value = false
approvalNotes.value = ''
zjForm.to_uid = ''
zjForm.reason = ''
instance_id.value = ''
// 刷新列表
getList()
} catch (error) {
console.error('转交失败:', error)
// ElMessage.error('转交失败')
}
}
</script>
<style scoped lang="scss">
.avatar {
width: 40px;
height: 40px;
border-radius: 6px;
margin-right: 10px;
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.stocking-page {
background: var(--art-main-bg-color);
@@ -414,22 +253,22 @@
.approval-content {
max-height: 70vh;
overflow-y: auto;
}
}
.header {
display: flex;
.header {
display: flex;
justify-content: center;
align-items: center;
align-items: center;
margin-bottom: 12px;
color: #333333;
.mr40 { margin-right: 40px; }
}
.info-section {
margin: 24px 0px;
margin-bottom: 24px;
.section-title {
display: flex;
display: flex;
align-items: center;
margin-bottom: 16px;
@@ -477,20 +316,20 @@
margin-bottom: 12px;
.detail-item {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
label {
margin-bottom: 6px;
font-size: 14px;
font-weight: 500;
font-weight: 500;
color: #606266;
}
}
}
}
}
}
}
.summary-section {
display: flex;
@@ -520,10 +359,10 @@
}
.dialog-footer {
display: flex;
justify-content: space-around;
width: 500px;
margin: auto;
display: flex;
gap: 12px;
.reject-btn {
@@ -32,13 +32,7 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120" />
@@ -89,12 +83,12 @@
<ElRow :gutter="12">
<ElCol :span="24">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
</ElCol>
</ElRow>
</div>
@@ -102,12 +96,7 @@
<template #footer>
<div class="dialog-footer">
<ElButton
type="primary"
plain
size="large"
class="submit-btn"
@click="handleApprove('reject')"
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleApprove('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
@@ -147,25 +136,7 @@
onMounted(() => {
getList()
})
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
@@ -203,14 +174,7 @@
const handleView = (row: any) => {
router.push({
path: '/crmpc/approval/auth_detil',
query: {
id: row.id,
biz_id: row.biz_id,
instance_id: row.instance_id,
status: row.status,
flow_type: row.flow_type,
step_index: row.step_index
}
query: { id: row.id, biz_id: row.biz_id, instance_id: row.instance_id, status: row.status } // 可带客户名等参数
})
}
const getDictItemSelectlist = ref<any[]>([])
+14 -48
View File
@@ -341,14 +341,7 @@
v-if="detailRow.desc_collect != '' && detailRow.desc_collect != 'null'"
v-html="detailRow.desc_collect"
class="detils-content"
@click="onDescImgClick"
></div>
<el-image-viewer
v-if="descViewerUrl"
:url-list="[descViewerUrl]"
hide-on-click-modal
@close="descViewerUrl = ''"
/>
</el-dialog>
<el-dialog v-model="reportDialogVisible" title="标记无效" width="600px">
<el-form :model="reportForm" label-width="80px">
@@ -637,41 +630,23 @@
})
}
const crmdelectfn = (row: any) => {
// ElMessageBox.confirm(
// '确定要删除此客户吗?删除后无法恢复,若存在疑问可标记为无效用户或操作移除,将客户移除到公海!',
// '提示',
// {
// confirmButtonText: '确定',
// cancelButtonText: '取消',
// type: 'warning'
// })
// .then(async () => {
// const res = await CrmService.crmCustomerDelete({ id: row.id, scene: 'force' })
// ElMessage.success(res?.message || '提交成功!')
// gettableList()
// // 你可以在这里调用接口或做后续处理
// })
// .catch(() => {
// // 用户点击了取消
// })
ElMessageBox.prompt(
'确定要删除此客户吗?删除后无法恢复,若存在疑问可标记为无效用户或操作移除,将客户移除到公海!删除请填写入删除原因!',
ElMessageBox.confirm(
'确定要删除此客户吗?删除后无法恢复,若存在疑问可标记为无效用户或操作移除,将客户移除到公海!',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^(?!\s*$).+/,
inputErrorMessage: '请填写入删除原因'
}
).then(async ({ value }) => {
const res = await CrmService.crmCustomerDelete({
id: row.id,
scene: 'force',
status_desc: value
})
ElMessage.success(res?.message || '提交成功!')
gettableList()
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const res = await CrmService.crmCustomerDelete({ id: row.id, scene: 'force' })
ElMessage.success(res?.message || '提交成功!')
gettableList()
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
// 标记无效
const reportfn = (row: any) => {
@@ -708,18 +683,9 @@
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.cursor = 'zoom-in'
})
return doc.body.innerHTML
}
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
const descViewerUrl = ref('')
function onDescImgClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.tagName === 'IMG') {
descViewerUrl.value = (target as HTMLImageElement).src
}
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row
+3 -23
View File
@@ -51,21 +51,13 @@
<el-table-column prop="created_at" label="创建时间"></el-table-column>
<el-table-column fixed="right" label="操作" width="160px">
<template #default="scope">
<div
class="follow-detli"
@click="onDetail(scope.row)"
v-if="hasAuth('highseas@show')"
>
<div class="follow-detli" @click="onDetail(scope.row)">
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
<i class="iconfont-sys">&#xe70f;</i>
</span>
详情
</div>
<div
class="follow-action"
@click="onFollow(scope.row)"
v-if="hasAuth('highseas@claim')"
>
<div class="follow-action" @click="onFollow(scope.row)">
<span class="custom-clock-icon" style="vertical-align: middle; margin-right: 4px">
<i class="iconfont-sys">&#xe841;</i>
</span>
@@ -78,8 +70,7 @@
</div>
</div>
<el-dialog v-model="detailDialogVisible" title="详情">
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content" @click="onDescImgClick"></div>
<el-image-viewer v-if="descViewerUrl" :url-list="[descViewerUrl]" hide-on-click-modal @close="descViewerUrl = ''" />
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
</el-dialog>
</template>
@@ -88,8 +79,6 @@
import dayjs from 'dayjs'
import { ElMessageBox } from 'element-plus'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'CrmHighseas' })
const loading = ref(false)
const pagination = reactive({
@@ -205,18 +194,9 @@
doc.querySelectorAll('img').forEach((img) => {
img.style.maxWidth = '100%'
img.style.height = 'auto'
img.style.cursor = 'zoom-in'
})
return doc.body.innerHTML
}
// 点击 desc_collect 中的图片 -> el-image-viewer 放大查看
const descViewerUrl = ref('')
function onDescImgClick(e: MouseEvent) {
const target = e.target as HTMLElement
if (target.tagName === 'IMG') {
descViewerUrl.value = (target as HTMLImageElement).src
}
}
function onDetail(row: any) {
row.desc_collect = processImages(row.desc_collect) // 处理图片
detailRow.value = row

Some files were not shown because too many files have changed in this diff Show More