From c77086ad2d7949ab36dd0c4e0b59907e672dd64f Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 18 九月 2026 15:04:42 +0800
Subject: [PATCH] Merge remote-tracking branch 'refs/remotes/origin/dev_pro2.0' into dev_宁夏_九泓化工
---
src/views/mes/dv/metering/components/select.vue | 142
src/views/srm/statistics/index.vue | 9
src/api/system/dept/index.ts | 13
src/router/routes/modules/crm.ts | 9
src/views/mes/pro/feedback/modules/form.vue | 124
src/views/crm/salesTarget/data.ts | 351
src/views/hrm/salary/structure/data.ts | 163
src/views/mes/pro/workorder/components/select.vue | 5
src/views/aftersales/problem/index.vue | 214
src/views/mes/dv/metering/index.vue | 195
src/views/erp/purchase/request/detail/index.vue | 181
src/packages/constants/src/biz-erp-enum.ts | 1
src/views/mes/md/workstation/workshop/modules/form.vue | 32
src/views/crm/contract/index.vue | 19
src/views/im/home/index.vue | 2
src/api/mes/md/workstation/worker/index.ts | 12
src/components/upload/file-upload.vue | 10
src/views/hrm/leave/modules/form.vue | 6
src/api/erp/purchase/order/index.ts | 32
src/views/bpm/businessAttach/data.ts | 163
src/views/crm/contract/detail/index.vue | 4
src/views/crm/receivable/index.vue | 51
src/views/srm/tender/modules/form.vue | 12
src/views/erp/purchase/invoice/index.vue | 194
src/views/crm/invoice/detail/data.ts | 134
src/views/mes/dv/meteringcheck/modules/form.vue | 99
src/views/hrm/salary/calculation/data.ts | 2
src/api/system/company-config/index.ts | 27
src/views/crm/saleQuotation/modules/ocr-upload.vue | 4
src/views/mes/pro/workorder/data.ts | 92
src/api/mes/pro/processdesign/processparam/index.ts | 52
src/views/hrm/employee/contract/modules/form.vue | 126
src/views/basicData/mdm/modules/import-form.vue | 105
src/views/wls/barcode/data.ts | 1
src/views/crm/invoice/detail/modules/info.vue | 49
src/api/mes/qc/template/item/index.ts | 28
src/views/hrm/salary/leave-type-config/index.vue | 43
src/views/wls/arrivalnotice/index.vue | 15
src/views/erp/finance/payment/index.vue | 78
src/views/mes/pro/feedback/data.ts | 204
src/views/mes/pro/workorder/index.vue | 175
src/api/mes/md/item/type/index.ts | 18
src/views/hrm/employee/modules/detail.vue | 11
src/api/mes/pro/mps/index.ts | 4
src/views/mes/md/item/components/product-bom-select.vue | 122
src/views/crm/invoice/modules/form.vue | 128
src/api/mes/wm/returnvendor/index.ts | 2
src/views/hrm/employee/contract/modules/terminate.vue | 103
FRONTEND_DEVELOPMENT.md | 63
src/views/crm/business/detail/index.vue | 4
src/views/hrm/overtime/modules/form.vue | 159
src/views/mes/qc/iqc/index.vue | 25
src/api/mes/pro/task/index.ts | 37
src/views/crm/saleQuotation/index.vue | 4
src/views/mes/dv/metering/modules/import-form.vue | 155
src/views/erp/sale/order/detail/index.vue | 154
src/views/crm/contract/components/data.ts | 12
src/views/srm/tender/detail.vue | 161
src/views/erp/purchase/order/data.ts | 23
src/views/hrm/attendance/exception/detail/approve/index.vue | 82
vite.config.ts | 39
src/views/wls/returnissue/modules/line-form.vue | 10
src/views/mes/qc/rqc/index.vue | 25
src/api/hrm/salary/calculation/index.ts | 2
src/views/erp/purchase/request/modules/form.vue | 9
src/api/mes/wm/productissue/line/index.ts | 26
src/api/aftersales/problem.ts | 103
src/views/srm/tender/data.ts | 13
src/views/wls/materialstock/components/index.ts | 1
src/api/system/post/index.ts | 13
src/api/hrm/shift/index.ts | 62
src/views/mes/dv/meteringcheck/index.vue | 158
src/api/mdm/item/index.ts | 18
public/logos/YZFXLogo.ico | 0
src/views/system/dept/index.vue | 19
src/views/mes/md/workstation/modules/form.vue | 34
src/views/mes/pro/mps/index.vue | 6
src/views/mes/pro/feedback/modules/batch-feedback.vue | 24
src/packages/constants/src/biz-mes-enum.ts | 30
src/views/crm/contract/detail/data.ts | 15
src/views/erp/purchase/order/detail/index.vue | 134
src/views/mes/pro/route/modules/process-form.vue | 2
src/main.ts | 8
src/views/mes/qc/oqc/data.ts | 37
src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue | 74
src/utils/index.ts | 15
src/views/mes/qc/oqc/index.vue | 25
src/packages/locales/src/langs/zh-CN/preferences.json | 19
src/views/dashboard/analytics/modules/alert-panel.vue | 20
src/views/mes/md/item/data.ts | 19
src/views/mes/md/item/components/select-dialog.vue | 64
src/views/mes/pro/workorder/modules/form.vue | 74
src/views/hrm/transfer/detail/approve/index.vue | 90
src/views/wls/itemreceipt/modules/an-select-dialog.vue | 8
src/components/upload/use-upload.ts | 10
src/utils/company-config.ts | 35
src/views/aftersales/ticket/detail.vue | 179
src/views/crm/permission/modules/transfer-form.vue | 6
src/packages/effects/layouts/src/widgets/index.ts | 1
src/api/mdm/unit/index.ts | 18
src/views/dashboard/analytics/modules/quick-actions.vue | 3
src/views/aftersales/problem/modules/form.vue | 139
src/views/hrm/employee/components/select.vue | 2
src/views/hrm/salary/leave-type-config/data.ts | 27
src/views/mes/dv/machinery/type/components/tree.vue | 11
src/packages/@core/base/icons/src/lucide.ts | 1
src/views/crm/invoice/index.vue | 259
src/api/hrm/salary/structure/index.ts | 50
src/views/mes/process-design/parameter/index.vue | 21
src/views/srm/supplierCertificate/data.ts | 20
src/views/hrm/employee/data.ts | 69
src/views/hrm/shift/index.vue | 162
src/api/mes/pro/workorder/process/index.ts | 1
src/api/mes/pd/project/index.ts | 19
src/packages/constants/src/biz-wms-enum.ts | 14
src/views/mes/qc/batchtrace/index.vue | 26
src/views/bpm/businessAttach/modules/form.vue | 88
src/views/mes/pd/archive/modules/detail.vue | 32
src/views/mes/md/workstation/index.vue | 42
src/views/mes/process-design/route/modules/product-form.vue | 107
src/views/mes/qc/components/export-range-modal.vue | 89
src/api/erp/finance/payment/index.ts | 18
src/views/mes/process-design/route/data.ts | 52
src/views/mes/pro/task/edit/index.vue | 5
src/preferences.ts | 5
src/views/crm/salesTarget/index.vue | 221
src/views/hrm/leave/detail/approve/index.vue | 93
src/views/hrm/overtime/detail/approve/index.vue | 83
src/views/system/user/data.ts | 1
internal/lint-configs/commitlint-config/index.mjs | 2
src/views/erp/purchase/invoice/data.ts | 250
src/views/wls/productissue/components/index.ts | 2
src/views/erp/purchase/order/modules/import-form.vue | 167
src/views/crm/invoice/detail/approve/index.vue | 105
WTXC.bat | 451 +
src/views/erp/purchase/invoice/modules/ocr-upload.vue | 186
src/api/hrm/attendance/record/index.ts | 1
src/api/mes/dv/metering/index.ts | 92
src/components/upload/image-upload.vue | 2
src/views/mes/pro/workorder/components/select-dialog.vue | 6
src/views/mes/pro/task/index.vue | 109
src/store/auth.ts | 4
src/packages/constants/src/dict-enum.ts | 12
src/views/hrm/salary/calculation/index.vue | 84
src/views/wls/stocktaking/plan/data.ts | 2
pnpm-workspace.yaml | 2
src/views/mes/pro/task/data.ts | 102
src/views/wls/warehouse/data.ts | 1
src/views/hrm/schedule/data.ts | 46
src/views/mes/dv/metering/modules/form.vue | 99
src/views/bpm/processInstance/detail/index.vue | 12
src/views/mes/dv/metering/components/index.ts | 2
src/components/form-create/helpers.ts | 9
src/views/hrm/salary/leave-type-config/modules/form.vue | 59
company.config.ts | 57
public/logos/YZFXLogo.png | 0
src/views/hrm/resignation/detail/approve/index.vue | 95
src/views/bpm/task/done/data.ts | 2
src/views/wls/returnissue/data.ts | 36
src/api/system/storage/index.ts | 11
src/views/basicData/mdm/index.vue | 74
src/views/mes/pro/workorder-center/modules/quick-feedback.vue | 60
src/views/mes/pro/workorder/modules/process-list.vue | 3
src/views/wls/warehouse/area/data.ts | 1
src/views/hrm/employee/contract/modules/import-form.vue | 165
src/views/erp/sale/order/modules/print.vue | 459 +
src/views/mes/workbench/components/tabs/TaskDetailTab.vue | 128
src/views/erp/finance/payment/modules/process-select-modal.vue | 76
internal/vite-config/src/utils/env.ts | 9
src/views/hrm/leave/data.ts | 15
src/views/hrm/shift/data.ts | 186
src/views/mes/pro/task/modules/task-list.vue | 30
src/api/hrm/employee/contract/index.ts | 156
src/api/bpm/autoApprove/index.ts | 59
src/api/erp/sale/order/index.ts | 41
src/views/hrm/attendance/record/data.ts | 1
src/api/mes/pd/document/index.ts | 2
src/views/srm/supplier/index.vue | 35
package.json | 5
src/api/crm/invoice/index.ts | 122
src/api/aftersales/ticket.ts | 8
src/types/app.d.ts | 1
src/api/mes/wm/materialstock/index.ts | 23
src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue | 70
src/views/mes/pd/project/modules/workflow-timeline.vue | 23
src/views/wls/arrivalnotice/modules/po-select-dialog.vue | 14
src/views/erp/finance/payment/data.ts | 84
src/views/hrm/employee/contract/index.vue | 183
src/api/crm/saleQuotation/ai/index.ts | 4
src/packages/locales/src/langs/en-US/preferences.json | 19
src/views/basicData/mdm/unit/data.ts | 24
src/api/hrm/overtime/index.ts | 89
src/views/hrm/overtime/data.ts | 320
src/views/mes/pd/project/detail/index.vue | 69
src/views/mes/dv/machinery/type/index.vue | 3
src/views/wls/warehouse/location/data.ts | 1
src/views/mes/pro/route/data.ts | 25
src/views/mes/qc/batchtrace/modules/trace-detail.vue | 51
src/views/mes/pd/project/modules/process-select-modal.vue | 75
src/api/bpm/businessAttach/index.ts | 52
src/api/hrm/leave-type-config/index.ts | 10
src/views/basicData/mdm/unit/index.vue | 34
src/views/wls/barcode/config/index.vue | 15
src/packages/effects/layouts/src/basic/layout.vue | 4
src/views/mes/pro/feedback/param-state.ts | 47
src/api/mes/dv/meteringcheck/index.ts | 60
src/views/crm/invoice/data.ts | 315
src/views/crm/statistics/performance/index.vue | 4
src/views/erp/purchase/order/index.vue | 55
src/views/erp/finance/payment/detail/approve/index.vue | 172
src/views/mes/dv/metering/data.ts | 437
src/views/mes/qc/template/data.ts | 5
src/views/srm/enums.ts | 19
src/views/bpm/model/modules/category-draggable-model.vue | 55
src/views/wls/materialstock/components/import-form.vue | 177
src/api/srm/supplierApply/index.ts | 53
src/api/crm/receivable/index.ts | 19
src/views/erp/purchase/request/modules/import-form.vue | 158
src/views/mes/md/item/modules/product-media-list.vue | 9
src/views/crm/receivable/detail/approve/index.vue | 112
src/views/mes/qc/batchtrace/modules/process-trace-list.vue | 66
src/views/basicData/mdm/data.ts | 37
src/api/system/database/index.ts | 50
src/views/hrm/overtime/index.vue | 200
src/views/wls/productreceipt/data.ts | 3
src/views/hrm/employee/contract/data.ts | 297
src/views/hrm/salary/structure/index.vue | 137
src/views/system/dept/modules/import-form.vue | 104
src/views/bpm/model/index.vue | 92
src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue | 11
src/views/mes/qc/iqc/data.ts | 28
src/api/erp/purchase/invoice/ai/index.ts | 33
src/views/crm/contract/data.ts | 12
src/views/crm/saleQuotation/modules/form.vue | 2
src/views/srm/supplier/data.ts | 24
src/views/erp/purchase/invoice/modules/purchase-order-select.vue | 194
src/views/system/post/data.ts | 24
src/views/crm/receivable/components/data.ts | 10
src/views/mes/pro/feedback/index.vue | 30
src/views/erp/purchase/invoice/detail/approve/index.vue | 90
src/views/erp/purchase/request/index.vue | 35
src/views/srm/supplierApply/detail/index.vue | 96
src/views/erp/purchase/request/data.ts | 4
src/views/mes/process-design/parameter/modules/process-link-dialog.vue | 70
src/store/company-config.ts | 71
src/views/hrm/shift/modules/form.vue | 96
src/api/mes/pro/feedback/index.ts | 43
src/views/crm/receivable/detail/data.ts | 8
src/views/hrm/attendance/record/modules/import-form.vue | 12
src/api/hrm/leave/index.ts | 20
src/api/srm/tender/index.ts | 9
src/views/crm/salesTarget/modules/form.vue | 94
src/views/erp/sale/order/modules/item-form.vue | 4
src/views/erp/finance/payment/modules/form.vue | 38
src/views/srm/supplierCertificate/modules/form.vue | 11
src/views/wls/returnvendor/modules/detail-form.vue | 10
src/views/basicData/mdm/modules/form.vue | 2
src/api/crm/permission/index.ts | 1
src/views/wls/materialstock/index.vue | 29
src/views/crm/receivable/plan/data.ts | 3
src/views/mes/workbench/types.ts | 1
src/views/mes/workbench/components/TaskListPanel.vue | 135
src/views/mes/md/item/type/modules/import-form.vue | 105
src/views/mes/qc/ipqc/index.vue | 25
src/views/mes/md/item/components/product-bom-select-dialog.vue | 182
src/api/mes/pro/workorder/index.ts | 24
src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts | 1
src/views/basicData/mdm/unit/modules/import-form.vue | 105
src/views/mes/md/item/type/data.ts | 24
src/views/mes/process-design/process/modules/param-manage.vue | 193
src/api/crm/salesTarget/index.ts | 113
src/views/crm/permission/modules/list.vue | 11
src/views/mes/qc/rqc/data.ts | 37
src/api/mes/pro/workorderparamrecord/index.ts | 1
src/views/bpm/task/done/index.vue | 35
src/views/mes/dv/meteringcheck/data.ts | 218
src/router/routes/modules/mes.ts | 10
src/views/hrm/employee/modules/contract-list.vue | 107
src/views/srm/supplier/modules/import-form.vue | 100
src/api/mes/wm/batch/index.ts | 81
src/views/mes/qc/batchtrace/modules/purchase-source-list.vue | 59
ZSJC.bat | 451 +
src/views/mes/qc/ipqc/data.ts | 43
src/views/mes/pd/project/index.vue | 75
src/views/wls/materialstock/components/stock-adjust-modal.vue | 7
src/views/wls/returnvendor/modules/form.vue | 17
src/views/aftersales/ticket/index.vue | 16
src/views/crm/receivable/plan/modules/form.vue | 15
src/views/crm/salesTarget/analysis/chart-options.ts | 130
pnpm-lock.yaml | 66
src/api/hrm/schedule/index.ts | 43
src/views/srm/supplierApply/modules/form.vue | 24
src/api/srm/supplierCertificate/index.ts | 3
src/views/aftersales/problem/data.ts | 252
src/views/system/dept/data.ts | 24
src/views/mes/dv/metering/components/select-dialog.vue | 213
src/views/mes/pro/task/modules/schedule-form.vue | 52
src/views/crm/clue/detail/index.vue | 4
src/views/mes/process-design/route/modules/form.vue | 146
src/views/wls/productissue/components/select-dialog.vue | 106
src/views/erp/finance/payment/modules/item-form.vue | 185
src/packages/effects/layouts/src/widgets/database-backup/index.ts | 1
src/views/erp/sale/order/modules/import-form.vue | 155
src/views/mes/qc/batchtrace/data.ts | 175
src/views/system/post/index.vue | 19
src/views/erp/purchase/invoice/modules/form.vue | 139
src/api/aftersales/visit.ts | 62
src/views/mes/pro/workorder/modules/process-form.vue | 11
src/views/system/post/modules/import-form.vue | 82
src/views/mes/md/item/modules/form.vue | 14
src/views/crm/backlog/modules/receivable-audit-list.vue | 31
src/views/crm/invoice/detail/index.vue | 128
src/views/crm/salesTarget/analysis/data.ts | 166
src/views/mes/pd/project/modules/document-audit-form.vue | 12
src/views/wls/productissue/data.ts | 45
src/packages/effects/layouts/src/basic/header/header.vue | 10
src/views/mes/workbench/components/tabs/ProductIssueLineFormModal.vue | 46
src/views/erp/sale/order/index.vue | 97
src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue | 151
src/views/hrm/employee/index.vue | 80
src/views/wls/returnissue/modules/line-list.vue | 5
src/views/hrm/leave/index.vue | 50
src/api/mes/wm/returnissue/line/index.ts | 1
.env.production | 4
src/views/bpm/model/data.ts | 6
src/views/mes/qc/batchtrace/modules/sales-target-list.vue | 59
src/views/wls/stocktaking/plan/modules/param-form.vue | 2
YZFX.bat | 451 +
src/api/hrm/employee/index.ts | 35
src/views/crm/statistics/performance/chartOptions.ts | 12
src/api/mes/pro/route/process/index.ts | 1
src/router/guard.ts | 5
src/views/crm/receivable/data.ts | 62
src/views/wls/returnissue/modules/form.vue | 2
src/views/crm/customer/detail/index.vue | 6
src/views/wls/productissue/components/select.vue | 141
src/views/crm/contract/modules/import-form.vue | 155
src/views/hrm/salary/structure/modules/form.vue | 95
src/api/bpm/model/index.ts | 2
src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue | 25
JHHG.bat | 451 +
src/api/erp/purchase/request/index.ts | 59
src/views/mes/pro/task/modules/task-form.vue | 41
src/views/srm/supplierApply/data.ts | 3
src/views/mes/workbench/index.vue | 34
src/views/crm/contact/detail/index.vue | 4
src/views/hrm/schedule/index.vue | 743 +
src/views/mes/pro/feedback/detail/index.vue | 104
src/api/crm/contract/index.ts | 22
src/views/hrm/employee/modules/import-form.vue | 161
.idea/mom-pro2-before.iml | 2
src/views/srm/supplierApply/modules/process-select-modal.vue | 74
commitlint.config.js | 2
src/bootstrap.ts | 17
src/views/erp/sale/order/data.ts | 27
src/api/system/user/index.ts | 7
src/store/index.ts | 1
deploy.bat | 523
src/views/srm/supplierCertificate/index.vue | 17
src/api/srm/supplier/index.ts | 23
/dev/null | 110
src/views/mes/process-design/process/index.vue | 21
src/views/crm/contract/detail/approve/index.vue | 190
src/views/mes/md/item/index.vue | 39
src/views/mes/md/item/type/index.vue | 54
src/views/mes/process-design/route/modules/workbench.vue | 545 +
src/views/mes/workbench/components/tabs/FeedbackTab.vue | 186
src/api/mes/md/item/index.ts | 7
src/api/erp/purchase/invoice/index.ts | 100
src/layouts/auth.vue | 22
src/views/crm/salesTarget/analysis/index.vue | 146
src/views/dashboard/analytics/index.vue | 1
src/views/wls/returnvendor/data.ts | 7
src/views/srm/supplierApply/index.vue | 188
src/views/bpm/businessAttach/index.vue | 129
src/views/crm/receivable/modules/form.vue | 20
src/views/mes/process-design/route/modules/process-form.vue | 2
377 files changed, 24,471 insertions(+), 1,776 deletions(-)
diff --git a/.env.production b/.env.production
index f7d87ea..5abb0d7 100644
--- a/.env.production
+++ b/.env.production
@@ -3,9 +3,9 @@
VITE_BASE=/
# 璇锋眰璺緞
-VITE_BASE_URL=http://36.213.180.217:9500
+VITE_BASE_URL=http://36.213.90.123:9501
# 鎺ュ彛鍦板潃
-VITE_GLOB_API_URL=http://36.213.180.217:9500/admin-api
+VITE_GLOB_API_URL=http://36.213.90.123:9501/admin-api
# 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔S3鏈嶅姟
VITE_UPLOAD_TYPE=server
diff --git a/.idea/mom-pro2-before.iml b/.idea/mom-pro2-before.iml
index 179693c..f93e5a6 100644
--- a/.idea/mom-pro2-before.iml
+++ b/.idea/mom-pro2-before.iml
@@ -7,6 +7,8 @@
<excludeFolder url="file://$MODULE_DIR$/.cache" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/build" />
+ <excludeFolder url="file://$MODULE_DIR$/.agents" />
+ <excludeFolder url="file://$MODULE_DIR$/.codex" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
diff --git a/FRONTEND_DEVELOPMENT.md b/FRONTEND_DEVELOPMENT.md
index 367ad5a..114299f 100644
--- a/FRONTEND_DEVELOPMENT.md
+++ b/FRONTEND_DEVELOPMENT.md
@@ -1020,7 +1020,68 @@
---
-## 鍗併�佺浉鍏宠祫婧�
+## 鍗併�丟it 鎻愪氦瑙勮寖
+
+### 10.1 鎻愪氦鏍煎紡
+
+```
+<type>: <subject>
+
+<body>
+```
+
+- **type** 鍜� **subject** 涔嬮棿蹇呴』鏈変竴涓┖鏍硷紙`: `锛�
+- **subject** 蹇呭~锛岀畝鐭弿杩板彉鏇村唴瀹�
+- **body** 鍙�夛紝涓虹┖鏃惰烦杩囧嵆鍙�
+
+### 10.2 鏀寔鐨� Type
+
+| type | 璇存槑 |
+| ---------- | ------------------ |
+| `feat` | 鏂板姛鑳� |
+| `fix` | 淇 Bug |
+| `docs` | 鏂囨。淇敼 |
+| `style` | 浠g爜鏍煎紡锛堜笉褰卞搷閫昏緫锛� |
+| `refactor` | 浠g爜閲嶆瀯 |
+| `perf` | 鎬ц兘浼樺寲 |
+| `test` | 娣诲姞娴嬭瘯浠g爜 |
+| `build` | 渚濊禆/鏋勫缓淇敼 |
+| `ci` | CI/CD 闆嗘垚 |
+| `chore` | 鏃ュ父浜嬪姟 |
+| `revert` | 鐗堟湰鍥為�� |
+| `types` | 绫诲瀷瀹氫箟淇敼 |
+
+### 10.3 绀轰緥
+
+```bash
+# 鏈�绠�鍗曪紙鍙湁涓婚锛�
+feat: 娣诲姞鐗╂枡绫诲瀷绠$悊椤甸潰
+
+# 甯﹁缁嗚鏄�
+fix: 淇瀹℃壒娴佺▼鏁版嵁涓㈠け
+
+鍘熷洜鏄悗绔帴鍙h繑鍥炴牸寮忓彉鍖栵紝璋冩暣浜嗘暟鎹В鏋愰�昏緫銆�
+
+# 甯﹀簭鍙峰垪琛�
+refactor: 閲嶆瀯璇锋眰鎷︽埅鍣�
+
+1. 鎻愬彇鍏叡閿欒澶勭悊閫昏緫
+2. 缁熶竴 Token 鍒锋柊鏈哄埗
+```
+
+### 10.4 鑷姩鏍¢獙
+
+椤圭洰宸查厤缃� commitlint锛屾瘡娆℃彁浜ゆ椂浼氳嚜鍔ㄦ牎楠屾牸寮忋�傚鏋滄牸寮忎笉绗﹀悎瑙勮寖锛孏it 浼氭嫆缁濇彁浜ゅ苟鎻愮ず閿欒銆�
+
+涔熷彲浠ヤ娇鐢ㄤ氦浜掑紡鎻愪氦锛�
+
+```bash
+pnpm commit
+```
+
+---
+
+## 鍗佷竴銆佺浉鍏宠祫婧�
- [Vue 3 鏂囨。](https://vuejs.org/)
- [Ant Design Vue 鏂囨。](https://antdv.com/)
diff --git a/JHHG.bat b/JHHG.bat
new file mode 100644
index 0000000..fd558ce
--- /dev/null
+++ b/JHHG.bat
@@ -0,0 +1,451 @@
+@echo off
+setlocal EnableExtensions EnableDelayedExpansion
+
+REM ============================================================
+REM Vue Frontend Deploy Script
+REM ============================================================
+
+REM =========================
+REM Configuration
+REM =========================
+
+set "COMPANY=JHHG"
+
+set "REMOTE_USER=root"
+set "REMOTE_HOST=42.63.70.90"
+set "REMOTE_PORT=22"
+
+set "PASSWORD=Nxjhhg!@##@!"
+
+set "ZIP_FILE=dist.zip"
+set "REMOTE_DIR=/home/vue/dist"
+
+set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
+
+set "RETRY_COUNT=3"
+
+set "SSHPASS_DIR=%~dp0bin"
+
+
+REM ============================================================
+REM Start
+REM ============================================================
+
+echo.
+echo ============================================================
+echo Vue Dist Auto Deploy
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_USER%@%REMOTE_HOST%
+echo Port : %REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo.
+
+
+REM ============================================================
+REM Check sshpass
+REM ============================================================
+
+echo [1/7] Checking sshpass...
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+)
+
+where sshpass >nul 2>&1
+
+if not errorlevel 1 (
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [WARN] sshpass not found.
+
+if not exist "%SSHPASS_DIR%" (
+ mkdir "%SSHPASS_DIR%" >nul 2>&1
+)
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [INFO] Downloading sshpass...
+
+powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to download sshpass.
+ echo [ERROR] Please install sshpass manually.
+ goto ERROR_EXIT
+)
+
+set "PATH=%SSHPASS_DIR%;%PATH%"
+
+echo [OK] sshpass installed.
+
+
+REM ============================================================
+REM Check pnpm
+REM ============================================================
+
+:CHECK_PNPM
+
+echo.
+echo [2/7] Checking pnpm...
+
+where pnpm >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] pnpm not found.
+ echo [HINT] npm install -g pnpm
+ goto ERROR_EXIT
+)
+
+call pnpm --version
+
+if errorlevel 1 (
+ echo [ERROR] pnpm execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node
+REM ============================================================
+
+echo.
+echo [3/7] Checking Node.js...
+
+where node >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] Node.js not found.
+ goto ERROR_EXIT
+)
+
+call node --version
+
+if errorlevel 1 (
+ echo [ERROR] Node.js execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node_modules
+REM ============================================================
+
+if not exist "node_modules" (
+ echo.
+ echo [INFO] node_modules not found.
+ echo [INFO] Running pnpm install...
+
+ call pnpm install
+
+ if errorlevel 1 (
+ echo [ERROR] pnpm install failed.
+ goto ERROR_EXIT
+ )
+
+ echo [OK] pnpm install completed.
+) else (
+ echo [OK] node_modules exists.
+)
+
+
+REM ============================================================
+REM Build
+REM ============================================================
+
+echo.
+echo [4/7] Building project...
+
+if exist "%ZIP_FILE%" (
+ echo.
+ echo [WARN] %ZIP_FILE% already exists.
+ set "REBUILD="
+ set /p "REBUILD=Rebuild? Y/N (default Y): "
+
+ if /I "!REBUILD!"=="N" (
+ echo [INFO] Skip build.
+ goto CHECK_ZIP
+ )
+)
+
+echo [INFO] Company: %COMPANY%
+echo [INFO] Command: pnpm run build
+echo.
+
+set "VITE_COMPANY=%COMPANY%"
+
+call pnpm run build
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Build failed.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Build completed.
+
+
+REM ============================================================
+REM Check ZIP
+REM ============================================================
+
+:CHECK_ZIP
+
+echo.
+echo [5/7] Checking package...
+
+if not exist "%ZIP_FILE%" (
+ echo [ERROR] %ZIP_FILE% not found.
+ echo [HINT] Please check whether your build generates dist.zip.
+ goto ERROR_EXIT
+)
+
+for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
+
+set /a ZIP_MB=ZIP_BYTES/1024/1024
+
+echo [OK] Package found.
+echo [INFO] File size: !ZIP_BYTES! bytes
+echo [INFO] File size: !ZIP_MB! MB
+
+
+REM ============================================================
+REM Set sshpass password
+REM ============================================================
+
+set "SSHPASS=%PASSWORD%"
+
+
+REM ============================================================
+REM Test SSH
+REM ============================================================
+
+echo.
+echo [6/7] Testing SSH connection...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
+
+if errorlevel 1 (
+ echo [ERROR] SSH connection failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] SSH connection successful.
+
+
+REM ============================================================
+REM Create remote directory
+REM ============================================================
+
+echo.
+echo [INFO] Creating remote directory...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to create remote directory.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote directory ready.
+
+
+REM ============================================================
+REM Upload temporary file
+REM ============================================================
+
+echo.
+echo [INFO] Uploading package...
+
+set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
+
+sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
+
+if errorlevel 1 (
+ echo [ERROR] Upload failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] Upload completed.
+
+
+REM ============================================================
+REM Check remote file
+REM ============================================================
+
+echo.
+echo [INFO] Checking remote package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
+
+if errorlevel 1 (
+ echo [ERROR] Remote package does not exist.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote package exists.
+
+
+REM ============================================================
+REM Compare file size
+REM ============================================================
+
+echo.
+echo [INFO] Checking package size...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
+
+echo.
+
+
+REM ============================================================
+REM Rename temporary file
+REM ============================================================
+
+echo [INFO] Activating package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to activate package.
+ goto ERROR_EXIT
+)
+
+echo [OK] Package activated.
+
+
+REM ============================================================
+REM Check unzip
+REM ============================================================
+
+echo.
+echo [INFO] Checking unzip...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
+
+if errorlevel 1 (
+ echo [ERROR] unzip is not installed on server.
+ echo.
+ echo CentOS/RHEL:
+ echo yum install -y unzip
+ echo.
+ echo Ubuntu/Debian:
+ echo apt install -y unzip
+ goto ERROR_EXIT
+)
+
+echo [OK] unzip found.
+
+
+REM ============================================================
+REM Test ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Testing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] ZIP test failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo [OK] ZIP test passed.
+
+
+REM ============================================================
+REM Extract
+REM ============================================================
+
+echo.
+echo [INFO] Extracting package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Remote unzip failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Extract completed.
+
+
+REM ============================================================
+REM Remove ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Removing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [WARN] Failed to remove remote ZIP.
+) else (
+ echo [OK] Remote ZIP removed.
+)
+
+
+REM ============================================================
+REM Success
+REM ============================================================
+
+echo.
+echo ============================================================
+echo DEPLOY SUCCESS
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_HOST%:%REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo Size : !ZIP_MB! MB
+echo.
+echo Deployment completed successfully.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM Error
+REM ============================================================
+
+:ERROR_EXIT
+
+echo.
+echo ============================================================
+echo DEPLOY FAILED
+echo ============================================================
+echo.
+echo Please check the error message above.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM End
+REM ============================================================
+
+:END
+
+pause
+endlocal
\ No newline at end of file
diff --git a/WTXC.bat b/WTXC.bat
new file mode 100644
index 0000000..215373b
--- /dev/null
+++ b/WTXC.bat
@@ -0,0 +1,451 @@
+@echo off
+setlocal EnableExtensions EnableDelayedExpansion
+
+REM ============================================================
+REM Vue Frontend Deploy Script
+REM ============================================================
+
+REM =========================
+REM Configuration
+REM =========================
+
+set "COMPANY=WTXC"
+
+set "REMOTE_USER=root"
+set "REMOTE_HOST=42.63.71.140"
+set "REMOTE_PORT=22"
+
+set "PASSWORD=Nxwtxc123."
+
+set "ZIP_FILE=dist.zip"
+set "REMOTE_DIR=/home/vue/dist"
+
+set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
+
+set "RETRY_COUNT=3"
+
+set "SSHPASS_DIR=%~dp0bin"
+
+
+REM ============================================================
+REM Start
+REM ============================================================
+
+echo.
+echo ============================================================
+echo Vue Dist Auto Deploy
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_USER%@%REMOTE_HOST%
+echo Port : %REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo.
+
+
+REM ============================================================
+REM Check sshpass
+REM ============================================================
+
+echo [1/7] Checking sshpass...
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+)
+
+where sshpass >nul 2>&1
+
+if not errorlevel 1 (
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [WARN] sshpass not found.
+
+if not exist "%SSHPASS_DIR%" (
+ mkdir "%SSHPASS_DIR%" >nul 2>&1
+)
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [INFO] Downloading sshpass...
+
+powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to download sshpass.
+ echo [ERROR] Please install sshpass manually.
+ goto ERROR_EXIT
+)
+
+set "PATH=%SSHPASS_DIR%;%PATH%"
+
+echo [OK] sshpass installed.
+
+
+REM ============================================================
+REM Check pnpm
+REM ============================================================
+
+:CHECK_PNPM
+
+echo.
+echo [2/7] Checking pnpm...
+
+where pnpm >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] pnpm not found.
+ echo [HINT] npm install -g pnpm
+ goto ERROR_EXIT
+)
+
+call pnpm --version
+
+if errorlevel 1 (
+ echo [ERROR] pnpm execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node
+REM ============================================================
+
+echo.
+echo [3/7] Checking Node.js...
+
+where node >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] Node.js not found.
+ goto ERROR_EXIT
+)
+
+call node --version
+
+if errorlevel 1 (
+ echo [ERROR] Node.js execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node_modules
+REM ============================================================
+
+if not exist "node_modules" (
+ echo.
+ echo [INFO] node_modules not found.
+ echo [INFO] Running pnpm install...
+
+ call pnpm install
+
+ if errorlevel 1 (
+ echo [ERROR] pnpm install failed.
+ goto ERROR_EXIT
+ )
+
+ echo [OK] pnpm install completed.
+) else (
+ echo [OK] node_modules exists.
+)
+
+
+REM ============================================================
+REM Build
+REM ============================================================
+
+echo.
+echo [4/7] Building project...
+
+if exist "%ZIP_FILE%" (
+ echo.
+ echo [WARN] %ZIP_FILE% already exists.
+ set "REBUILD="
+ set /p "REBUILD=Rebuild? Y/N (default Y): "
+
+ if /I "!REBUILD!"=="N" (
+ echo [INFO] Skip build.
+ goto CHECK_ZIP
+ )
+)
+
+echo [INFO] Company: %COMPANY%
+echo [INFO] Command: pnpm run build
+echo.
+
+set "VITE_COMPANY=%COMPANY%"
+
+call pnpm run build
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Build failed.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Build completed.
+
+
+REM ============================================================
+REM Check ZIP
+REM ============================================================
+
+:CHECK_ZIP
+
+echo.
+echo [5/7] Checking package...
+
+if not exist "%ZIP_FILE%" (
+ echo [ERROR] %ZIP_FILE% not found.
+ echo [HINT] Please check whether your build generates dist.zip.
+ goto ERROR_EXIT
+)
+
+for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
+
+set /a ZIP_MB=ZIP_BYTES/1024/1024
+
+echo [OK] Package found.
+echo [INFO] File size: !ZIP_BYTES! bytes
+echo [INFO] File size: !ZIP_MB! MB
+
+
+REM ============================================================
+REM Set sshpass password
+REM ============================================================
+
+set "SSHPASS=%PASSWORD%"
+
+
+REM ============================================================
+REM Test SSH
+REM ============================================================
+
+echo.
+echo [6/7] Testing SSH connection...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
+
+if errorlevel 1 (
+ echo [ERROR] SSH connection failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] SSH connection successful.
+
+
+REM ============================================================
+REM Create remote directory
+REM ============================================================
+
+echo.
+echo [INFO] Creating remote directory...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to create remote directory.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote directory ready.
+
+
+REM ============================================================
+REM Upload temporary file
+REM ============================================================
+
+echo.
+echo [INFO] Uploading package...
+
+set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
+
+sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
+
+if errorlevel 1 (
+ echo [ERROR] Upload failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] Upload completed.
+
+
+REM ============================================================
+REM Check remote file
+REM ============================================================
+
+echo.
+echo [INFO] Checking remote package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
+
+if errorlevel 1 (
+ echo [ERROR] Remote package does not exist.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote package exists.
+
+
+REM ============================================================
+REM Compare file size
+REM ============================================================
+
+echo.
+echo [INFO] Checking package size...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
+
+echo.
+
+
+REM ============================================================
+REM Rename temporary file
+REM ============================================================
+
+echo [INFO] Activating package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to activate package.
+ goto ERROR_EXIT
+)
+
+echo [OK] Package activated.
+
+
+REM ============================================================
+REM Check unzip
+REM ============================================================
+
+echo.
+echo [INFO] Checking unzip...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
+
+if errorlevel 1 (
+ echo [ERROR] unzip is not installed on server.
+ echo.
+ echo CentOS/RHEL:
+ echo yum install -y unzip
+ echo.
+ echo Ubuntu/Debian:
+ echo apt install -y unzip
+ goto ERROR_EXIT
+)
+
+echo [OK] unzip found.
+
+
+REM ============================================================
+REM Test ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Testing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] ZIP test failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo [OK] ZIP test passed.
+
+
+REM ============================================================
+REM Extract
+REM ============================================================
+
+echo.
+echo [INFO] Extracting package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Remote unzip failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Extract completed.
+
+
+REM ============================================================
+REM Remove ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Removing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [WARN] Failed to remove remote ZIP.
+) else (
+ echo [OK] Remote ZIP removed.
+)
+
+
+REM ============================================================
+REM Success
+REM ============================================================
+
+echo.
+echo ============================================================
+echo DEPLOY SUCCESS
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_HOST%:%REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo Size : !ZIP_MB! MB
+echo.
+echo Deployment completed successfully.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM Error
+REM ============================================================
+
+:ERROR_EXIT
+
+echo.
+echo ============================================================
+echo DEPLOY FAILED
+echo ============================================================
+echo.
+echo Please check the error message above.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM End
+REM ============================================================
+
+:END
+
+pause
+endlocal
\ No newline at end of file
diff --git a/YZFX.bat b/YZFX.bat
new file mode 100644
index 0000000..d4b3432
--- /dev/null
+++ b/YZFX.bat
@@ -0,0 +1,451 @@
+@echo off
+setlocal EnableExtensions EnableDelayedExpansion
+
+REM ============================================================
+REM Vue Frontend Deploy Script
+REM ============================================================
+
+REM =========================
+REM Configuration
+REM =========================
+
+set "COMPANY=YZFX"
+
+set "REMOTE_USER=root"
+set "REMOTE_HOST=42.63.70.231"
+set "REMOTE_PORT=22"
+
+set "PASSWORD=Nxyzfxxcl1@"
+
+set "ZIP_FILE=dist.zip"
+set "REMOTE_DIR=/home/vue/dist"
+
+set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
+
+set "RETRY_COUNT=3"
+
+set "SSHPASS_DIR=%~dp0bin"
+
+
+REM ============================================================
+REM Start
+REM ============================================================
+
+echo.
+echo ============================================================
+echo Vue Dist Auto Deploy
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_USER%@%REMOTE_HOST%
+echo Port : %REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo.
+
+
+REM ============================================================
+REM Check sshpass
+REM ============================================================
+
+echo [1/7] Checking sshpass...
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+)
+
+where sshpass >nul 2>&1
+
+if not errorlevel 1 (
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [WARN] sshpass not found.
+
+if not exist "%SSHPASS_DIR%" (
+ mkdir "%SSHPASS_DIR%" >nul 2>&1
+)
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [INFO] Downloading sshpass...
+
+powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to download sshpass.
+ echo [ERROR] Please install sshpass manually.
+ goto ERROR_EXIT
+)
+
+set "PATH=%SSHPASS_DIR%;%PATH%"
+
+echo [OK] sshpass installed.
+
+
+REM ============================================================
+REM Check pnpm
+REM ============================================================
+
+:CHECK_PNPM
+
+echo.
+echo [2/7] Checking pnpm...
+
+where pnpm >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] pnpm not found.
+ echo [HINT] npm install -g pnpm
+ goto ERROR_EXIT
+)
+
+call pnpm --version
+
+if errorlevel 1 (
+ echo [ERROR] pnpm execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node
+REM ============================================================
+
+echo.
+echo [3/7] Checking Node.js...
+
+where node >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] Node.js not found.
+ goto ERROR_EXIT
+)
+
+call node --version
+
+if errorlevel 1 (
+ echo [ERROR] Node.js execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node_modules
+REM ============================================================
+
+if not exist "node_modules" (
+ echo.
+ echo [INFO] node_modules not found.
+ echo [INFO] Running pnpm install...
+
+ call pnpm install
+
+ if errorlevel 1 (
+ echo [ERROR] pnpm install failed.
+ goto ERROR_EXIT
+ )
+
+ echo [OK] pnpm install completed.
+) else (
+ echo [OK] node_modules exists.
+)
+
+
+REM ============================================================
+REM Build
+REM ============================================================
+
+echo.
+echo [4/7] Building project...
+
+if exist "%ZIP_FILE%" (
+ echo.
+ echo [WARN] %ZIP_FILE% already exists.
+ set "REBUILD="
+ set /p "REBUILD=Rebuild? Y/N (default Y): "
+
+ if /I "!REBUILD!"=="N" (
+ echo [INFO] Skip build.
+ goto CHECK_ZIP
+ )
+)
+
+echo [INFO] Company: %COMPANY%
+echo [INFO] Command: pnpm run build
+echo.
+
+set "VITE_COMPANY=%COMPANY%"
+
+call pnpm run build
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Build failed.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Build completed.
+
+
+REM ============================================================
+REM Check ZIP
+REM ============================================================
+
+:CHECK_ZIP
+
+echo.
+echo [5/7] Checking package...
+
+if not exist "%ZIP_FILE%" (
+ echo [ERROR] %ZIP_FILE% not found.
+ echo [HINT] Please check whether your build generates dist.zip.
+ goto ERROR_EXIT
+)
+
+for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
+
+set /a ZIP_MB=ZIP_BYTES/1024/1024
+
+echo [OK] Package found.
+echo [INFO] File size: !ZIP_BYTES! bytes
+echo [INFO] File size: !ZIP_MB! MB
+
+
+REM ============================================================
+REM Set sshpass password
+REM ============================================================
+
+set "SSHPASS=%PASSWORD%"
+
+
+REM ============================================================
+REM Test SSH
+REM ============================================================
+
+echo.
+echo [6/7] Testing SSH connection...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
+
+if errorlevel 1 (
+ echo [ERROR] SSH connection failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] SSH connection successful.
+
+
+REM ============================================================
+REM Create remote directory
+REM ============================================================
+
+echo.
+echo [INFO] Creating remote directory...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to create remote directory.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote directory ready.
+
+
+REM ============================================================
+REM Upload temporary file
+REM ============================================================
+
+echo.
+echo [INFO] Uploading package...
+
+set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
+
+sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
+
+if errorlevel 1 (
+ echo [ERROR] Upload failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] Upload completed.
+
+
+REM ============================================================
+REM Check remote file
+REM ============================================================
+
+echo.
+echo [INFO] Checking remote package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
+
+if errorlevel 1 (
+ echo [ERROR] Remote package does not exist.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote package exists.
+
+
+REM ============================================================
+REM Compare file size
+REM ============================================================
+
+echo.
+echo [INFO] Checking package size...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
+
+echo.
+
+
+REM ============================================================
+REM Rename temporary file
+REM ============================================================
+
+echo [INFO] Activating package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to activate package.
+ goto ERROR_EXIT
+)
+
+echo [OK] Package activated.
+
+
+REM ============================================================
+REM Check unzip
+REM ============================================================
+
+echo.
+echo [INFO] Checking unzip...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
+
+if errorlevel 1 (
+ echo [ERROR] unzip is not installed on server.
+ echo.
+ echo CentOS/RHEL:
+ echo yum install -y unzip
+ echo.
+ echo Ubuntu/Debian:
+ echo apt install -y unzip
+ goto ERROR_EXIT
+)
+
+echo [OK] unzip found.
+
+
+REM ============================================================
+REM Test ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Testing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] ZIP test failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo [OK] ZIP test passed.
+
+
+REM ============================================================
+REM Extract
+REM ============================================================
+
+echo.
+echo [INFO] Extracting package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Remote unzip failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Extract completed.
+
+
+REM ============================================================
+REM Remove ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Removing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [WARN] Failed to remove remote ZIP.
+) else (
+ echo [OK] Remote ZIP removed.
+)
+
+
+REM ============================================================
+REM Success
+REM ============================================================
+
+echo.
+echo ============================================================
+echo DEPLOY SUCCESS
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_HOST%:%REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo Size : !ZIP_MB! MB
+echo.
+echo Deployment completed successfully.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM Error
+REM ============================================================
+
+:ERROR_EXIT
+
+echo.
+echo ============================================================
+echo DEPLOY FAILED
+echo ============================================================
+echo.
+echo Please check the error message above.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM End
+REM ============================================================
+
+:END
+
+pause
+endlocal
\ No newline at end of file
diff --git a/ZSJC.bat b/ZSJC.bat
new file mode 100644
index 0000000..355c255
--- /dev/null
+++ b/ZSJC.bat
@@ -0,0 +1,451 @@
+@echo off
+setlocal EnableExtensions EnableDelayedExpansion
+
+REM ============================================================
+REM Vue Frontend Deploy Script
+REM ============================================================
+
+REM =========================
+REM Configuration
+REM =========================
+
+set "COMPANY=ZSJC"
+
+set "REMOTE_USER=root"
+set "REMOTE_HOST=116.172.73.239"
+set "REMOTE_PORT=22"
+
+set "PASSWORD=ecs-QGY8ecs-QGY8"
+
+set "ZIP_FILE=dist.zip"
+set "REMOTE_DIR=/home/vue/dist"
+
+set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
+
+set "RETRY_COUNT=3"
+
+set "SSHPASS_DIR=%~dp0bin"
+
+
+REM ============================================================
+REM Start
+REM ============================================================
+
+echo.
+echo ============================================================
+echo Vue Dist Auto Deploy
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_USER%@%REMOTE_HOST%
+echo Port : %REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo.
+
+
+REM ============================================================
+REM Check sshpass
+REM ============================================================
+
+echo [1/7] Checking sshpass...
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+)
+
+where sshpass >nul 2>&1
+
+if not errorlevel 1 (
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [WARN] sshpass not found.
+
+if not exist "%SSHPASS_DIR%" (
+ mkdir "%SSHPASS_DIR%" >nul 2>&1
+)
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
+)
+
+echo [INFO] Downloading sshpass...
+
+powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to download sshpass.
+ echo [ERROR] Please install sshpass manually.
+ goto ERROR_EXIT
+)
+
+set "PATH=%SSHPASS_DIR%;%PATH%"
+
+echo [OK] sshpass installed.
+
+
+REM ============================================================
+REM Check pnpm
+REM ============================================================
+
+:CHECK_PNPM
+
+echo.
+echo [2/7] Checking pnpm...
+
+where pnpm >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] pnpm not found.
+ echo [HINT] npm install -g pnpm
+ goto ERROR_EXIT
+)
+
+call pnpm --version
+
+if errorlevel 1 (
+ echo [ERROR] pnpm execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node
+REM ============================================================
+
+echo.
+echo [3/7] Checking Node.js...
+
+where node >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] Node.js not found.
+ goto ERROR_EXIT
+)
+
+call node --version
+
+if errorlevel 1 (
+ echo [ERROR] Node.js execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node_modules
+REM ============================================================
+
+if not exist "node_modules" (
+ echo.
+ echo [INFO] node_modules not found.
+ echo [INFO] Running pnpm install...
+
+ call pnpm install
+
+ if errorlevel 1 (
+ echo [ERROR] pnpm install failed.
+ goto ERROR_EXIT
+ )
+
+ echo [OK] pnpm install completed.
+) else (
+ echo [OK] node_modules exists.
+)
+
+
+REM ============================================================
+REM Build
+REM ============================================================
+
+echo.
+echo [4/7] Building project...
+
+if exist "%ZIP_FILE%" (
+ echo.
+ echo [WARN] %ZIP_FILE% already exists.
+ set "REBUILD="
+ set /p "REBUILD=Rebuild? Y/N (default Y): "
+
+ if /I "!REBUILD!"=="N" (
+ echo [INFO] Skip build.
+ goto CHECK_ZIP
+ )
+)
+
+echo [INFO] Company: %COMPANY%
+echo [INFO] Command: pnpm run build
+echo.
+
+set "VITE_COMPANY=%COMPANY%"
+
+call pnpm run build
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Build failed.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Build completed.
+
+
+REM ============================================================
+REM Check ZIP
+REM ============================================================
+
+:CHECK_ZIP
+
+echo.
+echo [5/7] Checking package...
+
+if not exist "%ZIP_FILE%" (
+ echo [ERROR] %ZIP_FILE% not found.
+ echo [HINT] Please check whether your build generates dist.zip.
+ goto ERROR_EXIT
+)
+
+for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
+
+set /a ZIP_MB=ZIP_BYTES/1024/1024
+
+echo [OK] Package found.
+echo [INFO] File size: !ZIP_BYTES! bytes
+echo [INFO] File size: !ZIP_MB! MB
+
+
+REM ============================================================
+REM Set sshpass password
+REM ============================================================
+
+set "SSHPASS=%PASSWORD%"
+
+
+REM ============================================================
+REM Test SSH
+REM ============================================================
+
+echo.
+echo [6/7] Testing SSH connection...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
+
+if errorlevel 1 (
+ echo [ERROR] SSH connection failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] SSH connection successful.
+
+
+REM ============================================================
+REM Create remote directory
+REM ============================================================
+
+echo.
+echo [INFO] Creating remote directory...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to create remote directory.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote directory ready.
+
+
+REM ============================================================
+REM Upload temporary file
+REM ============================================================
+
+echo.
+echo [INFO] Uploading package...
+
+set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
+
+sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
+
+if errorlevel 1 (
+ echo [ERROR] Upload failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] Upload completed.
+
+
+REM ============================================================
+REM Check remote file
+REM ============================================================
+
+echo.
+echo [INFO] Checking remote package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
+
+if errorlevel 1 (
+ echo [ERROR] Remote package does not exist.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote package exists.
+
+
+REM ============================================================
+REM Compare file size
+REM ============================================================
+
+echo.
+echo [INFO] Checking package size...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
+
+echo.
+
+
+REM ============================================================
+REM Rename temporary file
+REM ============================================================
+
+echo [INFO] Activating package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to activate package.
+ goto ERROR_EXIT
+)
+
+echo [OK] Package activated.
+
+
+REM ============================================================
+REM Check unzip
+REM ============================================================
+
+echo.
+echo [INFO] Checking unzip...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
+
+if errorlevel 1 (
+ echo [ERROR] unzip is not installed on server.
+ echo.
+ echo CentOS/RHEL:
+ echo yum install -y unzip
+ echo.
+ echo Ubuntu/Debian:
+ echo apt install -y unzip
+ goto ERROR_EXIT
+)
+
+echo [OK] unzip found.
+
+
+REM ============================================================
+REM Test ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Testing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] ZIP test failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo [OK] ZIP test passed.
+
+
+REM ============================================================
+REM Extract
+REM ============================================================
+
+echo.
+echo [INFO] Extracting package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Remote unzip failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Extract completed.
+
+
+REM ============================================================
+REM Remove ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Removing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [WARN] Failed to remove remote ZIP.
+) else (
+ echo [OK] Remote ZIP removed.
+)
+
+
+REM ============================================================
+REM Success
+REM ============================================================
+
+echo.
+echo ============================================================
+echo DEPLOY SUCCESS
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_HOST%:%REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo Size : !ZIP_MB! MB
+echo.
+echo Deployment completed successfully.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM Error
+REM ============================================================
+
+:ERROR_EXIT
+
+echo.
+echo ============================================================
+echo DEPLOY FAILED
+echo ============================================================
+echo.
+echo Please check the error message above.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM End
+REM ============================================================
+
+:END
+
+pause
+endlocal
\ No newline at end of file
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 0000000..92db1e9
--- /dev/null
+++ b/commitlint.config.js
@@ -0,0 +1,2 @@
+// 寮曠敤鍐呴儴 commitlint 閰嶇疆
+export { default } from './internal/lint-configs/commitlint-config/index.mjs';
diff --git a/company.config.ts b/company.config.ts
index 0ab5170..6f0227f 100644
--- a/company.config.ts
+++ b/company.config.ts
@@ -5,6 +5,8 @@
logo: string;
/** 缃戦〉鏍囬鍥炬爣璺緞锛岀浉瀵逛簬 public 鐩綍 */
favicon: string;
+ /** API 鎺ュ彛鍦板潃 */
+ apiUrl: string;
}
/**
@@ -15,5 +17,58 @@
* 绀轰緥锛歴et "COMPANY=TEST"
*/
export const companies: Record<string, CompanyConfig> = {
- JHHG: { name: '涔濇硴鍖栧伐', logo: '/logos/TEST.png', favicon: '/logos/TEST.ico' },
+ TEST: {
+ name: '娴嬭瘯鍏徃',
+ logo: '/logos/TEST.png',
+ favicon: '/logos/TEST.ico',
+ apiUrl: 'http://36.213.90.123:9501/admin-api',
+ },
+ JHHG: {
+ name: '瀹佸涔濇硴鍖栧伐',
+ logo: '/logos/TEST.png',
+ favicon: '/logos/TEST.ico',
+ apiUrl: 'http://42.63.70.90:9001/admin-api',
+ },
+ YZFX: {
+ name: '瀹佸鑻辨辰闃查攬',
+ logo: '/logos/YZFXLogo.png',
+ favicon: '/logos/YZFXLogo.ico',
+ apiUrl: 'http://42.63.70.231:9001/admin-api',
+ },
+ WTXC: {
+ name: '瀹佸涓囬�氭柊鏉�',
+ logo: '/logos/YZFXLogo.png',
+ favicon: '/logos/YZFXLogo.ico',
+ apiUrl: 'http://42.63.71.140:9001/admin-api',
+ },
+ YSJX: {
+ name: '瀹佸鐜夊北鏈烘',
+ logo: '/logos/YZFXLogo.png',
+ favicon: '/logos/YZFXLogo.ico',
+ apiUrl: 'http://42.63.71.140:9004/admin-api',
+ },
+ XLZB: {
+ name: '瀹佸鍗忓姏瑁呭',
+ logo: '/logos/YZFXLogo.png',
+ favicon: '/logos/YZFXLogo.ico',
+ apiUrl: 'http://42.63.71.140:9004/admin-api',
+ },
+ ZSJC: {
+ name: '瀹佸涓洓寤烘潗绉戞妧鏈夐檺鍏徃',
+ logo: '/logos/YZFXLogo.png',
+ favicon: '/logos/YZFXLogo.ico',
+ apiUrl: 'http://116.172.73.239:9001/admin-api',
+ },
+ KYZN: {
+ name: '搴锋剰鏅鸿兘',
+ logo: '/logos/YZFXLogo.png',
+ favicon: '/logos/YZFXLogo.ico',
+ apiUrl: 'http://36.213.150.237:9004/admin-api',
+ },
+ CHDL: {
+ name: '闀胯緣鐢靛姏',
+ logo: '/logos/YZFXLogo.png',
+ favicon: '/logos/YZFXLogo.ico',
+ apiUrl: 'http://36.213.150.237:9006/admin-api',
+ },
};
diff --git a/deploy.bat b/deploy.bat
index afc5eac..f07c556 100644
--- a/deploy.bat
+++ b/deploy.bat
@@ -1,170 +1,451 @@
@echo off
-chcp 65001 >nul
-setlocal enabledelayedexpansion
+setlocal EnableExtensions EnableDelayedExpansion
-:: ================= Config =================
-set "COMPANY=JHHG"
-set "USER=root"
-set "PASS=Xdrjkj@10086#"
-set "IP=36.213.180.217"
-set "PORT=65022"
-set "DIST_DIR=dist"
-set "REMOTE_DIR=/data/project/mom/vue/test"
-set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10"
-:: ===========================================
+REM ============================================================
+REM Vue Frontend Deploy Script
+REM ============================================================
-echo ==========================================
-echo Vue Dist Auto Deploy Script
-echo ==========================================
+REM =========================
+REM Configuration
+REM =========================
+
+set "COMPANY=TEST"
+
+set "REMOTE_USER=root"
+set "REMOTE_HOST=36.213.90.123"
+set "REMOTE_PORT=65022"
+
+set "PASSWORD=Xdrjkj@10086#"
+
+set "ZIP_FILE=dist.zip"
+set "REMOTE_DIR=/data/project/mom/vue/test/dist"
+
+set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
+
+set "RETRY_COUNT=3"
+
+set "SSHPASS_DIR=%~dp0bin"
+
+
+REM ============================================================
+REM Start
+REM ============================================================
+
+echo.
+echo ============================================================
+echo Vue Dist Auto Deploy
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_USER%@%REMOTE_HOST%
+echo Port : %REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
echo.
-:: Check if sshpass is available
-set "SSHPASS_DIR=%~dp0bin"
-if exist "!SSHPASS_DIR!\sshpass.exe" (
- set "PATH=!SSHPASS_DIR!;%PATH%"
+
+REM ============================================================
+REM Check sshpass
+REM ============================================================
+
+echo [1/7] Checking sshpass...
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
)
where sshpass >nul 2>&1
-if %errorlevel% equ 0 goto :check_node
-echo [WARN] sshpass not found, trying to install...
-echo.
-
-where bash >nul 2>&1
-if %errorlevel% equ 0 (
- echo [INFO] Git Bash detected, trying pacman install...
- bash -c "pacman -S --noconfirm sshpass" 2>nul
- if !errorlevel! equ 0 (
- echo [OK] sshpass installed via pacman!
- goto :check_node
- )
+if not errorlevel 1 (
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
)
-if not exist "!SSHPASS_DIR!" mkdir "!SSHPASS_DIR!"
+echo [WARN] sshpass not found.
-if exist "!SSHPASS_DIR!\sshpass.exe" (
- set "PATH=!SSHPASS_DIR!;%PATH%"
- echo [OK] sshpass found at: !SSHPASS_DIR!
- goto :check_node
+if not exist "%SSHPASS_DIR%" (
+ mkdir "%SSHPASS_DIR%" >nul 2>&1
+)
+
+if exist "%SSHPASS_DIR%\sshpass.exe" (
+ set "PATH=%SSHPASS_DIR%;%PATH%"
+ echo [OK] sshpass found.
+ goto CHECK_PNPM
)
echo [INFO] Downloading sshpass...
-powershell -NoProfile -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='!SSHPASS_DIR!\sshpass.exe'; Write-Host \"Downloading to: $out\"; try { Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing; exit 0 } catch { Write-Host $_.Exception.Message; exit 1 }"
-if !errorlevel! neq 0 (
- echo [ERROR] Failed to download sshpass!
- echo [HINT] Please install sshpass manually or use Git Bash: pacman -S sshpass
- goto :error_exit
+
+powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to download sshpass.
+ echo [ERROR] Please install sshpass manually.
+ goto ERROR_EXIT
)
-set "PATH=!SSHPASS_DIR!;%PATH%"
-echo [OK] sshpass downloaded to: !SSHPASS_DIR!
-echo.
+set "PATH=%SSHPASS_DIR%;%PATH%"
-:check_node
-:: Check if pnpm is available
+echo [OK] sshpass installed.
+
+
+REM ============================================================
+REM Check pnpm
+REM ============================================================
+
+:CHECK_PNPM
+
+echo.
+echo [2/7] Checking pnpm...
+
where pnpm >nul 2>&1
-if %errorlevel% neq 0 (
- echo [ERROR] pnpm not found! Please install pnpm first.
+
+if errorlevel 1 (
+ echo [ERROR] pnpm not found.
echo [HINT] npm install -g pnpm
- goto :error_exit
+ goto ERROR_EXIT
)
-:: Check if node_modules exists
+call pnpm --version
+
+if errorlevel 1 (
+ echo [ERROR] pnpm execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node
+REM ============================================================
+
+echo.
+echo [3/7] Checking Node.js...
+
+where node >nul 2>&1
+
+if errorlevel 1 (
+ echo [ERROR] Node.js not found.
+ goto ERROR_EXIT
+)
+
+call node --version
+
+if errorlevel 1 (
+ echo [ERROR] Node.js execution failed.
+ goto ERROR_EXIT
+)
+
+
+REM ============================================================
+REM Check node_modules
+REM ============================================================
+
if not exist "node_modules" (
- echo [INFO] node_modules not found, running pnpm install...
echo.
+ echo [INFO] node_modules not found.
+ echo [INFO] Running pnpm install...
+
call pnpm install
- if !errorlevel! neq 0 (
- echo [ERROR] pnpm install failed!
- goto :error_exit
+
+ if errorlevel 1 (
+ echo [ERROR] pnpm install failed.
+ goto ERROR_EXIT
)
+
+ echo [OK] pnpm install completed.
+) else (
+ echo [OK] node_modules exists.
+)
+
+
+REM ============================================================
+REM Build
+REM ============================================================
+
+echo.
+echo [4/7] Building project...
+
+if exist "%ZIP_FILE%" (
echo.
-)
+ echo [WARN] %ZIP_FILE% already exists.
+ set "REBUILD="
+ set /p "REBUILD=Rebuild? Y/N (default Y): "
-:: Check if dist exists and ask user
-if exist "%DIST_DIR%" (
- echo [WARN] dist directory already exists.
- set /p "REBUILD=Do you want to rebuild? (Y/N, default Y): "
- if /i "!REBUILD!"=="N" (
- echo [INFO] Skip build, using existing dist
- goto :upload
+ if /I "!REBUILD!"=="N" (
+ echo [INFO] Skip build.
+ goto CHECK_ZIP
)
)
-:: Build the project
-echo [STEP 1/3] Building project for %COMPANY%...
-echo [INFO] Running: pnpm run build
-echo.
-set "COMPANY=!COMPANY!" && call pnpm run build
-if !errorlevel! neq 0 (
- echo [ERROR] Build failed! Check your project configuration.
- goto :error_exit
-)
-echo [OK] Build completed!
+echo [INFO] Company: %COMPANY%
+echo [INFO] Command: pnpm run build
echo.
-:upload
-if not exist "%DIST_DIR%" (
- echo [ERROR] Dist directory not found: %DIST_DIR%
- echo [HINT] Build may have failed or output directory is incorrect.
- goto :error_exit
+set "VITE_COMPANY=%COMPANY%"
+
+call pnpm run build
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Build failed.
+ goto ERROR_EXIT
)
-:: Count files and calculate size
-set "FILE_COUNT=0"
-set "TOTAL_SIZE=0"
-for /r "%DIST_DIR%" %%F in (*) do (
- set /a "FILE_COUNT+=1"
- set /a "TOTAL_SIZE+=%%~zF"
-)
-set /a "TOTAL_SIZE_MB=TOTAL_SIZE/1024/1024"
-
-echo [INFO] Dist directory: %DIST_DIR%
-echo [INFO] Files count: %FILE_COUNT%
-echo [INFO] Total size: %TOTAL_SIZE_MB% MB
-echo [INFO] Target: %USER%@%IP%:%PORT%
-echo [INFO] Remote dir: %REMOTE_DIR%
echo.
+echo [OK] Build completed.
-:: Optional: Ask for confirmation before upload
-set /p "CONFIRM=Proceed with upload? (Y/N, default Y): "
-if /i "!CONFIRM!"=="N" (
- echo [INFO] Upload cancelled by user.
- goto :end
+
+REM ============================================================
+REM Check ZIP
+REM ============================================================
+
+:CHECK_ZIP
+
+echo.
+echo [5/7] Checking package...
+
+if not exist "%ZIP_FILE%" (
+ echo [ERROR] %ZIP_FILE% not found.
+ echo [HINT] Please check whether your build generates dist.zip.
+ goto ERROR_EXIT
)
-:: Upload dist directory
-echo [STEP 2/3] Uploading dist directory...
-sshpass -p "%PASS%" scp -P %PORT% %SSH_OPTS% -r "%DIST_DIR%" %USER%@%IP%:%REMOTE_DIR%/
-if %errorlevel% neq 0 (
- echo [ERROR] Upload failed! Check network or credentials.
- goto :error_exit
+for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
+
+set /a ZIP_MB=ZIP_BYTES/1024/1024
+
+echo [OK] Package found.
+echo [INFO] File size: !ZIP_BYTES! bytes
+echo [INFO] File size: !ZIP_MB! MB
+
+
+REM ============================================================
+REM Set sshpass password
+REM ============================================================
+
+set "SSHPASS=%PASSWORD%"
+
+
+REM ============================================================
+REM Test SSH
+REM ============================================================
+
+echo.
+echo [6/7] Testing SSH connection...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
+
+if errorlevel 1 (
+ echo [ERROR] SSH connection failed.
+ goto ERROR_EXIT
)
-echo [OK] Upload completed!
-echo.
-:: Optional: Remote restart/cleanup (uncomment if needed)
-:: echo [STEP 3/3] Restarting remote service...
-:: sshpass -p "%PASS%" ssh -p %PORT% %SSH_OPTS% %USER%@%IP% "cd %REMOTE_DIR% && npm run restart"
+echo [OK] SSH connection successful.
+
+
+REM ============================================================
+REM Create remote directory
+REM ============================================================
echo.
-echo ==========================================
-echo [SUCCESS] Deploy completed!
-echo ==========================================
+echo [INFO] Creating remote directory...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to create remote directory.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote directory ready.
+
+
+REM ============================================================
+REM Upload temporary file
+REM ============================================================
+
echo.
-echo Summary:
-echo - Build: Completed
-echo - Upload: %FILE_COUNT% files (%TOTAL_SIZE_MB% MB)
-echo - Target: %USER%@%IP%:%PORT%%REMOTE_DIR%
+echo [INFO] Uploading package...
+
+set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
+
+sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
+
+if errorlevel 1 (
+ echo [ERROR] Upload failed.
+ goto ERROR_EXIT
+)
+
+echo [OK] Upload completed.
+
+
+REM ============================================================
+REM Check remote file
+REM ============================================================
+
+echo.
+echo [INFO] Checking remote package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
+
+if errorlevel 1 (
+ echo [ERROR] Remote package does not exist.
+ goto ERROR_EXIT
+)
+
+echo [OK] Remote package exists.
+
+
+REM ============================================================
+REM Compare file size
+REM ============================================================
+
+echo.
+echo [INFO] Checking package size...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
+
echo.
-goto :end
-:error_exit
+REM ============================================================
+REM Rename temporary file
+REM ============================================================
+
+echo [INFO] Activating package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [ERROR] Failed to activate package.
+ goto ERROR_EXIT
+)
+
+echo [OK] Package activated.
+
+
+REM ============================================================
+REM Check unzip
+REM ============================================================
+
echo.
-echo ==========================================
-echo [FAILED] Deploy failed!
-echo ==========================================
+echo [INFO] Checking unzip...
-:end
-pause
\ No newline at end of file
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
+
+if errorlevel 1 (
+ echo [ERROR] unzip is not installed on server.
+ echo.
+ echo CentOS/RHEL:
+ echo yum install -y unzip
+ echo.
+ echo Ubuntu/Debian:
+ echo apt install -y unzip
+ goto ERROR_EXIT
+)
+
+echo [OK] unzip found.
+
+
+REM ============================================================
+REM Test ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Testing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] ZIP test failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo [OK] ZIP test passed.
+
+
+REM ============================================================
+REM Extract
+REM ============================================================
+
+echo.
+echo [INFO] Extracting package...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo.
+ echo [ERROR] Remote unzip failed.
+ echo [INFO] Remote ZIP has been kept.
+ goto ERROR_EXIT
+)
+
+echo.
+echo [OK] Extract completed.
+
+
+REM ============================================================
+REM Remove ZIP
+REM ============================================================
+
+echo.
+echo [INFO] Removing remote ZIP...
+
+sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
+
+if errorlevel 1 (
+ echo [WARN] Failed to remove remote ZIP.
+) else (
+ echo [OK] Remote ZIP removed.
+)
+
+
+REM ============================================================
+REM Success
+REM ============================================================
+
+echo.
+echo ============================================================
+echo DEPLOY SUCCESS
+echo ============================================================
+echo.
+echo Company : %COMPANY%
+echo Server : %REMOTE_HOST%:%REMOTE_PORT%
+echo Remote Dir : %REMOTE_DIR%
+echo Package : %ZIP_FILE%
+echo Size : !ZIP_MB! MB
+echo.
+echo Deployment completed successfully.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM Error
+REM ============================================================
+
+:ERROR_EXIT
+
+echo.
+echo ============================================================
+echo DEPLOY FAILED
+echo ============================================================
+echo.
+echo Please check the error message above.
+echo.
+
+goto END
+
+
+REM ============================================================
+REM End
+REM ============================================================
+
+:END
+
+pause
+endlocal
\ No newline at end of file
diff --git a/internal/lint-configs/commitlint-config/index.mjs b/internal/lint-configs/commitlint-config/index.mjs
index 36c3b09..64aaf04 100644
--- a/internal/lint-configs/commitlint-config/index.mjs
+++ b/internal/lint-configs/commitlint-config/index.mjs
@@ -90,7 +90,7 @@
* ^^^^^^^^^^^^^^ empty line.
* - Something here
*/
- 'body-leading-blank': [2, 'always'],
+ 'body-leading-blank': [0],
/**
* type[scope]: [function] description
*
diff --git a/internal/vite-config/src/utils/env.ts b/internal/vite-config/src/utils/env.ts
index f342216..d14019b 100644
--- a/internal/vite-config/src/utils/env.ts
+++ b/internal/vite-config/src/utils/env.ts
@@ -38,7 +38,7 @@
match = 'VITE_GLOB_',
confFiles = getConfFiles(),
) {
- let envConfig = {};
+ let envConfig: Record<string, string> = {};
for (const confFile of confFiles) {
try {
@@ -60,6 +60,13 @@
Reflect.deleteProperty(envConfig, key);
}
});
+ // 鍚堝苟杩涚▼鐜鍙橀噺锛屼娇 vite.config.ts 鎸夊叕鍙告敞鍏ョ殑 VITE_GLOB_API_URL 鑳借繘鍏� _app-config 鏂囦欢
+ Object.keys(process.env).forEach((key) => {
+ const value = process.env[key];
+ if (reg.test(key) && value) {
+ envConfig[key] = value;
+ }
+ });
return envConfig as T;
}
diff --git a/package.json b/package.json
index bbc683e..f8cce41 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
+ "commit": "czg",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vue-tsc --noEmit --skipLibCheck --incremental --tsBuildInfoFile node_modules/.cache/vue-tsc/tsconfig.tsbuildinfo"
@@ -80,11 +81,13 @@
"@vben/tailwind-config": "workspace:*",
"@vben/tsconfig": "workspace:*",
"@vben/vite-config": "workspace:*",
+ "cross-env": "catalog:",
"jiti": "^2.7.0",
"tsdown": "^0.22.3",
"tsx": "^4.22.4",
"typescript": "catalog:",
- "vite": "catalog:"
+ "vite": "catalog:",
+ "vue-tsc": "catalog:"
},
"packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b02d913..81f3c62 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -231,6 +231,9 @@
cropperjs:
specifier: ^1.6.2
version: 1.6.2
+ cross-env:
+ specifier: ^10.1.0
+ version: 10.1.0
crypto-js:
specifier: ^4.2.0
version: 4.2.0
@@ -504,6 +507,9 @@
vue-tippy:
specifier: ^6.7.1
version: 6.7.1
+ vue-tsc:
+ specifier: ^3.3.9
+ version: 3.3.9
vue3-print-nb:
specifier: ^0.1.4
version: 0.1.4
@@ -693,12 +699,15 @@
'@vben/vite-config':
specifier: workspace:*
version: link:internal/vite-config
+ cross-env:
+ specifier: 'catalog:'
+ version: 10.1.0
jiti:
specifier: ^2.7.0
version: 2.7.0
tsdown:
specifier: ^0.22.3
- version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
+ version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3))
tsx:
specifier: ^4.22.4
version: 4.22.4
@@ -708,6 +717,9 @@
vite:
specifier: 'catalog:'
version: 8.0.10(@types/node@25.9.4)(esbuild@0.28.1)(jiti@2.7.0)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)
+ vue-tsc:
+ specifier: 'catalog:'
+ version: 3.3.9(typescript@6.0.3)
internal/lint-configs/commitlint-config:
dependencies:
@@ -2552,6 +2564,9 @@
'@emotion/unitless@0.8.1':
resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
+
+ '@epic-web/invariant@1.0.0':
+ resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
'@es-joy/jsdoccomment@0.84.0':
resolution: {integrity: sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==}
@@ -4754,6 +4769,9 @@
'@vue/devtools-shared@8.1.3':
resolution: {integrity: sha512-CM3uIPL+v+lrJUk33+pxspYo0MhuMWlCvf7zC9fybifvCPyM2jUbYRPwoYEJgYbwRqPikm5HozbUhp60MF2QuA==}
+ '@vue/language-core@3.3.9':
+ resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==}
+
'@vue/reactivity@3.5.38':
resolution: {integrity: sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==}
@@ -4914,6 +4932,9 @@
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
+
+ alien-signals@3.2.1:
+ resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -5527,6 +5548,11 @@
cropperjs@1.6.2:
resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
+
+ cross-env@10.1.0:
+ resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
+ engines: {node: '>=20'}
+ hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
@@ -9595,6 +9621,12 @@
peerDependencies:
vue: ^3.2.0
+ vue-tsc@3.3.9:
+ resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==}
+ hasBin: true
+ peerDependencies:
+ typescript: '>=5.0.0'
+
vue-types@3.0.0:
resolution: {integrity: sha512-xzq2D7zLGbzn3bPQ0CWWYx4W78JWdGYZAFfRkzxz1aTm9DaNoH0qxYDiLXBofBEQ5X78l32uIq/G9INQ2E8X/Q==}
engines: {node: '>=10.15.0'}
@@ -10976,6 +11008,8 @@
'@emotion/hash@0.9.2': {}
'@emotion/unitless@0.8.1': {}
+
+ '@epic-web/invariant@1.0.0': {}
'@es-joy/jsdoccomment@0.84.0':
dependencies:
@@ -12903,6 +12937,16 @@
'@vue/devtools-shared@8.1.3': {}
+ '@vue/language-core@3.3.9':
+ dependencies:
+ '@volar/language-core': 2.4.28
+ '@vue/compiler-dom': 3.5.38
+ '@vue/shared': 3.5.38
+ alien-signals: 3.2.1
+ muggle-string: 0.4.1
+ path-browserify: 1.0.1
+ picomatch: 4.0.4
+
'@vue/reactivity@3.5.38':
dependencies:
'@vue/shared': 3.5.38
@@ -13069,6 +13113,8 @@
fast-uri: 3.1.2
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
+
+ alien-signals@3.2.1: {}
ansi-regex@5.0.1: {}
@@ -13722,6 +13768,11 @@
croner@10.0.1: {}
cropperjs@1.6.2: {}
+
+ cross-env@10.1.0:
+ dependencies:
+ '@epic-web/invariant': 1.0.0
+ cross-spawn: 7.0.6
cross-spawn@7.0.6:
dependencies:
@@ -16668,7 +16719,7 @@
robust-predicates@3.0.3: {}
- rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3):
+ rolldown-plugin-dts@0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3)):
dependencies:
'@babel/generator': 8.0.0
'@babel/helper-validator-identifier': 8.0.2
@@ -16681,6 +16732,7 @@
rolldown: 1.1.2
optionalDependencies:
typescript: 6.0.3
+ vue-tsc: 3.3.9(typescript@6.0.3)
transitivePeerDependencies:
- oxc-resolver
@@ -17573,7 +17625,7 @@
minimist: 1.2.8
strip-bom: 3.0.0
- tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)):
+ tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))(vue-tsc@3.3.9(typescript@6.0.3)):
dependencies:
ansis: 4.3.1
cac: 7.0.0
@@ -17584,7 +17636,7 @@
obug: 2.1.3
picomatch: 4.0.4
rolldown: 1.1.2
- rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)
+ rolldown-plugin-dts: 0.26.0(rolldown@1.1.2)(typescript@6.0.3)(vue-tsc@3.3.9(typescript@6.0.3))
semver: 7.8.5
tinyexec: 1.2.4
tinyglobby: 0.2.17
@@ -18092,6 +18144,12 @@
tippy.js: 6.3.7
vue: 3.5.38(typescript@6.0.3)
+ vue-tsc@3.3.9(typescript@6.0.3):
+ dependencies:
+ '@volar/typescript': 2.4.28
+ '@vue/language-core': 3.3.9
+ typescript: 6.0.3
+
vue-types@3.0.0(vue@3.5.38(typescript@6.0.3)):
dependencies:
is-plain-object: 3.0.1
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 5df4a75..6105520 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -193,7 +193,7 @@
vue-json-pretty: ^2.6.0
vue-router: ^5.1.0
vue-tippy: ^6.7.1
- vue-tsc: ^3.3.3
+ vue-tsc: ^3.3.9
vue3-print-nb: ^0.1.4
vue3-signature: ^0.4.4
vuedraggable: ^4.1.0
diff --git a/public/logos/YZFXLogo.ico b/public/logos/YZFXLogo.ico
new file mode 100644
index 0000000..2438b06
--- /dev/null
+++ b/public/logos/YZFXLogo.ico
Binary files differ
diff --git a/public/logos/YZFXLogo.png b/public/logos/YZFXLogo.png
new file mode 100644
index 0000000..2438b06
--- /dev/null
+++ b/public/logos/YZFXLogo.png
Binary files differ
diff --git a/src/api/aftersales/problem.ts b/src/api/aftersales/problem.ts
new file mode 100644
index 0000000..313f3ba
--- /dev/null
+++ b/src/api/aftersales/problem.ts
@@ -0,0 +1,103 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace AftersalesProblemApi {
+ /** 鍞悗闂 */
+ export interface Problem {
+ id?: number;
+ no?: string;
+ title?: string;
+ sourceTicketId?: number;
+ sourceTicketNo?: string;
+ customerId?: number;
+ customerName?: string;
+ itemId?: number;
+ itemCode?: string;
+ itemName?: string;
+ batchId?: number;
+ batchCode?: string;
+ issueType?: number;
+ issueCategory?: string;
+ severityLevel?: number;
+ description?: string;
+ /** 鏍瑰洜鍒嗙被: 1-璁捐 2-鐗╂枡 3-宸ヨ壓 4-瑁呴厤 5-杩愯緭 6-鍖呰 7-浣跨敤 8-杞欢 9-鍏朵粬 */
+ rootCauseCategory?: number;
+ rootCause?: string;
+ solution?: string;
+ /** 鐘舵��: 0-鑽夌 1-宸插彂甯� 2-宸插仠鐢� */
+ status?: number;
+ ownerUserId?: number;
+ ownerUserName?: string;
+ publishTime?: string;
+ remark?: string;
+ creator?: string;
+ createTime?: string;
+ updater?: string;
+ updateTime?: string;
+ /** 闄勪欢 blobId 鍒楄〃锛堜繚瀛樻椂浼犲叆锛� */
+ blobIds?: number[];
+ /** 闄勪欢鍒楄〃锛堣鎯呰繑鍥烇級 */
+ attachmentList?: { id: number; name?: string; url?: string }[];
+ }
+}
+
+/** 鏂板鍞悗闂锛堣崏绋匡級 */
+export function createProblem(data: AftersalesProblemApi.Problem) {
+ return requestClient.post<number>('/aftersales/problem/create', data);
+}
+
+/** 鏇存柊鍞悗闂锛堜粎鑽夌锛� */
+export function updateProblem(data: AftersalesProblemApi.Problem) {
+ return requestClient.put<boolean>('/aftersales/problem/update', data);
+}
+
+/** 鍒犻櫎鍞悗闂锛堜粎鑽夌锛� */
+export function deleteProblem(id: number) {
+ return requestClient.delete<boolean>('/aftersales/problem/delete', {
+ params: { id },
+ });
+}
+
+/** 鑾峰彇鍞悗闂璇︽儏 */
+export function getProblem(id: number) {
+ return requestClient.get<AftersalesProblemApi.Problem>(
+ `/aftersales/problem/get?id=${id}`,
+ );
+}
+
+/** 鍒嗛〉鏌ヨ鍞悗闂 */
+export function getProblemPage(params: PageParam) {
+ return requestClient.get<PageResult<AftersalesProblemApi.Problem>>(
+ '/aftersales/problem/page',
+ { params },
+ );
+}
+
+/** 鍙戝竷闂锛堣崏绋�/鍋滅敤 -> 宸插彂甯冿級 */
+export function publishProblem(id: number) {
+ return requestClient.put<boolean>('/aftersales/problem/publish', null, {
+ params: { id },
+ });
+}
+
+/** 鍋滅敤闂锛堝凡鍙戝竷 -> 鍋滅敤锛� */
+export function disableProblem(id: number) {
+ return requestClient.put<boolean>('/aftersales/problem/disable', null, {
+ params: { id },
+ });
+}
+
+/** 鍙傝�冩绱紙鏂板伐鍗曞彈鐞嗭紝鍙繑鍥炲凡鍙戝竷锛� */
+export function referenceProblem(params: {
+ itemId?: number;
+ issueCategory?: string;
+ issueType?: number;
+ rootCauseCategory?: number;
+ keyword?: string;
+}) {
+ return requestClient.get<AftersalesProblemApi.Problem[]>(
+ '/aftersales/problem/reference',
+ { params },
+ );
+}
diff --git a/src/api/aftersales/ticket.ts b/src/api/aftersales/ticket.ts
index 36120d0..ad1056e 100644
--- a/src/api/aftersales/ticket.ts
+++ b/src/api/aftersales/ticket.ts
@@ -46,6 +46,14 @@
repairId?: number;
resolvedTime?: string;
closedTime?: string;
+ /** 鍥炶鐘舵��: 0-寰呭洖璁� 1-宸插洖璁块�氳繃 */
+ visitStatus?: number;
+ lastVisitTime?: string;
+ lastVisitSatisfied?: boolean;
+ lastVisitRecurred?: boolean;
+ /** 褰掓。娌夋穩鐨勯棶棰� */
+ problemId?: number;
+ problemNo?: string;
remark?: string;
creator?: string;
createTime?: string;
diff --git a/src/api/aftersales/visit.ts b/src/api/aftersales/visit.ts
new file mode 100644
index 0000000..d9ccd3b
--- /dev/null
+++ b/src/api/aftersales/visit.ts
@@ -0,0 +1,62 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace AftersalesVisitApi {
+ /** 鍥炶璁板綍 */
+ export interface Visit {
+ id?: number;
+ ticketId?: number;
+ visitType?: number;
+ visitorUserId?: number;
+ visitorUserName?: string;
+ content?: string;
+ satisfied?: boolean;
+ recurred?: boolean;
+ visitTime?: string;
+ remark?: string;
+ creator?: string;
+ createTime?: string;
+ /** 闄勪欢鍒楄〃锛堣鎯呰繑鍥烇級 */
+ attachmentList?: { id: number; name?: string; url?: string }[];
+ }
+
+ /** 鐧昏鍥炶鍏ュ弬 */
+ export interface CreateVisit {
+ ticketId: number;
+ visitType: number;
+ content?: string;
+ satisfied: boolean;
+ recurred: boolean;
+ remark?: string;
+ /** 闄勪欢 blobId 鍒楄〃锛堜繚瀛樻椂浼犲叆锛� */
+ blobIds?: number[];
+ }
+}
+
+/** 鐧昏鍞悗鍥炶 */
+export function createVisit(data: AftersalesVisitApi.CreateVisit) {
+ return requestClient.post<number>('/aftersales/visit/create', data);
+}
+
+/** 鑾峰彇鍥炶璇︽儏 */
+export function getVisit(id: number) {
+ return requestClient.get<AftersalesVisitApi.Visit>(
+ `/aftersales/visit/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇鏌愬伐鍗曠殑鍥炶璁板綍鍒楄〃 */
+export function getVisitListByTicketId(ticketId: number) {
+ return requestClient.get<AftersalesVisitApi.Visit[]>(
+ `/aftersales/visit/list?ticketId=${ticketId}`,
+ );
+}
+
+/** 鍒嗛〉鏌ヨ鍥炶璁板綍 */
+export function getVisitPage(params: PageParam) {
+ return requestClient.get<PageResult<AftersalesVisitApi.Visit>>(
+ '/aftersales/visit/page',
+ { params },
+ );
+}
diff --git a/src/api/bpm/autoApprove/index.ts b/src/api/bpm/autoApprove/index.ts
new file mode 100644
index 0000000..def749a
--- /dev/null
+++ b/src/api/bpm/autoApprove/index.ts
@@ -0,0 +1,59 @@
+import { requestClient } from '#/api/request';
+
+export namespace BpmAutoApproveApi {
+ /** 娴佺▼鍏嶅鎵归厤缃� */
+ export interface AutoApproveConfig {
+ id: number;
+ /** 娴佺▼瀹氫箟鐨� Key */
+ processDefinitionKey: string;
+ /** 鏄惁鍏嶅鎵癸細true-鎻愪氦鍚庣郴缁熻嚜鍔ㄩ�氳繃锛宖alse-姝e父浜哄伐瀹℃壒 */
+ autoApprove: boolean;
+ /** 澶囨敞 */
+ remark?: string;
+ createTime: string;
+ }
+}
+
+/** 鑾峰彇鍏ㄩ儴娴佺▼鍏嶅鎵归厤缃� */
+export async function getAutoApproveList() {
+ return requestClient.get<BpmAutoApproveApi.AutoApproveConfig[]>(
+ '/bpm/process-auto-approve/list',
+ );
+}
+
+/** 鑾峰緱鍏ㄥ眬鍏嶅鎵规�诲紑鍏崇姸鎬� */
+export async function getGlobalAutoApprove() {
+ return requestClient.get<boolean>('/bpm/process-auto-approve/global');
+}
+
+/** 璁剧疆鍏ㄥ眬鍏嶅鎵规�诲紑鍏筹細寮�鍚悗鎵�鏈夋祦绋嬩竴寰嬪厤瀹℃壒 */
+export async function updateGlobalAutoApprove(
+ autoApprove: boolean,
+ remark?: string,
+) {
+ const params = new URLSearchParams({ autoApprove: String(autoApprove) });
+ if (remark) {
+ params.append('remark', remark);
+ }
+ return requestClient.put<boolean>(
+ `/bpm/process-auto-approve/update-global?${params.toString()}`,
+ );
+}
+
+/** 鎸夋祦绋嬪畾涔夌殑 Key 璁剧疆鍏嶅鎵瑰紑鍏筹紙涓嶅瓨鍦ㄥ垯鏂板銆佸瓨鍦ㄥ垯鏇存柊锛� */
+export async function updateAutoApproveByKey(
+ processDefinitionKey: string,
+ autoApprove: boolean,
+ remark?: string,
+) {
+ const params = new URLSearchParams({
+ processDefinitionKey,
+ autoApprove: String(autoApprove),
+ });
+ if (remark) {
+ params.append('remark', remark);
+ }
+ return requestClient.put<boolean>(
+ `/bpm/process-auto-approve/update-by-key?${params.toString()}`,
+ );
+}
diff --git a/src/api/bpm/businessAttach/index.ts b/src/api/bpm/businessAttach/index.ts
new file mode 100644
index 0000000..4dd1aa9
--- /dev/null
+++ b/src/api/bpm/businessAttach/index.ts
@@ -0,0 +1,52 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace BpmBusinessAttachApi {
+ /** 涓氬姟闄勪欢閰嶇疆 */
+ export interface BusinessAttach {
+ id: number;
+ processDefinitionKey: string;
+ businessType?: string;
+ componentPath: string;
+ status: number;
+ remark?: string;
+ createTime?: string;
+ }
+}
+
+/** 鏌ヨ涓氬姟闄勪欢閰嶇疆鍒嗛〉 */
+export async function getBusinessAttachPage(params: PageParam) {
+ return requestClient.get<PageResult<BpmBusinessAttachApi.BusinessAttach>>(
+ '/bpm/business-attach/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ涓氬姟闄勪欢閰嶇疆璇︽儏 */
+export async function getBusinessAttach(id: number) {
+ return requestClient.get<BpmBusinessAttachApi.BusinessAttach>(
+ `/bpm/business-attach/get?id=${id}`,
+ );
+}
+
+/** 鏂板涓氬姟闄勪欢閰嶇疆 */
+export async function createBusinessAttach(
+ data: BpmBusinessAttachApi.BusinessAttach,
+) {
+ return requestClient.post<number>('/bpm/business-attach/create', data);
+}
+
+/** 淇敼涓氬姟闄勪欢閰嶇疆 */
+export async function updateBusinessAttach(
+ data: BpmBusinessAttachApi.BusinessAttach,
+) {
+ return requestClient.put<boolean>('/bpm/business-attach/update', data);
+}
+
+/** 鍒犻櫎涓氬姟闄勪欢閰嶇疆 */
+export async function deleteBusinessAttach(id: number) {
+ return requestClient.delete<boolean>(
+ `/bpm/business-attach/delete?id=${id}`,
+ );
+}
diff --git a/src/api/bpm/model/index.ts b/src/api/bpm/model/index.ts
index e0c033a..8c83a1c 100644
--- a/src/api/bpm/model/index.ts
+++ b/src/api/bpm/model/index.ts
@@ -20,6 +20,8 @@
createTime: string;
bpmnXml: string;
startUsers?: UserInfo[];
+ /** 鏄惁鍏嶅鎵癸紙鐢遍〉闈㈡牴鎹祦绋嬪厤瀹℃壒閰嶇疆鍚堝苟锛岄潪鍚庣杩斿洖瀛楁锛� */
+ autoApprove?: boolean;
}
/** 娴佺▼瀹氫箟 */
diff --git a/src/api/crm/contract/index.ts b/src/api/crm/contract/index.ts
index 4626920..808b869 100644
--- a/src/api/crm/contract/index.ts
+++ b/src/api/crm/contract/index.ts
@@ -28,6 +28,8 @@
totalPrice: number;
depositPrice?: number; // 瀹氶噾閲戦锛屽崟浣嶏細鍏�
totalReceivablePrice: number;
+ totalInvoicePrice?: number; // 宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛夛紝鍗曚綅锛氬厓
+ unInvoicePrice?: number; // 鏈紑绁ㄩ噾棰濓紝鍗曚綅锛氬厓
signContactId: number;
signContactName?: string;
signUserId: number;
@@ -158,3 +160,23 @@
export function generateSaleOrder(id: number) {
return requestClient.post<number>('/crm/contract/generate-sale-order', null, { params: { id } });
}
+
+/** 鍚堝悓瀵煎叆缁撴灉 */
+export interface ImportResult {
+ /** 鍒涘缓鎴愬姛鐨勫悎鍚岀紪鍙� */
+ createList?: string[];
+ /** 鏇存柊鎴愬姛鐨勫悎鍚岀紪鍙� */
+ updateList?: string[];
+ /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓哄悎鍚岀紪鍙锋垨琛屽彿锛寁alue 涓哄け璐ュ師鍥� */
+ failureList?: Record<string, string>;
+}
+
+/** 涓嬭浇鍚堝悓瀵煎叆妯℃澘 */
+export function importContractTemplate() {
+ return requestClient.download('/crm/contract/get-import-template');
+}
+
+/** 瀵煎叆鍚堝悓 */
+export function importContract(file: File) {
+ return requestClient.upload<ImportResult>('/crm/contract/import', { file });
+}
diff --git a/src/api/crm/invoice/index.ts b/src/api/crm/invoice/index.ts
new file mode 100644
index 0000000..121f6ec
--- /dev/null
+++ b/src/api/crm/invoice/index.ts
@@ -0,0 +1,122 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmInvoiceApi {
+ /** 鍚堝悓淇℃伅 */
+ export interface Contract {
+ id?: number;
+ name?: string;
+ no: string;
+ }
+
+ /** 闄勪欢淇℃伅 */
+ export interface Attachment {
+ id: number;
+ url?: string;
+ name?: string;
+ }
+
+ /** 寮�绁ㄤ俊鎭� */
+ export interface Invoice {
+ id?: number;
+ no?: string;
+ contractId?: number;
+ contract?: Contract;
+ customerId?: number;
+ customerName?: string;
+ invoiceType?: number;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
+ invoiceTime?: string;
+ auditStatus?: number;
+ remark?: string;
+ ownerUserId?: number;
+ ownerUserName?: string;
+ ownerUserDeptName?: string;
+ processInstanceId?: string;
+ attachmentList?: Attachment[];
+ blobIds?: number[];
+ hasAttachment?: boolean;
+ remainingInvoicePrice?: number;
+ creator?: string;
+ creatorName?: string;
+ createTime?: string;
+ updateTime?: string;
+ }
+
+ /** 寮�绁ㄧ畝瑕佷俊鎭紙鐢ㄤ簬鍥炴鍏宠仈閫夋嫨锛� */
+ export interface InvoiceSimple {
+ id: number;
+ no: string;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
+ customerId?: number;
+ contractId?: number;
+ auditStatus?: number;
+ hasAttachment?: boolean;
+ remainingInvoicePrice?: number;
+ }
+}
+
+/** 鏌ヨ寮�绁ㄥ垎椤� */
+export function getInvoicePage(params: PageParam) {
+ return requestClient.get<PageResult<CrmInvoiceApi.Invoice>>(
+ '/crm/invoice/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ寮�绁ㄥ垎椤碉紝鍩轰簬鎸囧畾瀹㈡埛 */
+export function getInvoicePageByCustomer(params: PageParam) {
+ return requestClient.get<PageResult<CrmInvoiceApi.Invoice>>(
+ '/crm/invoice/page-by-customer',
+ { params },
+ );
+}
+
+/** 鏌ヨ寮�绁ㄨ鎯� */
+export function getInvoice(id: number) {
+ return requestClient.get<CrmInvoiceApi.Invoice>(
+ `/crm/invoice/get?id=${id}`,
+ );
+}
+
+/** 鏂板寮�绁� */
+export function createInvoice(data: CrmInvoiceApi.Invoice) {
+ return requestClient.post('/crm/invoice/create', data);
+}
+
+/** 淇敼寮�绁� */
+export function updateInvoice(data: CrmInvoiceApi.Invoice) {
+ return requestClient.put('/crm/invoice/update', data);
+}
+
+/** 鍒犻櫎寮�绁� */
+export function deleteInvoice(id: number) {
+ return requestClient.delete(`/crm/invoice/delete?id=${id}`);
+}
+
+/** 瀵煎嚭寮�绁� */
+export function exportInvoice(params: Record<string, unknown>) {
+ return requestClient.download('/crm/invoice/export-excel', { params });
+}
+
+/** 鎻愪氦瀹℃牳 */
+export function submitInvoice(id: number) {
+ return requestClient.put(`/crm/invoice/submit?id=${id}`);
+}
+
+/** 鑾峰緱寰呭鏍稿紑绁ㄦ暟閲� */
+export function getAuditInvoiceCount() {
+ return requestClient.get<number>('/crm/invoice/audit-count');
+}
+
+/** 鏌ヨ寮�绁ㄤ笅鎷夊垪琛紙鎸夊悎鍚屾煡鍙�夊紑绁級 */
+export function getInvoiceSimpleList(contractId: number) {
+ return requestClient.get<CrmInvoiceApi.InvoiceSimple[]>(
+ `/crm/invoice/simple-list?contractId=${contractId}`,
+ );
+}
diff --git a/src/api/crm/permission/index.ts b/src/api/crm/permission/index.ts
index b95e142..dad85ce 100644
--- a/src/api/crm/permission/index.ts
+++ b/src/api/crm/permission/index.ts
@@ -43,6 +43,7 @@
CRM_PRODUCT = 6, // 浜у搧
CRM_RECEIVABLE = 7, // 鍥炴
CRM_RECEIVABLE_PLAN = 8, // 鍥炴璁″垝
+ CRM_INVOICE = 9, // 寮�绁�
}
/**
diff --git a/src/api/crm/receivable/index.ts b/src/api/crm/receivable/index.ts
index 36b3db0..974be6e 100644
--- a/src/api/crm/receivable/index.ts
+++ b/src/api/crm/receivable/index.ts
@@ -14,6 +14,8 @@
contractId?: number;
contractNo?: string;
contract?: Contract;
+ invoiceId?: number;
+ invoice?: InvoiceInfo;
auditStatus: number;
processInstanceId: number;
returnTime: Date;
@@ -34,6 +36,15 @@
name?: string;
no: string;
totalPrice: number;
+ }
+
+ /** 寮�绁ㄤ俊鎭� */
+ export interface InvoiceInfo {
+ id?: number;
+ no?: string;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
}
export interface ReceivablePageParam extends PageParam {
@@ -87,9 +98,11 @@
return requestClient.download('/crm/receivable/export-excel', { params });
}
-/** 鎻愪氦瀹℃牳 */
-export function submitReceivable(id: number) {
- return requestClient.put(`/crm/receivable/submit?id=${id}`);
+/** 鏇存柊鍥炴鐨勭姸鎬侊紙瀹℃牳/鍙嶅鏍革級 */
+export function updateReceivableStatus(id: number, status: number) {
+ return requestClient.put('/crm/receivable/update-status', null, {
+ params: { id, status },
+ });
}
/** 鑾峰緱寰呭鏍稿洖娆炬暟閲� */
diff --git a/src/api/crm/saleQuotation/ai/index.ts b/src/api/crm/saleQuotation/ai/index.ts
index a9b18fb..2409471 100644
--- a/src/api/crm/saleQuotation/ai/index.ts
+++ b/src/api/crm/saleQuotation/ai/index.ts
@@ -9,7 +9,7 @@
quotationPrice?: number;
}
- /** AI OCR 璇嗗埆鎶ヤ环鏂囦欢鍝嶅簲 */
+ /** 璇嗗埆鎶ヤ环鏂囦欢鍝嶅簲 */
export interface OcrResultVO {
name?: string;
customerName?: string;
@@ -23,7 +23,7 @@
}
}
-/** AI OCR 璇嗗埆鎶ヤ环鏂囦欢 */
+/** 璇嗗埆鎶ヤ环鏂囦欢 */
export function ocrSaleQuotation(blobId: number) {
return requestClient.post<CrmSaleQuotationAiApi.OcrResultVO>(
'/crm/sale-quotation/ai/ocr',
diff --git a/src/api/crm/salesTarget/index.ts b/src/api/crm/salesTarget/index.ts
new file mode 100644
index 0000000..22e5330
--- /dev/null
+++ b/src/api/crm/salesTarget/index.ts
@@ -0,0 +1,113 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace CrmSalesTargetApi {
+ /** 閿�鍞洰鏍囦俊鎭� */
+ export interface SalesTarget {
+ id?: number;
+ name?: string;
+ periodType?: number;
+ periodValue?: string;
+ startTime?: string;
+ endTime?: string;
+ targetDimension?: number;
+ targetUserId?: number;
+ targetUserName?: string;
+ targetDeptId?: number;
+ targetDeptName?: string;
+ targetItemId?: number;
+ targetItemName?: string;
+ contractTarget?: number;
+ receivableTarget?: number;
+ businessTarget?: number;
+ customerTarget?: number;
+ contractCountTarget?: number;
+ actualContractPrice?: number;
+ actualReceivablePrice?: number;
+ actualBusinessPrice?: number;
+ actualCustomerCount?: number;
+ actualContractCount?: number;
+ contractRate?: number;
+ receivableRate?: number;
+ businessRate?: number;
+ customerRate?: number;
+ contractCountRate?: number;
+ status?: number;
+ remark?: string;
+ creator?: string;
+ creatorName?: string;
+ createTime?: string;
+ updateTime?: string;
+ }
+
+ /** 閿�鍞洰鏍囧垎椤垫煡璇㈠弬鏁� */
+ export interface PageParams extends PageParam {
+ name?: string;
+ periodType?: number;
+ periodValue?: string;
+ targetDimension?: number;
+ status?: number;
+ }
+
+ /** 閿�鍞洰鏍囪揪鎴愬垎鏋愭煡璇㈠弬鏁� */
+ export interface AnalysisParams {
+ periodType?: number;
+ periodValue?: string;
+ targetDimension?: number;
+ status?: number;
+ }
+}
+
+/** 鏌ヨ閿�鍞洰鏍囧垎椤� */
+export function getSalesTargetPage(params: CrmSalesTargetApi.PageParams) {
+ return requestClient.get<PageResult<CrmSalesTargetApi.SalesTarget>>(
+ '/crm/sales-target/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ閿�鍞洰鏍囪鎯� */
+export function getSalesTarget(id: number) {
+ return requestClient.get<CrmSalesTargetApi.SalesTarget>(
+ `/crm/sales-target/get?id=${id}`,
+ );
+}
+
+/** 鏂板閿�鍞洰鏍� */
+export function createSalesTarget(data: CrmSalesTargetApi.SalesTarget) {
+ return requestClient.post('/crm/sales-target/create', data);
+}
+
+/** 淇敼閿�鍞洰鏍� */
+export function updateSalesTarget(data: CrmSalesTargetApi.SalesTarget) {
+ return requestClient.put('/crm/sales-target/update', data);
+}
+
+/** 鍙樻洿閿�鍞洰鏍囩姸鎬侊紙浣滃簾/鎭㈠绛夛級 */
+export function changeSalesTargetStatus(data: {
+ id: number;
+ status: number;
+}) {
+ return requestClient.put('/crm/sales-target/change-status', data);
+}
+
+/** 鍒犻櫎閿�鍞洰鏍� */
+export function deleteSalesTarget(id: number) {
+ return requestClient.delete(`/crm/sales-target/delete?id=${id}`);
+}
+
+/** 瀵煎嚭閿�鍞洰鏍� */
+export function exportSalesTarget(params: Record<string, unknown>) {
+ return requestClient.download('/crm/sales-target/export-excel', { params });
+}
+
+/** 鏌ヨ閿�鍞洰鏍囪揪鎴愬垎鏋� */
+export function getSalesTargetAnalysis(
+ params: CrmSalesTargetApi.AnalysisParams,
+) {
+ return requestClient.get<CrmSalesTargetApi.SalesTarget[]>(
+ '/crm/sales-target/get-achievement-analysis',
+ { params },
+ );
+}
diff --git a/src/api/erp/finance/payment/index.ts b/src/api/erp/finance/payment/index.ts
index 7f8586d..276c047 100644
--- a/src/api/erp/finance/payment/index.ts
+++ b/src/api/erp/finance/payment/index.ts
@@ -1,5 +1,7 @@
import type { PageParam, PageResult } from '@vben/request';
+import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
+
import { requestClient } from '#/api/request';
export namespace ErpFinancePaymentApi {
@@ -9,11 +11,14 @@
no: string; // 浠樻鍗曞彿
supplierId?: number; // 渚涘簲鍟嗙紪鍙�
supplierName?: string; // 渚涘簲鍟嗗悕绉�
+ invoiceId?: number; // 鍏宠仈鏉ョエ缂栧彿
+ invoice?: ErpPurchaseInvoiceApi.PurchaseInvoice; // 鍏宠仈鏉ョエ淇℃伅
paymentTime?: Date; // 浠樻鏃堕棿
totalPrice: number; // 鍚堣閲戦锛屽崟浣嶏細鍏�
discountPrice: number; // 浼樻儬閲戦
paymentPrice: number; // 瀹為檯浠樻閲戦
status: number; // 鐘舵��
+ processInstanceId?: string; // 宸ヤ綔娴佺紪鍙�
remark: string; // 澶囨敞
attachmentList?: { id: number; name?: string; url?: string }[]; // 闄勪欢鍒楄〃
accountId?: number; // 浠樻璐︽埛
@@ -71,13 +76,18 @@
return requestClient.put('/erp/finance-payment/update', data);
}
-/** 鏇存柊浠樻鍗曠殑鐘舵�� */
-export function updateFinancePaymentStatus(id: number, status: number) {
- return requestClient.put('/erp/finance-payment/update-status', null, {
- params: { id, status },
+/** 鎻愪氦浠樻鍗曞鎵� */
+export function submitFinancePayment(id: number, processDefinitionKey: string) {
+ return requestClient.post('/erp/finance-payment/submit', null, {
+ params: { id, processDefinitionKey },
});
}
+/** 鑾峰彇浠樻鍗曞鎵规祦绋嬪垪琛� */
+export function getFinancePaymentApproveProcessList() {
+ return requestClient.get('/erp/finance-payment/approve-process-list');
+}
+
/** 鍒犻櫎浠樻鍗� */
export function deleteFinancePayment(ids: number[]) {
return requestClient.delete('/erp/finance-payment/delete', {
diff --git a/src/api/erp/purchase/invoice/ai/index.ts b/src/api/erp/purchase/invoice/ai/index.ts
new file mode 100644
index 0000000..3b7e308
--- /dev/null
+++ b/src/api/erp/purchase/invoice/ai/index.ts
@@ -0,0 +1,33 @@
+import { requestClient } from '#/api/request';
+
+export namespace ErpPurchaseInvoiceAiApi {
+ /** 璇嗗埆鍙戠エ鍝嶅簲 */
+ export interface OcrResultVO {
+ /** 鍙戠エ鍙风爜 */
+ invoiceNo?: string;
+ /** 鍙戠エ鎶ご锛堣喘涔版柟鍚嶇О锛� */
+ invoiceTitle?: string;
+ /** 閿�鍞柟鍚嶇О锛堟枃鏈紝渚涙牳瀵逛緵搴斿晢锛� */
+ supplierName?: string;
+ /** 鏉ョエ閲戦锛堜环绋庡悎璁★級锛屽崟浣嶏細鍏� */
+ price?: number;
+ /** 寮�绁ㄦ棩鏈燂紝鏍煎紡 yyyy-MM-dd */
+ invoiceTime?: string;
+ /** 鍙戠エ绫诲瀷 */
+ invoiceType?: string;
+ /** 绋庣巼锛堢櫨鍒嗘瘮锛� */
+ taxRate?: number;
+ /** 澶囨敞 */
+ remark?: string;
+ /** 鍘熷璇嗗埆鏂囨湰锛堣瘑鍒け璐ユ椂涓洪敊璇彁绀猴級 */
+ rawText?: string;
+ }
+}
+
+/** 璇嗗埆鍙戠エ鏂囦欢 */
+export function ocrPurchaseInvoice(blobId: number) {
+ return requestClient.post<ErpPurchaseInvoiceAiApi.OcrResultVO>(
+ '/erp/purchase-invoice/ai/ocr',
+ { blobId },
+ );
+}
diff --git a/src/api/erp/purchase/invoice/index.ts b/src/api/erp/purchase/invoice/index.ts
new file mode 100644
index 0000000..305230c
--- /dev/null
+++ b/src/api/erp/purchase/invoice/index.ts
@@ -0,0 +1,100 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace ErpPurchaseInvoiceApi {
+ /** 閲囪喘璁㈠崟淇℃伅锛堟潵绁ㄥ叧鑱旓級 */
+ export interface PurchaseOrder {
+ id?: number;
+ no?: string;
+ supplierId?: number;
+ supplierName?: string;
+ orderTime?: Date | string;
+ totalPrice?: number;
+ status?: number;
+ }
+
+ /** 鏉ョエ淇℃伅 */
+ export interface PurchaseInvoice {
+ id?: number;
+ no?: string;
+ supplierId?: number;
+ supplierName?: string;
+ purchaseOrderId?: number;
+ order?: PurchaseOrder;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
+ invoiceTime?: string;
+ remark?: string;
+ attachmentList?: { id: number; name?: string; url?: string }[];
+ blobIds?: number[];
+ hasAttachment?: boolean;
+ remainingInvoicePrice?: number;
+ creator?: string;
+ creatorName?: string;
+ createTime?: string;
+ updateTime?: string;
+ }
+
+ /** 鏉ョエ绠�瑕佷俊鎭紙鐢ㄤ簬浠樻鍏宠仈閫夋嫨锛� */
+ export interface PurchaseInvoiceSimple {
+ id: number;
+ no: string;
+ invoiceNo?: string;
+ invoiceTitle?: string;
+ price?: number;
+ supplierId?: number;
+ purchaseOrderId?: number;
+ hasAttachment?: boolean;
+ remainingInvoicePrice?: number;
+ }
+}
+
+/** 鏌ヨ鏉ョエ鍒嗛〉 */
+export function getPurchaseInvoicePage(params: PageParam) {
+ return requestClient.get<PageResult<ErpPurchaseInvoiceApi.PurchaseInvoice>>(
+ '/erp/purchase-invoice/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鏉ョエ璇︽儏 */
+export function getPurchaseInvoice(id: number) {
+ return requestClient.get<ErpPurchaseInvoiceApi.PurchaseInvoice>(
+ `/erp/purchase-invoice/get?id=${id}`,
+ );
+}
+
+/** 鏂板鏉ョエ */
+export function createPurchaseInvoice(data: ErpPurchaseInvoiceApi.PurchaseInvoice) {
+ return requestClient.post('/erp/purchase-invoice/create', data);
+}
+
+/** 淇敼鏉ョエ */
+export function updatePurchaseInvoice(data: ErpPurchaseInvoiceApi.PurchaseInvoice) {
+ return requestClient.put('/erp/purchase-invoice/update', data);
+}
+
+/** 鍒犻櫎鏉ョエ */
+export function deletePurchaseInvoice(id: number) {
+ return requestClient.delete(`/erp/purchase-invoice/delete?id=${id}`);
+}
+
+/** 瀵煎嚭鏉ョエ */
+export function exportPurchaseInvoice(params: Record<string, unknown>) {
+ return requestClient.download('/erp/purchase-invoice/export-excel', { params });
+}
+
+/** 鏌ヨ鏉ョエ涓嬫媺鍒楄〃锛堟寜渚涘簲鍟嗗彲閫夋潵绁紝宸蹭笂浼犲彂绁ㄩ檮浠朵笖鍓╀綑鍙粯閲戦澶т簬 0锛� */
+export function getPurchaseInvoiceSimpleList(
+ supplierId: number,
+ purchaseOrderId?: number,
+) {
+ const params = `supplierId=${supplierId}${
+ purchaseOrderId ? `&purchaseOrderId=${purchaseOrderId}` : ''
+ }`;
+ return requestClient.get<ErpPurchaseInvoiceApi.PurchaseInvoiceSimple[]>(
+ `/erp/purchase-invoice/simple-list?${params}`,
+ );
+}
diff --git a/src/api/erp/purchase/order/index.ts b/src/api/erp/purchase/order/index.ts
index 5ea3dff..4f0c6d6 100644
--- a/src/api/erp/purchase/order/index.ts
+++ b/src/api/erp/purchase/order/index.ts
@@ -13,6 +13,8 @@
totalCount?: number; // 鍚堣鏁伴噺
totalPrice?: number; // 鍚堣閲戦锛屽崟浣嶏細鍏�
totalProductPrice?: number; // 浜у搧閲戦锛屽崟浣嶏細鍏�
+ totalTaxPrice?: number; // 鍚堣绋庨锛屽崟浣嶏細鍏�
+ remainInvoicePrice?: number; // 鍓╀綑鍙紑绁ㄩ噾棰濓紙璁㈠崟鎬婚噾棰�-宸叉潵绁ㄩ噾棰濓級锛屽崟浣嶏細鍏�
discountPercent?: number; // 浼樻儬鐜囷紝鐧惧垎姣�
discountPrice?: number; // 浼樻儬閲戦锛屽崟浣嶏細鍏�
depositPrice?: number; // 瀹氶噾閲戦锛屽崟浣嶏細鍏�
@@ -103,6 +105,23 @@
return requestClient.get<any[]>('/erp/purchase-order/approve-process-list');
}
+/** 鍙嶅鎵癸紙鍗忓悓鍔炲叕宸插姙浠诲姟涓挙鍥炲凡瀹℃牳璁㈠崟锛屽洖閫�涓烘湭瀹℃牳锛� */
+export function reverseApprove(processInstanceId: string) {
+ return requestClient.post('/erp/purchase-order/reverse-approve', null, {
+ params: { processInstanceId },
+ });
+}
+
+/** 閲囪喘璁㈠崟瀵煎叆缁撴灉 */
+export interface ImportResult {
+ /** 鍒涘缓鎴愬姛鐨勯噰璐崟缂栧彿 */
+ createList?: string[];
+ /** 鏇存柊鎴愬姛鐨勯噰璐崟缂栧彿 */
+ updateList?: string[];
+ /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓洪噰璐崟缂栧彿鎴栬鍙凤紝value 涓哄け璐ュ師鍥� */
+ failureList?: Record<string, string>;
+}
+
/** 鑾峰彇閲囪喘瀹℃牳娴佺▼鍒楄〃鍝嶅簲 */
export interface ProcessDefinition {
id: string; // 娴佺▼瀹氫箟ID
@@ -117,3 +136,16 @@
params: { id },
});
}
+
+/** 涓嬭浇閲囪喘璁㈠崟瀵煎叆妯℃澘 */
+export function importPurchaseOrderTemplate() {
+ return requestClient.download('/erp/purchase-order/get-import-template');
+}
+
+/** 瀵煎叆閲囪喘璁㈠崟 */
+export function importPurchaseOrder(file: File, directApprove: boolean) {
+ return requestClient.upload<ImportResult>(
+ '/erp/purchase-order/import',
+ { file, directApprove },
+ );
+}
diff --git a/src/api/erp/purchase/request/index.ts b/src/api/erp/purchase/request/index.ts
index c446da4..3553382 100644
--- a/src/api/erp/purchase/request/index.ts
+++ b/src/api/erp/purchase/request/index.ts
@@ -46,6 +46,16 @@
}
}
+/** 閲囪喘鐢宠瀵煎叆缁撴灉 */
+export interface ImportResult {
+ /** 鍒涘缓鎴愬姛鐨勯噰璐敵璇峰崟鍙� */
+ createList?: string[];
+ /** 鏇存柊鎴愬姛鐨勯噰璐敵璇峰崟鍙� */
+ updateList?: string[];
+ /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓洪噰璐敵璇峰崟鍙锋垨琛屽彿锛寁alue 涓哄け璐ュ師鍥� */
+ failureList?: Record<string, string>;
+}
+
/** 鍒涘缓閲囪喘鐢宠 */
export function createPurchaseRequest(data: ErpPurchaseRequestApi.PurchaseRequest) {
return requestClient.post('/erp/purchase-request/create', data);
@@ -59,6 +69,11 @@
/** 鎻愪氦閲囪喘鐢宠瀹℃壒 */
export function submitPurchaseRequest(id: number, processDefinitionKey: string) {
return requestClient.put('/erp/purchase-request/submit', null, { params: { id, processDefinitionKey } });
+}
+
+/** 閲嶇疆琚┏鍥炵殑閲囪喘鐢宠涓鸿崏绋跨姸鎬� */
+export function resetPurchaseRequestStatus(id: number) {
+ return requestClient.put('/erp/purchase-request/reset-status', null, { params: { id } });
}
/** 鐢熸垚閲囪喘璁㈠崟 */
@@ -95,3 +110,47 @@
export function getApproveProcessList(id: number) {
return requestClient.get(`/erp/purchase-request/approve-process-list`, { params: { id } });
}
+
+/** BPM 娴佺▼瀹炰緥鐘舵�� 鈫� 涓氬姟鐘舵�佹槧灏� */
+export const BPM_STATUS_TO_BIZ: Record<number, number> = {
+ 1: 10, // RUNNING 鈫� 瀹℃壒涓�
+ 2: 20, // APPROVE 鈫� 瀹℃牳閫氳繃
+ 3: 30, // REJECT 鈫� 瀹℃牳涓嶉�氳繃
+ 4: 40, // CANCEL 鈫� 宸插彇娑�
+};
+
+/**
+ * 鍚屾閲囪喘鐢宠鐨勪笟鍔$姸鎬侊紙鏍规嵁 BPM 娴佺▼瀹炰緥瀹為檯鐘舵�侊級
+ * 鐢ㄤ簬淇鍚庣鐩戝惉鍣ㄦ湭姝g‘鍥炲啓瀵艰嚧鐨勪笟鍔$姸鎬佷笉涓�鑷撮棶棰�
+ */
+export async function syncPurchaseRequestStatus(items: ErpPurchaseRequestApi.PurchaseRequest[]) {
+ const processingItems = items.filter(
+ (item) => item.status === 10 && item.processInstanceId,
+ );
+ if (processingItems.length === 0) return;
+
+ const { getApprovalDetail } = await import('#/api/bpm/processInstance');
+ for (const item of processingItems) {
+ try {
+ const detail = await getApprovalDetail({
+ processInstanceId: item.processInstanceId,
+ });
+ const bizStatus = BPM_STATUS_TO_BIZ[detail.processInstance?.status];
+ if (bizStatus && bizStatus !== 10) {
+ item.status = bizStatus;
+ }
+ } catch {
+ // 鏌ヨ澶辫触鏃朵繚鎸佸師鏈夌姸鎬�
+ }
+ }
+}
+
+/** 涓嬭浇閲囪喘鐢宠瀵煎叆妯℃澘 */
+export function importPurchaseRequestTemplate() {
+ return requestClient.download('/erp/purchase-request/get-import-template');
+}
+
+/** 瀵煎叆閲囪喘鐢宠 */
+export function importPurchaseRequest(file: File) {
+ return requestClient.upload<ImportResult>('/erp/purchase-request/import', { file });
+}
diff --git a/src/api/erp/sale/order/index.ts b/src/api/erp/sale/order/index.ts
index 5468cab..069fc90 100644
--- a/src/api/erp/sale/order/index.ts
+++ b/src/api/erp/sale/order/index.ts
@@ -10,11 +10,13 @@
customerId: number; // 瀹㈡埛缂栧彿
customerName?: string; // 瀹㈡埛鍚嶇О
accountId?: number; // 鏀舵璐︽埛缂栧彿
+ saleUserId?: number; // 閿�鍞汉鍛樼紪鍙�
orderTime: Date; // 璁㈠崟鏃堕棿
totalCount: number; // 鍚堣鏁伴噺
totalPrice: number; // 鍚堣閲戦锛屽崟浣嶏細鍏�
status: number; // 鐘舵��
remark: string; // 澶囨敞
+ processInstanceId?: string; // BPM 娴佺▼瀹炰緥缂栧彿锛堟彁浜ゅ崗鍚屽姙鍏鎵瑰悗鐢熸垚锛�
outCount: number; // 閿�鍞嚭搴撴暟閲�
outStatus?: number; // 鍑哄簱鐘舵�侊細0-鏈嚭搴擄紝1-閮ㄥ垎鍑哄簱锛�2-鍏ㄩ儴鍑哄簱
attachmentList?: { id: number; name?: string; url?: string }[]; // 闄勪欢鍒楄〃
@@ -31,6 +33,9 @@
productionFinishTime?: Date; // 鐢熶骇瀹屾垚鏃堕棿
hasSalesNotice?: boolean; // 鏄惁宸茬敓鎴愬彂璐ч�氱煡鍗�
canCreateSalesNotice?: boolean; // 鏄惁鍙互鐢熸垚鍙戣揣閫氱煡鍗�
+ creator?: string; // 鍒涘缓浜虹紪鍙凤紙瀛楃涓诧級
+ creatorName?: string; // 鍒涘缓浜哄悕绉�
+ createTime?: Date; // 鍒涘缓鏃堕棿
items?: SaleOrderItem[]; // 閿�鍞鍗曚骇鍝佹槑缁嗗垪琛�
}
@@ -53,10 +58,21 @@
totalTaxPrice?: number; // 鍚◣鎬讳环锛屽崟浣嶏細鍏�
remark?: string; // 澶囨敞
stockCount?: number; // 搴撳瓨鏁伴噺锛堟樉绀哄瓧娈碉級
+ productSpecification?: string; // 瑙勬牸鍨嬪彿锛堟樉绀哄瓧娈碉級
needProduction?: number; // 鏄惁闇�瑕佺敓浜э細0-涓嶉渶瑕侊紝1-闇�瑕�
mpsId?: number; // MPS缂栧彿
batchCode?: string; // 鎵规鍙�
}
+}
+
+/** 閿�鍞鍗曞鍏ョ粨鏋� */
+export interface ImportResult {
+ /** 鍒涘缓鎴愬姛鐨勯攢鍞崟缂栧彿 */
+ createList?: string[];
+ /** 鏇存柊鎴愬姛鐨勯攢鍞崟缂栧彿 */
+ updateList?: string[];
+ /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓洪攢鍞崟缂栧彿鎴栬鍙凤紝value 涓哄け璐ュ師鍥� */
+ failureList?: Record<string, string>;
}
/** 鏌ヨ閿�鍞鍗曞垎椤� */
@@ -91,11 +107,18 @@
return requestClient.put('/erp/sale-order/update', data);
}
-/** 鏇存柊閿�鍞鍗曠殑鐘舵�� */
-export function updateSaleOrderStatus(id: number, status: number) {
- return requestClient.put('/erp/sale-order/update-status', null, {
- params: { id, status },
+/** 鎻愪氦閿�鍞鍗曞鎵癸紙鍗忓悓鍔炲叕锛� */
+export function submitSaleOrder(id: number, processDefinitionKey: string) {
+ return requestClient.post('/erp/sale-order/submit', null, {
+ params: { id, processDefinitionKey },
});
+}
+
+/** 鑾峰彇閿�鍞鍗曞鎵规祦绋嬪畾涔夊垪琛� */
+export function getSaleOrderApproveProcessList() {
+ return requestClient.get<
+ { id: string; key: string; name: string }[]
+ >('/erp/sale-order/approve-process-list');
}
/** 鍒犻櫎閿�鍞鍗� */
@@ -123,3 +146,13 @@
params: { id },
});
}
+
+/** 涓嬭浇閿�鍞鍗曞鍏ユā鏉� */
+export function importSaleOrderTemplate() {
+ return requestClient.download('/erp/sale-order/get-import-template');
+}
+
+/** 瀵煎叆閿�鍞鍗� */
+export function importSaleOrder(file: File) {
+ return requestClient.upload<ImportResult>('/erp/sale-order/import', { file });
+}
diff --git a/src/api/hrm/attendance/record/index.ts b/src/api/hrm/attendance/record/index.ts
index 584cb2e..f5a1144 100644
--- a/src/api/hrm/attendance/record/index.ts
+++ b/src/api/hrm/attendance/record/index.ts
@@ -10,6 +10,7 @@
userName?: string;
deptId?: number;
deptName?: string;
+ phone?: string;
date?: string;
clockInTime?: string;
clockOutTime?: string;
diff --git a/src/api/hrm/employee/contract/index.ts b/src/api/hrm/employee/contract/index.ts
new file mode 100644
index 0000000..a252c3e
--- /dev/null
+++ b/src/api/hrm/employee/contract/index.ts
@@ -0,0 +1,156 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmEmployeeContractApi {
+ /** 鍛樺伐鍚堝悓 */
+ export interface EmployeeContract {
+ id?: number;
+ /** 缁涓婁竴浠藉悎鍚孖D */
+ parentId?: number;
+ /** 缁涓婁竴浠藉悎鍚岀紪鍙� */
+ parentNo?: string;
+ /** 鏄惁褰撳墠鐢熸晥鍚堝悓 */
+ isCurrent?: boolean;
+ /** 鍚堝悓缂栧彿锛堝悗绔嚜鍔ㄧ敓鎴愶級 */
+ contractNo?: string;
+ /** 鍛樺伐ID */
+ employeeId?: number;
+ /** 鍛樺伐濮撳悕 */
+ employeeName?: string;
+ /** 鍛樺伐宸ュ彿 */
+ employeeNo?: string;
+ /** 閮ㄩ棬鍚嶇О */
+ deptName?: string;
+ /** 鍚堝悓绫诲瀷锛�1-鍔冲姩鍚堝悓 2-鍔冲姟鍚堝悓 3-瀹炰範鍗忚 4-鍔冲姟娲鹃仯 5-鍏朵粬 */
+ contractType?: number;
+ /** 鍚堝悓鏈熼檺绫诲瀷锛�1-鍥哄畾鏈熼檺 2-鏃犲浐瀹氭湡闄� 3-浠ュ畬鎴愪竴瀹氬伐浣滀换鍔′负鏈熼檺 */
+ contractTermType?: number;
+ /** 绛剧害涓讳綋 */
+ signCompany?: string;
+ /** 绛捐鏃ユ湡 */
+ signDate?: string;
+ /** 鍚堝悓寮�濮嬫棩鏈� */
+ startDate?: string;
+ /** 鍚堝悓缁撴潫鏃ユ湡 */
+ endDate?: string;
+ /** 璇曠敤鏈熷紑濮嬫棩鏈� */
+ probationStartDate?: string;
+ /** 璇曠敤鏈熺粨鏉熸棩鏈� */
+ probationEndDate?: string;
+ /** 璇曠敤鏈熷伐璧� */
+ probationSalary?: number;
+ /** 杞宸ヨ祫 */
+ regularSalary?: number;
+ /** 瑙i櫎缁堟鐘舵�侊細0-姝e父 1-宸茶В闄� 2-宸茬粓姝� */
+ terminateStatus?: number;
+ /** 瑙i櫎/缁堟鏃ユ湡 */
+ terminateDate?: string;
+ /** 瑙i櫎/缁堟鍘熷洜 */
+ terminateReason?: string;
+ /** 鍚堝悓鐘舵�侊紙鍔ㄦ�佽绠楋級锛�1-寰呯敓鏁� 2-鐢熸晥涓� 3-鍗冲皢鍒版湡 4-宸插埌鏈� 5-宸茶В闄� 6-宸茬粓姝� */
+ status?: number;
+ /** 闄勪欢 */
+ attachmentList?: AttachmentItem[];
+ /** 闄勪欢 blobId 鍒楄〃 */
+ blobIds?: number[];
+ remark?: string;
+ createTime?: string;
+ }
+
+ /** 闄勪欢椤� */
+ export interface AttachmentItem {
+ id: number;
+ name?: string;
+ url?: string;
+ uid?: string;
+ }
+
+ /** 鍛樺伐鍚堝悓鏌ヨ鍙傛暟 */
+ export interface EmployeeContractPageParam extends PageParam {
+ contractNo?: string;
+ employeeId?: number;
+ contractType?: number;
+ contractTermType?: number;
+ terminateStatus?: number;
+ status?: number;
+ /** 鍒版湡绫诲瀷锛�1-鍗冲皢鍒版湡 2-宸插埌鏈� */
+ expiryType?: number;
+ }
+
+ /** 瑙i櫎/缁堟鍚堝悓璇锋眰 */
+ export interface TerminateReq {
+ id: number;
+ terminateStatus: number;
+ terminateDate: string;
+ terminateReason: string;
+ }
+
+ /** 鍚堝悓瀵煎叆缁撴灉 */
+ export interface ImportResult {
+ /** 鏂板鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+ createNames?: string[];
+ /** 瑕嗙洊鏇存柊鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+ updateNames?: string[];
+ /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓哄憳宸ュ悕锛寁alue 涓哄け璐ュ師鍥� */
+ failureNames?: Record<string, string>;
+ }
+}
+
+/** 鑾峰彇鍛樺伐鍚堝悓鍒嗛〉鍒楄〃 */
+export function getEmployeeContractPage(
+ params: HrmEmployeeContractApi.EmployeeContractPageParam,
+) {
+ return requestClient.get<PageResult<HrmEmployeeContractApi.EmployeeContract>>(
+ '/hrm/employee-contract/page',
+ { params },
+ );
+}
+
+/** 鑾峰彇鍛樺伐鍚堝悓璇︽儏 */
+export function getEmployeeContract(id: number) {
+ return requestClient.get<HrmEmployeeContractApi.EmployeeContract>(
+ `/hrm/employee-contract/get?id=${id}`,
+ );
+}
+
+/** 鍒涘缓鍛樺伐鍚堝悓 */
+export function createEmployeeContract(data: HrmEmployeeContractApi.EmployeeContract) {
+ return requestClient.post('/hrm/employee-contract/create', data);
+}
+
+/** 鏇存柊鍛樺伐鍚堝悓 */
+export function updateEmployeeContract(data: HrmEmployeeContractApi.EmployeeContract) {
+ return requestClient.put('/hrm/employee-contract/update', data);
+}
+
+/** 瑙i櫎/缁堟鍛樺伐鍚堝悓 */
+export function terminateEmployeeContract(data: HrmEmployeeContractApi.TerminateReq) {
+ return requestClient.put('/hrm/employee-contract/terminate', data);
+}
+
+/** 鍒犻櫎鍛樺伐鍚堝悓 */
+export function deleteEmployeeContract(id: number) {
+ return requestClient.delete(`/hrm/employee-contract/delete?id=${id}`);
+}
+
+/** 鑾峰彇鍗冲皢鍒版湡鍚堝悓鏁伴噺 */
+export function getExpiringContractCount() {
+ return requestClient.get<number>('/hrm/employee-contract/expiring-count');
+}
+
+/** 涓嬭浇鍛樺伐鍚堝悓瀵煎叆妯℃澘 */
+export function importEmployeeContractTemplate() {
+ return requestClient.download('/hrm/employee-contract/import-template');
+}
+
+/** 瀵煎叆鍛樺伐鍚堝悓 */
+export function importEmployeeContract(
+ file: File,
+ updateSupport: boolean,
+) {
+ return requestClient.upload<HrmEmployeeContractApi.ImportResult>(
+ '/hrm/employee-contract/import',
+ { file, updateSupport },
+ );
+}
diff --git a/src/api/hrm/employee/index.ts b/src/api/hrm/employee/index.ts
index 9aeea4d..4c0360a 100644
--- a/src/api/hrm/employee/index.ts
+++ b/src/api/hrm/employee/index.ts
@@ -9,8 +9,8 @@
userId?: number;
deptId?: number;
deptName?: string;
- postId?: number;
- postName?: string;
+ postIds?: number[];
+ postNames?: string;
employeeNo?: string;
name?: string;
gender?: number;
@@ -18,12 +18,18 @@
phone?: string;
email?: string;
idCard?: string;
+ nation?: string;
+ marriageStatus?: string;
+ age?: number;
+ education?: string;
+ politicalStatus?: string;
hireDate?: string;
regularDate?: string;
leaveDate?: string;
employeeStatus?: number;
annualLeaveBalance?: number;
sickLeaveBalance?: number;
+ salaryStructureId?: number;
baseSalary?: number;
roleIds?: number[];
roleNames?: string[];
@@ -82,6 +88,16 @@
deptName?: string;
postName?: string;
}
+
+ /** 鍛樺伐瀵煎叆缁撴灉 */
+ export interface ImportResult {
+ /** 鍒涘缓鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+ createNames?: string[];
+ /** 鏇存柊鎴愬姛鐨勫憳宸ュ悕鏁扮粍 */
+ updateNames?: string[];
+ /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓哄憳宸ュ悕锛寁alue 涓哄け璐ュ師鍥� */
+ failureNames?: Record<string, string>;
+ }
}
/** 鏌ヨ鍛樺伐鍒嗛〉鍒楄〃 */
@@ -126,6 +142,19 @@
return requestClient.download('/hrm/employee/export-excel', { params });
}
+/** 涓嬭浇鍛樺伐瀵煎叆妯℃澘 */
+export function importEmployeeTemplate() {
+ return requestClient.download('/hrm/employee/import-template');
+}
+
+/** 瀵煎叆鍛樺伐 */
+export function importEmployee(file: File, updateSupport: boolean) {
+ return requestClient.upload<HrmEmployeeApi.ImportResult>(
+ '/hrm/employee/import',
+ { file, updateSupport },
+ );
+}
+
/** 鍒犻櫎鏁欒偛缁忓巻 */
export function deleteEducation(id: number) {
return requestClient.delete(`/hrm/employee/education/delete?id=${id}`);
@@ -139,4 +168,4 @@
/** 鍒犻櫎绱ф�ヨ仈绯讳汉 */
export function deleteEmergencyContact(id: number) {
return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`);
-}
\ No newline at end of file
+}
diff --git a/src/api/hrm/leave-type-config/index.ts b/src/api/hrm/leave-type-config/index.ts
index 4c58a3f..bf2131a 100644
--- a/src/api/hrm/leave-type-config/index.ts
+++ b/src/api/hrm/leave-type-config/index.ts
@@ -22,7 +22,17 @@
);
}
+/** 鏂板璇峰亣绫诲瀷閰嶇疆 */
+export function createLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
+ return requestClient.post<number>('/hrm/leave-type-config/create', data);
+}
+
/** 鏇存柊璇峰亣绫诲瀷閰嶇疆 */
export function updateLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
return requestClient.put('/hrm/leave-type-config/update', data);
+}
+
+/** 鍒犻櫎璇峰亣绫诲瀷閰嶇疆 */
+export function deleteLeaveTypeConfig(id: number) {
+ return requestClient.delete(`/hrm/leave-type-config/delete?id=${id}`);
}
\ No newline at end of file
diff --git a/src/api/hrm/leave/index.ts b/src/api/hrm/leave/index.ts
index e280286..ee16a80 100644
--- a/src/api/hrm/leave/index.ts
+++ b/src/api/hrm/leave/index.ts
@@ -7,8 +7,8 @@
/** 璇峰亣鐢宠 */
export interface Leave {
id?: number;
- employeeId?: number;
- employeeName?: string;
+ userId?: number;
+ userName?: string;
deptName?: string;
leaveType?: number;
startTime?: string;
@@ -21,6 +21,12 @@
processInstanceId?: string;
remark?: string;
createTime?: string;
+ }
+
+ /** 瀹℃壒娴佺▼ */
+ export interface ApproveProcess {
+ key: string;
+ name: string;
}
}
@@ -53,8 +59,10 @@
}
/** 鎻愪氦璇峰亣鐢宠锛堝彂璧峰鎵癸級 */
-export function submitLeave(id: number) {
- return requestClient.post(`/hrm/leave/submit?id=${id}`);
+export function submitLeave(id: number, processDefinitionKey: string) {
+ return requestClient.post('/hrm/leave/submit', null, {
+ params: { id, processDefinitionKey },
+ });
}
/** 鎾ら攢璇峰亣鐢宠 */
@@ -64,5 +72,7 @@
/** 鑾峰彇鍙敤娴佺▼瀹氫箟鍒楄〃 */
export function getLeaveApproveProcessDefinitionList() {
- return requestClient.get<any[]>('/hrm/leave/approve-process-definition-list');
+ return requestClient.get<HrmLeaveApi.ApproveProcess[]>(
+ '/hrm/leave/approve-process-definition-list',
+ );
}
\ No newline at end of file
diff --git a/src/api/hrm/overtime/index.ts b/src/api/hrm/overtime/index.ts
new file mode 100644
index 0000000..37f5b15
--- /dev/null
+++ b/src/api/hrm/overtime/index.ts
@@ -0,0 +1,89 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmOvertimeApi {
+ /** 鍔犵彮鐢宠 */
+ export interface Overtime {
+ id?: number;
+ employeeId?: number;
+ employeeName?: string;
+ employeeNo?: string;
+ deptName?: string;
+ postName?: string;
+ overtimeDate?: string;
+ startTime?: string;
+ endTime?: string;
+ duration?: number;
+ overtimeType?: number;
+ crossDay?: boolean;
+ mealDeduction?: boolean;
+ mealDeductionHours?: number;
+ reason?: string;
+ relatedType?: number;
+ relatedCode?: string;
+ workContent?: string;
+ status?: number;
+ processInstanceId?: string;
+ createTime?: string;
+ }
+
+ export interface ApproveProcess {
+ key: string;
+ name: string;
+ }
+}
+
+/** 鏌ヨ鍔犵彮鐢宠鍒嗛〉鍒楄〃 */
+export function getOvertimePage(params: PageParam) {
+ return requestClient.get<PageResult<HrmOvertimeApi.Overtime>>(
+ '/hrm/overtime/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ鍔犵彮鐢宠璇︽儏 */
+export function getOvertime(id: number) {
+ return requestClient.get<HrmOvertimeApi.Overtime>(
+ `/hrm/overtime/get?id=${id}`,
+ );
+}
+
+/** 鑾峰彇褰撳墠鐢宠浜哄叧鑱旂殑鍛樺伐妗f */
+export function getCurrentOvertimeApplicant() {
+ return requestClient.get<HrmOvertimeApi.Overtime>('/hrm/overtime/current-applicant');
+}
+
+/** 鏂板鍔犵彮鐢宠锛堣崏绋匡級 */
+export function createOvertime(data: HrmOvertimeApi.Overtime) {
+ return requestClient.post('/hrm/overtime/create', data);
+}
+
+/** 鏇存柊鍔犵彮鐢宠 */
+export function updateOvertime(data: HrmOvertimeApi.Overtime) {
+ return requestClient.put('/hrm/overtime/update', data);
+}
+
+/** 鍒犻櫎鍔犵彮鐢宠 */
+export function deleteOvertime(id: number) {
+ return requestClient.delete(`/hrm/overtime/delete?id=${id}`);
+}
+
+/** 鎻愪氦鍔犵彮鐢宠锛堝彂璧峰鎵癸級 */
+export function submitOvertime(id: number, processDefinitionKey: string) {
+ return requestClient.post('/hrm/overtime/submit', null, {
+ params: { id, processDefinitionKey },
+ });
+}
+
+/** 鎾ら攢鍔犵彮鐢宠 */
+export function cancelOvertime(id: number) {
+ return requestClient.post(`/hrm/overtime/cancel?id=${id}`);
+}
+
+/** 鑾峰彇鍗忓悓鍔炲叕涓殑鍙敤鍔犵彮瀹℃壒娴佺▼ */
+export function getOvertimeApproveProcessDefinitionList() {
+ return requestClient.get<HrmOvertimeApi.ApproveProcess[]>(
+ '/hrm/overtime/approve-process-definition-list',
+ );
+}
diff --git a/src/api/hrm/salary/calculation/index.ts b/src/api/hrm/salary/calculation/index.ts
index 4aa6f4c..76d4fdd 100644
--- a/src/api/hrm/salary/calculation/index.ts
+++ b/src/api/hrm/salary/calculation/index.ts
@@ -26,6 +26,8 @@
actualSalary?: number;
workDays?: number;
overtimeHours?: number;
+ /** 鎺掔彮鎬诲伐鏃讹紙灏忔椂锛� */
+ totalWorkHours?: number;
status?: number;
statusName?: string;
remark?: string;
diff --git a/src/api/hrm/salary/structure/index.ts b/src/api/hrm/salary/structure/index.ts
new file mode 100644
index 0000000..20a25b8
--- /dev/null
+++ b/src/api/hrm/salary/structure/index.ts
@@ -0,0 +1,50 @@
+import { requestClient } from '#/api/request';
+
+export namespace HrmSalaryStructureApi {
+ export interface SalaryStructure {
+ id?: number;
+ name?: string;
+ overtimePayRatio?: number;
+ restDayOvertimePayRatio?: number;
+ legalHolidayOvertimePayRatio?: number;
+ status?: number;
+ remark?: string;
+ createTime?: string;
+ }
+
+ export interface SalaryStructureQueryParam {
+ name?: string;
+ status?: number;
+ }
+}
+
+export function getSalaryStructureList(
+ params?: HrmSalaryStructureApi.SalaryStructureQueryParam,
+) {
+ return requestClient.get<HrmSalaryStructureApi.SalaryStructure[]>(
+ '/hrm/salary/structure/list',
+ { params },
+ );
+}
+
+export function getSalaryStructure(id: number) {
+ return requestClient.get<HrmSalaryStructureApi.SalaryStructure>(
+ `/hrm/salary/structure/get?id=${id}`,
+ );
+}
+
+export function createSalaryStructure(
+ data: HrmSalaryStructureApi.SalaryStructure,
+) {
+ return requestClient.post('/hrm/salary/structure/create', data);
+}
+
+export function updateSalaryStructure(
+ data: HrmSalaryStructureApi.SalaryStructure,
+) {
+ return requestClient.put('/hrm/salary/structure/update', data);
+}
+
+export function deleteSalaryStructure(id: number) {
+ return requestClient.delete(`/hrm/salary/structure/delete?id=${id}`);
+}
diff --git a/src/api/hrm/schedule/index.ts b/src/api/hrm/schedule/index.ts
new file mode 100644
index 0000000..100b8b5
--- /dev/null
+++ b/src/api/hrm/schedule/index.ts
@@ -0,0 +1,43 @@
+import { requestClient } from '#/api/request';
+
+export namespace HrmEmployeeScheduleApi {
+ /** 鎺掔彮璁板綍 */
+ export interface EmployeeSchedule {
+ id?: number;
+ employeeId?: number;
+ employeeName?: string;
+ employeeNo?: string;
+ /** 鎺掔彮鏃ユ湡 yyyy-MM-dd */
+ date?: string;
+ shiftId?: number;
+ shiftName?: string;
+ shiftColor?: string;
+ remark?: string;
+ }
+
+ /** 淇濆瓨椤癸紙鏁存湀鍏ㄩ噺鎻愪氦锛宻hiftId 涓虹┖琛ㄧず浼戞伅/娓呴櫎锛� */
+ export interface ScheduleItem {
+ employeeId: number;
+ /** 鎺掔彮鏃ユ湡 yyyy-MM-dd */
+ date: string;
+ shiftId?: number | null;
+ remark?: string;
+ }
+}
+
+/** 鏌ヨ鏌愭湀鎺掔彮鍒楄〃 */
+export function getEmployeeScheduleList(params: {
+ yearMonth: string;
+ deptId?: number;
+ employeeId?: number;
+}) {
+ return requestClient.get<HrmEmployeeScheduleApi.EmployeeSchedule[]>(
+ '/hrm/employee-schedule/list',
+ { params },
+ );
+}
+
+/** 鎵归噺淇濆瓨鎺掔彮锛堟暣鏈堣鐩栧紡锛氭湁鐝 upsert锛屾棤鐝鍒犻櫎锛� */
+export function saveEmployeeSchedule(data: HrmEmployeeScheduleApi.ScheduleItem[]) {
+ return requestClient.post('/hrm/employee-schedule/save', data);
+}
diff --git a/src/api/hrm/shift/index.ts b/src/api/hrm/shift/index.ts
new file mode 100644
index 0000000..5988198
--- /dev/null
+++ b/src/api/hrm/shift/index.ts
@@ -0,0 +1,62 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace HrmShiftApi {
+ /** 鐝瀹氫箟 */
+ export interface Shift {
+ id?: number;
+ name?: string;
+ /** 涓婄彮鏃堕棿 HH:mm:ss */
+ startTime?: string;
+ /** 涓嬬彮鏃堕棿 HH:mm:ss */
+ endTime?: string;
+ /** 鏍囧噯宸ユ椂锛堝皬鏃讹級 */
+ workHours?: number;
+ /** 鏄惁璺ㄥぉ */
+ crossDay?: boolean;
+ /** 鏍囩棰滆壊锛堝崄鍏繘鍒讹級 */
+ color?: string;
+ /** 鐘舵�侊細0-鍚敤锛�1-绂佺敤 */
+ status?: number;
+ remark?: string;
+ createTime?: string;
+ }
+}
+
+/** 鏌ヨ鐝鍒嗛〉鍒楄〃 */
+export function getShiftPage(params: PageParam) {
+ return requestClient.get<PageResult<HrmShiftApi.Shift>>('/hrm/shift/page', {
+ params,
+ });
+}
+
+/** 鏌ヨ鐝鍒楄〃锛堝惎鐢ㄧ姸鎬侊紝渚涙帓鐝〉閫夋嫨锛� */
+export function getShiftList() {
+ return requestClient.get<HrmShiftApi.Shift[]>('/hrm/shift/list');
+}
+
+/** 鏌ヨ鐝璇︽儏 */
+export function getShift(id: number) {
+ return requestClient.get<HrmShiftApi.Shift>(`/hrm/shift/get?id=${id}`);
+}
+
+/** 鏂板鐝 */
+export function createShift(data: HrmShiftApi.Shift) {
+ return requestClient.post('/hrm/shift/create', data);
+}
+
+/** 鏇存柊鐝 */
+export function updateShift(data: HrmShiftApi.Shift) {
+ return requestClient.put('/hrm/shift/update', data);
+}
+
+/** 鍒犻櫎鐝 */
+export function deleteShift(id: number) {
+ return requestClient.delete(`/hrm/shift/delete?id=${id}`);
+}
+
+/** 瀵煎嚭鐝 Excel */
+export function exportShift(params: any) {
+ return requestClient.download('/hrm/shift/export-excel', { params });
+}
diff --git a/src/api/mdm/item/index.ts b/src/api/mdm/item/index.ts
index f169a1f..9b0d538 100644
--- a/src/api/mdm/item/index.ts
+++ b/src/api/mdm/item/index.ts
@@ -107,7 +107,25 @@
return requestClient.put('/mdm/item/update-status', { id, status });
}
+/** 鏇存柊鐗╂枡鍚屾鐘舵�� */
+export function updateItemSync(id: number, sync: boolean) {
+ return requestClient.put('/mdm/item/update-sync', { id, sync });
+}
+
/** 瀵煎嚭鐗╂枡 */
export function exportItem(params: Partial<MdmItemApi.Item>) {
return requestClient.download('/mdm/item/export-excel', { params });
}
+
+/** 涓嬭浇鐗╂枡瀵煎叆妯℃澘 */
+export function importItemTemplate() {
+ return requestClient.download('/mdm/item/get-import-template');
+}
+
+/** 瀵煎叆鐗╂枡 */
+export function importItem(file: File, updateSupport: boolean) {
+ return requestClient.upload('/mdm/item/import', {
+ file,
+ updateSupport,
+ });
+}
diff --git a/src/api/mdm/unit/index.ts b/src/api/mdm/unit/index.ts
index 6b437ba..0a5eb6c 100644
--- a/src/api/mdm/unit/index.ts
+++ b/src/api/mdm/unit/index.ts
@@ -47,3 +47,21 @@
export function deleteUnit(id: number) {
return requestClient.delete(`/mdm/unit/delete?id=${id}`);
}
+
+/** 瀵煎嚭璁¢噺鍗曚綅 */
+export function exportUnit(params: Record<string, any>) {
+ return requestClient.download('/mdm/unit/export-excel', { params });
+}
+
+/** 涓嬭浇璁¢噺鍗曚綅瀵煎叆妯℃澘 */
+export function importUnitTemplate() {
+ return requestClient.download('/mdm/unit/get-import-template');
+}
+
+/** 瀵煎叆璁¢噺鍗曚綅 */
+export function importUnit(file: File, updateSupport: boolean) {
+ return requestClient.upload('/mdm/unit/import', {
+ file,
+ updateSupport,
+ });
+}
diff --git a/src/api/mes/dv/metering/index.ts b/src/api/mes/dv/metering/index.ts
new file mode 100644
index 0000000..138dd73
--- /dev/null
+++ b/src/api/mes/dv/metering/index.ts
@@ -0,0 +1,92 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesDvMeteringApi {
+ /** MES 璁¢噺鍣ㄥ叿 */
+ export interface Metering {
+ id?: number; // 涓婚敭ID
+ code?: string; // 璁¢噺鍣ㄥ叿缂栫爜
+ name?: string; // 璁¢噺鍣ㄥ叿鍚嶇О
+ specification?: string; // 瑙勬牸鍨嬪彿
+ brand?: string; // 鍝佺墝/鐢熶骇鍘傚
+ category?: number; // 鍣ㄥ叿绫诲埆锛堝瓧鍏革級
+ precision?: string; // 绮惧害
+ range?: string; // 閲忕▼
+ deptId?: number; // 浣跨敤閮ㄩ棬缂栧彿
+ deptName?: string; // 浣跨敤閮ㄩ棬鍚嶇О
+ chargeUserId?: number; // 璐d换浜虹紪鍙�
+ chargeUserNickname?: string; // 璐d换浜烘樀绉�
+ checkCycleCount?: number; // 鏍″噯鍛ㄦ湡锛堟湀锛�
+ useDate?: string; // 鎶曞叆浣跨敤鏃ユ湡
+ lastCheckDate?: string; // 涓婃妫�娴嬫棩鏈�
+ nextCheckDate?: string; // 涓嬫妫�娴嬫棩鏈燂紙鍒版湡鏃ワ級
+ status?: number; // 鐘舵�侊紙瀛楀吀锛�
+ expireStatus?: number; // 鍒版湡鐘舵�侊細1 宸茶繃鏈� / 2 鍗冲皢鍒版湡 / 3 姝e父
+ expireDays?: number; // 璺濆埌鏈熷ぉ鏁�
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+}
+
+/** 璁¢噺鍣ㄥ叿瀵煎叆缁撴灉 */
+export interface ImportResult {
+ /** 鍒涘缓鎴愬姛鐨勮閲忓櫒鍏风紪鐮� */
+ createList?: string[];
+ /** 鏇存柊鎴愬姛鐨勮閲忓櫒鍏风紪鐮� */
+ updateList?: string[];
+ /** 瀵煎叆澶辫触闆嗗悎锛宬ey 涓鸿閲忓櫒鍏风紪鐮佹垨琛屽彿锛寁alue 涓哄け璐ュ師鍥� */
+ failureList?: Record<string, string>;
+}
+
+/** 鏌ヨ璁¢噺鍣ㄥ叿鍒嗛〉 */
+export function getMeteringPage(params: PageParam) {
+ return requestClient.get<PageResult<MesDvMeteringApi.Metering>>(
+ '/mes/dv/metering/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ璁¢噺鍣ㄥ叿璇︽儏 */
+export function getMetering(id: number) {
+ return requestClient.get<MesDvMeteringApi.Metering>(
+ `/mes/dv/metering/get?id=${id}`,
+ );
+}
+
+/** 鏂板璁¢噺鍣ㄥ叿 */
+export function createMetering(data: MesDvMeteringApi.Metering) {
+ return requestClient.post<number>('/mes/dv/metering/create', data);
+}
+
+/** 淇敼璁¢噺鍣ㄥ叿 */
+export function updateMetering(data: MesDvMeteringApi.Metering) {
+ return requestClient.put('/mes/dv/metering/update', data);
+}
+
+/** 鍒犻櫎璁¢噺鍣ㄥ叿 */
+export function deleteMetering(id: number) {
+ return requestClient.delete(`/mes/dv/metering/delete?id=${id}`);
+}
+
+/** 瀵煎嚭璁¢噺鍣ㄥ叿 */
+export function exportMetering(params: any) {
+ return requestClient.download('/mes/dv/metering/export-excel', { params });
+}
+
+/** 鏌ヨ璁¢噺鍣ㄥ叿绮剧畝鍒楄〃锛堜笅鎷夐�夐」锛� */
+export function getMeteringSimpleList() {
+ return requestClient.get<MesDvMeteringApi.Metering[]>(
+ '/mes/dv/metering/simple-list',
+ );
+}
+
+/** 涓嬭浇璁¢噺鍣ㄥ叿瀵煎叆妯℃澘 */
+export function importMeteringTemplate() {
+ return requestClient.download('/mes/dv/metering/get-import-template');
+}
+
+/** 瀵煎叆璁¢噺鍣ㄥ叿 */
+export function importMetering(file: File) {
+ return requestClient.upload<ImportResult>('/mes/dv/metering/import', { file });
+}
diff --git a/src/api/mes/dv/meteringcheck/index.ts b/src/api/mes/dv/meteringcheck/index.ts
new file mode 100644
index 0000000..e8995ec
--- /dev/null
+++ b/src/api/mes/dv/meteringcheck/index.ts
@@ -0,0 +1,60 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesDvMeteringCheckApi {
+ /** MES 璁¢噺妫�娴嬭褰� */
+ export interface MeteringCheck {
+ id?: number; // 涓婚敭ID
+ meteringId?: number; // 璁¢噺鍣ㄥ叿ID
+ meteringCode?: string; // 璁¢噺鍣ㄥ叿缂栫爜
+ meteringName?: string; // 璁¢噺鍣ㄥ叿鍚嶇О
+ checkCode?: string; // 妫�娴嬬紪鍙�
+ checkDate?: string; // 妫�娴嬫棩鏈�
+ checkOrg?: string; // 妫�娴嬫満鏋�
+ checkResult?: number; // 妫�娴嬬粨璁猴紙瀛楀吀锛�
+ certNo?: string; // 璇佷功缂栧彿
+ checkUserId?: number; // 妫�娴嬩汉缂栧彿
+ checkUserNickname?: string; // 妫�娴嬩汉鏄电О
+ nextCheckDate?: string; // 涓嬫妫�娴嬫棩鏈�
+ remark?: string; // 澶囨敞
+ createTime?: Date; // 鍒涘缓鏃堕棿
+ }
+}
+
+/** 鏌ヨ璁¢噺妫�娴嬭褰曞垎椤� */
+export function getMeteringCheckPage(params: PageParam) {
+ return requestClient.get<PageResult<MesDvMeteringCheckApi.MeteringCheck>>(
+ '/mes/dv/metering-check/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ璁¢噺妫�娴嬭褰曡鎯� */
+export function getMeteringCheck(id: number) {
+ return requestClient.get<MesDvMeteringCheckApi.MeteringCheck>(
+ `/mes/dv/metering-check/get?id=${id}`,
+ );
+}
+
+/** 鏂板璁¢噺妫�娴嬭褰� */
+export function createMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) {
+ return requestClient.post<number>('/mes/dv/metering-check/create', data);
+}
+
+/** 淇敼璁¢噺妫�娴嬭褰� */
+export function updateMeteringCheck(data: MesDvMeteringCheckApi.MeteringCheck) {
+ return requestClient.put('/mes/dv/metering-check/update', data);
+}
+
+/** 鍒犻櫎璁¢噺妫�娴嬭褰� */
+export function deleteMeteringCheck(id: number) {
+ return requestClient.delete(`/mes/dv/metering-check/delete?id=${id}`);
+}
+
+/** 瀵煎嚭璁¢噺妫�娴嬭褰� */
+export function exportMeteringCheck(params: any) {
+ return requestClient.download('/mes/dv/metering-check/export-excel', {
+ params,
+ });
+}
diff --git a/src/api/mes/md/item/index.ts b/src/api/mes/md/item/index.ts
index f2a23c0..6ad491e 100644
--- a/src/api/mes/md/item/index.ts
+++ b/src/api/mes/md/item/index.ts
@@ -54,13 +54,6 @@
return requestClient.put('/mes/md/item/update', data);
}
-/** 淇敼鐗╂枡浜у搧鐘舵�� */
-export function updateItemStatus(id: number, status: number) {
- return requestClient.put('/mes/md/item/update-status', undefined, {
- params: { id, status },
- });
-}
-
/** 鍒犻櫎鐗╂枡浜у搧 */
export function deleteItem(id: number) {
return requestClient.delete(`/mes/md/item/delete?id=${id}`);
diff --git a/src/api/mes/md/item/type/index.ts b/src/api/mes/md/item/type/index.ts
index e9367ee..86dc13d 100644
--- a/src/api/mes/md/item/type/index.ts
+++ b/src/api/mes/md/item/type/index.ts
@@ -52,3 +52,21 @@
export function deleteItemType(id: number) {
return requestClient.delete(`/mes/md/item-type/delete?id=${id}`);
}
+
+/** 瀵煎嚭鐗╂枡浜у搧鍒嗙被 */
+export function exportItemType(params: Record<string, any>) {
+ return requestClient.download('/mes/md/item-type/export-excel', { params });
+}
+
+/** 涓嬭浇鐗╂枡浜у搧鍒嗙被瀵煎叆妯℃澘 */
+export function importItemTypeTemplate() {
+ return requestClient.download('/mes/md/item-type/get-import-template');
+}
+
+/** 瀵煎叆鐗╂枡浜у搧鍒嗙被 */
+export function importItemType(file: File, updateSupport: boolean) {
+ return requestClient.upload('/mes/md/item-type/import', {
+ file,
+ updateSupport,
+ });
+}
diff --git a/src/api/mes/md/workstation/worker/index.ts b/src/api/mes/md/workstation/worker/index.ts
index 3bd4ab9..ce60cb5 100644
--- a/src/api/mes/md/workstation/worker/index.ts
+++ b/src/api/mes/md/workstation/worker/index.ts
@@ -1,4 +1,5 @@
import { requestClient } from '#/api/request';
+import type { SystemUserApi } from '#/api/system/user';
export namespace MesMdWorkstationWorkerApi {
/** MES 宸ヤ綔绔欎汉鍔涜祫婧� */
@@ -21,6 +22,17 @@
);
}
+/** 鑾峰緱宸ヤ綔绔欏搴斿矖浣嶇殑鍛樺伐鍒楄〃 */
+export function getUserListByWorkstationId(params: { workstationId: number }) {
+ if (!params || !params.workstationId) {
+ return Promise.resolve([]);
+ }
+ return requestClient.get<SystemUserApi.UserSimple[]>(
+ '/mes/md-workstation-worker/list-user-by-workstation',
+ { params },
+ );
+}
+
/** 鏂板宸ヤ綔绔欎汉鍔涜祫婧� */
export function createWorkstationWorker(
data: MesMdWorkstationWorkerApi.WorkstationWorker,
diff --git a/src/api/mes/pd/document/index.ts b/src/api/mes/pd/document/index.ts
index 9ee756c..73d4d5e 100644
--- a/src/api/mes/pd/document/index.ts
+++ b/src/api/mes/pd/document/index.ts
@@ -17,6 +17,8 @@
documentVersion?: string;
/** 鏂囦欢鍚� */
fileName?: string;
+ /** 鏂囦欢鍦板潃 */
+ fileUrl?: string;
/** 涓婁紶浜猴紙鐢ㄦ埛ID锛� */
uploadUserId?: number;
/** 涓婁紶浜哄悕绉� */
diff --git a/src/api/mes/pd/project/index.ts b/src/api/mes/pd/project/index.ts
index 40e34ab..1e927d5 100644
--- a/src/api/mes/pd/project/index.ts
+++ b/src/api/mes/pd/project/index.ts
@@ -32,6 +32,10 @@
remark?: string;
/** 鍒涘缓鏃堕棿 */
createTime?: number;
+ /** BPM 娴佺▼瀹炰緥缂栧彿 */
+ processInstanceId?: string;
+ /** 寰呭鏍告枃浠舵暟锛坅uditStatus=0 鐨勫鏍歌褰曟暟锛� */
+ pendingAuditCount?: number;
}
export interface PageParams extends PageParam {
@@ -87,13 +91,20 @@
});
}
-/** 瀹℃牳锛堝緟瀹℃牳鈫掑緟褰掓。锛� */
-export function auditPdProject(id: number) {
- return requestClient.put('/mes/pd/project/audit', undefined, {
- params: { id },
+/** 鎻愪氦瀹℃壒锛堝緟瀹℃牳鈫掑鎵逛腑锛� */
+export function submitPdProjectApproval(id: number, processDefinitionKey: string) {
+ return requestClient.put('/mes/pd/project/submit-approval', undefined, {
+ params: { id, processDefinitionKey },
});
}
+/** 鑾峰緱瀹℃壒娴佺▼鍒楄〃 */
+export function getPdProjectApproveProcessList() {
+ return requestClient.get<Array<{ id: string; name: string; key: string }>>(
+ '/mes/pd/project/approve-process-list',
+ );
+}
+
/** 褰掓。锛堝緟褰掓。鈫掑凡褰掓。锛夛紝鍚庣鑷姩鍒涘缓褰掓。鍙拌处璁板綍 */
export function archivePdProject(id: number) {
return requestClient.put('/mes/pd/project/archive', undefined, {
diff --git a/src/api/mes/pro/feedback/index.ts b/src/api/mes/pro/feedback/index.ts
index 7666071..0580b85 100644
--- a/src/api/mes/pro/feedback/index.ts
+++ b/src/api/mes/pro/feedback/index.ts
@@ -1,8 +1,23 @@
-锘縤mport type { PageParam, PageResult } from '@vben/request';
+import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProFeedbackApi {
+ export interface FeedbackSubmitReqVO {
+ id: number;
+ produceDate?: string;
+ expireDate?: string;
+ receiptDate?: string;
+ vendorId?: number;
+ clientId?: number;
+ purchaseOrderCode?: string;
+ salesOrderCode?: string;
+ toolId?: number;
+ moldId?: number;
+ lotNumber?: string;
+ qualityStatus?: number;
+ }
+
/** MES 鐢熶骇鎶ュ伐 */
export interface Feedback {
id?: number;
@@ -47,6 +62,21 @@
remark?: string; // 澶囨敞
creator?: string; // 鍒涘缓浜�
createTime?: number; // 鍒涘缓鏃堕棿
+ processInstanceId?: string; // BPM 娴佺▼瀹炰緥缂栧彿
+ processDefinitionKey?: string; // BPM 娴佺▼瀹氫箟 Key
+ paramValues?: ParamValue[];
+ }
+
+ export interface ParamValue {
+ detailId: number;
+ value?: string;
+ // ---- 鍥炴樉瀛楁锛堟姤宸ヨ鎯�/鍒楄〃杩斿洖锛� ----
+ paramCode?: string;
+ paramName?: string;
+ paramType?: number;
+ unitMeasureName?: string;
+ paramValue?: string;
+ required?: boolean;
}
/** MES 鐢熶骇鎶ュ伐鍒嗛〉鏌ヨ鍙傛暟 */
@@ -100,8 +130,8 @@
}
/** 鎻愪氦鐢熶骇鎶ュ伐 */
-export function submitFeedback(id: number) {
- return requestClient.put(`/mes/pro/feedback/submit?id=${id}`);
+export function submitFeedback(data: MesProFeedbackApi.FeedbackSubmitReqVO) {
+ return requestClient.put(`/mes/pro/feedback/submit`, data);
}
/** 椹冲洖鐢熶骇鎶ュ伐 */
@@ -146,9 +176,8 @@
/** 蹇�熸姤宸ヨ姹� */
export interface QuickCreateReq {
taskId: number;
+ paramValues?: MesProFeedbackApi.ParamValue[];
feedbackQuantity: number;
- qualifiedQuantity?: number;
- unqualifiedQuantity?: number;
remark?: string;
}
@@ -157,14 +186,14 @@
feedbackId?: number;
taskStatus?: number;
workOrderStatus?: number;
+ /** 鎶ュ伐鍚庡墿浣欏彲鎶ュ伐鏁伴噺 */
+ reportableQuantity?: number;
}
/** 鎵归噺鎶ュ伐璇锋眰 */
export interface BatchCreateReq {
taskIds: number[];
feedbackQuantity: number;
- qualifiedQuantity?: number;
- unqualifiedQuantity?: number;
remark?: string;
}
diff --git a/src/api/mes/pro/mps/index.ts b/src/api/mes/pro/mps/index.ts
index c8b4b8e..0ead6e3 100644
--- a/src/api/mes/pro/mps/index.ts
+++ b/src/api/mes/pro/mps/index.ts
@@ -80,9 +80,9 @@
}
/** 鍒犻櫎 MPS */
-export function deleteMps(ids: number[]) {
+export function deleteMps(id: number) {
return requestClient.delete('/mes/pro-mps/delete', {
- params: { ids: ids.join(',') },
+ params: { id },
});
}
diff --git a/src/api/mes/pro/processdesign/processparam/index.ts b/src/api/mes/pro/processdesign/processparam/index.ts
new file mode 100644
index 0000000..7ddf303
--- /dev/null
+++ b/src/api/mes/pro/processdesign/processparam/index.ts
@@ -0,0 +1,52 @@
+import { requestClient } from '#/api/request';
+
+export namespace MesProProcessParamApi {
+ /** 宸ュ簭-宸ヨ壓鍙傛暟缁戝畾 */
+ export interface ProcessParam {
+ id?: number;
+ processId?: number;
+ processParamId?: number;
+ paramCode?: string;
+ paramName?: string;
+ paramType?: number;
+ unitMeasureId?: number;
+ unitMeasureName?: string;
+ required?: boolean;
+ sort?: number;
+ remark?: string;
+ }
+
+ /** 淇濆瓨缁戝畾椤� */
+ export interface SaveItem {
+ processParamId: number;
+ required?: boolean;
+ sort?: number;
+ remark?: string;
+ }
+}
+
+/** 鏌ヨ宸ュ簭缁戝畾鐨勫伐鑹哄弬鏁板垪琛� */
+export function getProcessParamListByProcess(processId: number) {
+ return requestClient.get<MesProProcessParamApi.ProcessParam[]>(
+ '/mes/pro/process-param/list-by-process',
+ { params: { processId } },
+ );
+}
+
+/** 鍏ㄩ噺瑕嗙洊淇濆瓨宸ュ簭缁戝畾鐨勫伐鑹哄弬鏁� */
+export function updateProcessParams(
+ processId: number,
+ items: MesProProcessParamApi.SaveItem[],
+) {
+ return requestClient.post('/mes/pro/process-param/update-bindings', {
+ processId,
+ items,
+ });
+}
+
+/** 鏌ヨ鏌愬伐鑹哄弬鏁拌鍝簺宸ュ簭缁戝畾 */
+export function getProcessListByParam(paramId: number) {
+ return requestClient.get<
+ { id: number; code?: string; name?: string; attention?: string }[]
+ >('/mes/pro/process-param/list-by-param', { params: { paramId } });
+}
diff --git a/src/api/mes/pro/route/process/index.ts b/src/api/mes/pro/route/process/index.ts
index c207284..a08056f 100644
--- a/src/api/mes/pro/route/process/index.ts
+++ b/src/api/mes/pro/route/process/index.ts
@@ -15,6 +15,7 @@
id?: number;
routeId?: number;
processId?: number;
+ paramTemplateId?: number;
processCode?: string;
processName?: string;
sort?: number;
diff --git a/src/api/mes/pro/task/index.ts b/src/api/mes/pro/task/index.ts
index 515185b..13fb333 100644
--- a/src/api/mes/pro/task/index.ts
+++ b/src/api/mes/pro/task/index.ts
@@ -1,8 +1,21 @@
-锘縤mport type { PageParam, PageResult } from '@vben/request';
+import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProTaskApi {
+ export interface PauseRecord {
+ id: number;
+ pauseTime: string;
+ pauseUserId: number;
+ pauseUserName: string;
+ reason: string;
+ remark?: string;
+ resumeTime?: string;
+ resumeUserId?: number;
+ resumeUserName?: string;
+ taskId: number;
+ }
+
/** MES 鐢熶骇浠诲姟 */
export interface Task {
id?: number;
@@ -16,6 +29,7 @@
workstationName?: string; // 宸ヤ綔绔欏悕绉�
routeId?: number; // 宸ヨ壓璺嚎缂栧彿
processId?: number; // 宸ュ簭缂栧彿
+ paramTemplateId?: number; // 宸ヨ壓鍙傛暟妯℃澘缂栧彿
processName?: string; // 宸ュ簭鍚嶇О
itemId?: number; // 浜у搧鐗╂枡缂栧彿
itemCode?: string; // 浜у搧缂栫爜
@@ -41,6 +55,10 @@
checkFlag?: boolean; // 鏄惁璐ㄦ锛堟淳鐢熻嚜宸ヨ壓璺嚎宸ュ簭锛�
backflushFlag?: boolean; // 鏄惁鍊掑啿锛堟淳鐢熻嚜宸ヨ壓璺嚎宸ュ簭锛�
remark?: string; // 澶囨敞
+ pauseRecords?: PauseRecord[]; // 鍋滃伐璁板綍
+ reportableQuantity?: number; // 鍙姤宸ユ暟閲�
+ assigneeUserIds?: number[]; // 鎶ュ伐浜�
+ feedbackApprove?: boolean; // 鏄惁鎶ュ伐瀹℃壒锛堟淳鐢熻嚜宸ュ崟锛�
}
/** MES 鐢熶骇浠诲姟鍒嗛〉鏌ヨ鍙傛暟 */
@@ -174,3 +192,20 @@
{ params: { workstationId, quantity } },
);
}
+
+/** 鐢熶骇浠诲姟鍋滃伐璇锋眰鍙傛暟 */
+export interface MesProTaskPauseReqVO {
+ id: number;
+ reason: string;
+ remark?: string;
+}
+
+/** 鐢熶骇浠诲姟鍋滃伐 */
+export function pauseTask(data: MesProTaskPauseReqVO) {
+ return requestClient.put('/mes/pro/task/pause', data);
+}
+
+/** 鐢熶骇浠诲姟澶嶅伐 */
+export function resumeTask(data: { id: number }) {
+ return requestClient.put('/mes/pro/task/resume', data);
+}
diff --git a/src/api/mes/pro/workorder/index.ts b/src/api/mes/pro/workorder/index.ts
index 8dacc2e..68b8037 100644
--- a/src/api/mes/pro/workorder/index.ts
+++ b/src/api/mes/pro/workorder/index.ts
@@ -1,4 +1,4 @@
-锘縤mport type { PageParam, PageResult } from '@vben/request';
+import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
@@ -34,8 +34,15 @@
finishDate?: number; // 瀹屾垚鏃堕棿
cancelDate?: number; // 鍙栨秷鏃堕棿
status?: number; // 璁㈠崟鐘舵��
+ scheduleStatus?: number; // 鎺掍骇鐘舵��
+ productionStatus?: number; // 鐢熶骇鐘舵��
+ processProgressList?: ProcessProgress[]; // 宸ュ簭鐢熶骇杩涘害
+ completionProgress?: number; // 瀹屾垚杩涘害
remark?: string; // 澶囨敞
createTime?: number; // 鍒涘缓鏃堕棿
+ feedbackApprove?: boolean; // 鏄惁鎶ュ伐瀹℃壒
+ processDefinitionKey?: string; // 鎶ュ伐瀹℃壒娴佺▼瀹氫箟 Key
+ feedbackRole?: string; // 鎶ュ伐瑙掕壊
}
/** MES 鐢熶骇璁㈠崟鍒嗛〉鏌ヨ鍙傛暟 */
@@ -56,6 +63,14 @@
return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>(
'/mes/pro/work-order/page',
{ params },
+ );
+}
+
+/** 鑾峰緱鐢熶骇宸ュ崟璋冨害鐘舵�佸垎椤� */
+export function getWorkOrderSchedulePage(params: MesProWorkOrderApi.PageParams) {
+ return requestClient.get<PageResult<MesProWorkOrderApi.WorkOrder>>(
+ '/mes/pro/work-order/schedule-page',
+ { params },
);
}
@@ -147,3 +162,10 @@
`/mes/pro/work-order/check-auto-finish/${id}`,
);
}
+
+/** 鑾峰緱鐢熶骇璁㈠崟瀹℃壒娴佺▼鍒楄〃 */
+export function getWorkOrderApproveProcessList() {
+ return requestClient.get<Array<{ id: string; name: string; key: string }>>(
+ '/mes/pro/work-order/approve-process-list',
+ );
+}
diff --git a/src/api/mes/pro/workorder/process/index.ts b/src/api/mes/pro/workorder/process/index.ts
index cc3630b..e49e863 100644
--- a/src/api/mes/pro/workorder/process/index.ts
+++ b/src/api/mes/pro/workorder/process/index.ts
@@ -19,6 +19,7 @@
processId?: number; // 宸ュ簭缂栧彿
processCode?: string; // 宸ュ簭缂栫爜
processName?: string; // 宸ュ簭鍚嶇О
+ paramTemplateId?: number; // 宸ヨ壓鍙傛暟妯℃澘缂栧彿
sort?: number; // 搴忓彿
nextProcessId?: number; // 涓嬩竴閬撳伐搴忕紪鍙�
nextProcessName?: string; // 涓嬩竴閬撳伐搴忓悕绉�
diff --git a/src/api/mes/pro/workorderparamrecord/index.ts b/src/api/mes/pro/workorderparamrecord/index.ts
index babaafd..90ea727 100644
--- a/src/api/mes/pro/workorderparamrecord/index.ts
+++ b/src/api/mes/pro/workorderparamrecord/index.ts
@@ -13,6 +13,7 @@
unitMeasureId?: number;
unitMeasureName?: string;
paramValue?: string;
+ required?: boolean;
remark?: string;
}
diff --git a/src/api/mes/qc/template/item/index.ts b/src/api/mes/qc/template/item/index.ts
index a7b92c1..d10c641 100644
--- a/src/api/mes/qc/template/item/index.ts
+++ b/src/api/mes/qc/template/item/index.ts
@@ -21,6 +21,21 @@
}
}
+/** 璐ㄦ鏂规閫夐」锛堟柟妗�-浜у搧鍏宠仈锛� */
+export namespace MesQcTemplateItemOptionApi {
+ export interface Option {
+ id?: number; // 浜у搧鍏宠仈缂栧彿
+ templateId?: number; // 璐ㄦ鏂规 ID
+ templateCode?: string; // 璐ㄦ鏂规缂栧彿
+ templateName?: string; // 璐ㄦ鏂规鍚嶇О
+ quantityCheck?: number; // 鏈�浣庢娴嬫暟
+ quantityUnqualified?: number; // 鏈�澶т笉鍚堟牸鏁�
+ criticalRate?: number; // 鏈�澶ц嚧鍛界己闄风巼锛�%锛�
+ majorRate?: number; // 鏈�澶т弗閲嶇己闄风巼锛�%锛�
+ minorRate?: number; // 鏈�澶ц交寰己闄风巼锛�%锛�
+ }
+}
+
/** 鏌ヨ浜у搧鍏宠仈鍒嗛〉 */
export function getTemplateItemPage(
params: PageParam & { templateId?: number },
@@ -52,3 +67,16 @@
export function deleteTemplateItem(id: number) {
return requestClient.delete(`/mes/qc/template/item/delete?id=${id}`);
}
+
+/** 鏍规嵁鐗╂枡鎴栧伐鍗�/浠诲姟鍜屾娴嬬绫昏幏鍙栧彲鐢ㄨ川妫�鏂规鍒楄〃 */
+export function getTemplateItemOptionList(params: {
+ itemId?: number;
+ qcType: number;
+ taskId?: number;
+ workOrderId?: number;
+}) {
+ return requestClient.get<MesQcTemplateItemOptionApi.Option[]>(
+ '/mes/qc/template/item/list-by-item-and-type',
+ { params },
+ );
+}
diff --git a/src/api/mes/wm/batch/index.ts b/src/api/mes/wm/batch/index.ts
index 7ed63bc..569efd7 100644
--- a/src/api/mes/wm/batch/index.ts
+++ b/src/api/mes/wm/batch/index.ts
@@ -56,6 +56,56 @@
expireDate?: string[]; // 杩囨湡鏃ユ湡
receiptDate?: string[]; // 鍏ュ簱鏃ユ湡
}
+
+ /** MES 鎵规閲囪喘鏉ユ簮锛堥噰璐叆搴撳崟 + IQC锛� */
+ export interface PurchaseSource {
+ receiptCode?: string; // 閲囪喘鍏ュ簱鍗曞彿
+ receiptDate?: Date; // 鍏ュ簱鏃ユ湡
+ purchaseOrderCode?: string; // 閲囪喘璁㈠崟缂栧彿
+ vendorId?: number; // 渚涘簲鍟� ID
+ vendorCode?: string; // 渚涘簲鍟嗙紪鐮�
+ vendorName?: string; // 渚涘簲鍟嗗悕绉�
+ iqcCode?: string; // IQC 妫�楠屽崟鍙�
+ checkResult?: number; // IQC 妫�楠岀粨璁猴紙瀛楀吀 mes_qc_check_result锛�
+ itemId?: number; // 鐗╂枡 ID
+ itemCode?: string; // 鐗╂枡缂栫爜
+ itemName?: string; // 鐗╂枡鍚嶇О
+ batchCode?: string; // 鎵规鍙�
+ receivedQuantity?: number; // 鍏ュ簱鏁伴噺
+ }
+
+ /** MES 鎵规閿�鍞幓鍚戯紙閿�鍞嚭搴撳崟 + OQC锛� */
+ export interface SalesTarget {
+ salesCode?: string; // 閿�鍞嚭搴撳崟鍙�
+ salesDate?: Date; // 鍑哄簱鏃ユ湡
+ salesOrderCode?: string; // 閿�鍞鍗曠紪鍙�
+ clientId?: number; // 瀹㈡埛 ID
+ clientCode?: string; // 瀹㈡埛缂栫爜
+ clientName?: string; // 瀹㈡埛鍚嶇О
+ oqcCode?: string; // OQC 妫�楠屽崟鍙�
+ checkResult?: number; // OQC 妫�楠岀粨璁猴紙瀛楀吀 mes_qc_check_result锛�
+ itemId?: number; // 鐗╂枡 ID
+ itemCode?: string; // 鐗╂枡缂栫爜
+ itemName?: string; // 鐗╂枡鍚嶇О
+ batchCode?: string; // 鎵规鍙�
+ quantity?: number; // 鍑哄簱鏁伴噺
+ qualityStatus?: number; // 璐ㄩ噺鐘舵�侊紙瀛楀吀 mes_wm_quality_status锛�
+ }
+
+ /** MES 鎵规宸ュ簭娴佽浆锛堜骇鍑�/鎶曞叆浜嬩欢锛� */
+ export interface ProcessTrace {
+ eventType?: 'OUT' | 'IN'; // 浜嬩欢绫诲瀷锛歄UT-浜у嚭 / IN-鎶曞叆
+ workOrderId?: number; // 鐢熶骇宸ュ崟 ID
+ workOrderCode?: string; // 鐢熶骇宸ュ崟缂栧彿
+ processId?: number; // 宸ュ簭 ID
+ processCode?: string; // 宸ュ簭缂栫爜
+ processName?: string; // 宸ュ簭鍚嶇О
+ batchCode?: string; // 鎵规鍙�
+ quantity?: number; // 鏁伴噺
+ qualityStatus?: number; // 璐ㄩ噺鐘舵�侊紙瀛楀吀 mes_wm_quality_status锛夛紝浠呬骇鍑轰晶鏈夊��
+ eventTime?: Date; // 浜嬩欢鏃堕棿
+ processSort?: number; // 宸ュ簭搴忓彿
+ }
}
/** 鏌ヨ鎵规璇︽儏 */
@@ -88,3 +138,34 @@
{ params: { code } },
);
}
+
+/** 鎵规閲囪喘鏉ユ簮锛堥噰璐叆搴撳崟 + IQC锛� */
+export function getPurchaseSourceList(code: string) {
+ return requestClient.get<MesWmBatchApi.PurchaseSource[]>(
+ '/mes/wm/batch/purchase-sources',
+ { params: { code } },
+ );
+}
+
+/** 鎵规閿�鍞幓鍚戯紙閿�鍞嚭搴撳崟 + OQC锛� */
+export function getSalesTargetList(code: string) {
+ return requestClient.get<MesWmBatchApi.SalesTarget[]>(
+ '/mes/wm/batch/sales-targets',
+ { params: { code } },
+ );
+}
+
+/** 鎵规宸ュ簭娴佽浆锛堜骇鍑�/鎶曞叆浜嬩欢锛� */
+export function getProcessTraceList(code: string) {
+ return requestClient.get<MesWmBatchApi.ProcessTrace[]>(
+ '/mes/wm/batch/process-trace',
+ { params: { code } },
+ );
+}
+
+/** 鏍规嵁 SN 鐮佸弽鏌ユ壒娆� */
+export function getBatchBySn(sn: string) {
+ return requestClient.get<MesWmBatchApi.Batch | null>('/mes/wm/batch/get-by-sn', {
+ params: { sn },
+ });
+}
diff --git a/src/api/mes/wm/materialstock/index.ts b/src/api/mes/wm/materialstock/index.ts
index edbd6fe..06a2911 100644
--- a/src/api/mes/wm/materialstock/index.ts
+++ b/src/api/mes/wm/materialstock/index.ts
@@ -64,6 +64,29 @@
});
}
+/** 搴撳瓨鐜版湁閲忓鍏ョ粨鏋� */
+export interface ImportResult {
+ /** 鏂板缓搴撳瓨璁板綍鎴愬姛鐨勮锛屽厓绱犳牸寮忋�岀墿鏂欑紪鐮�-浠撳簱銆� */
+ createNames?: string[];
+ /** 宸插瓨鍦ㄥ簱瀛樿褰曡皟鏁存垚鍔熺殑琛岋紝鍏冪礌鏍煎紡銆岀墿鏂欑紪鐮�-浠撳簱銆� */
+ updateNames?: string[];
+ /** 瀵煎叆澶辫触鐨勮褰曪紝key 涓恒�岀墿鏂欑紪鐮�-浠撳簱銆嶏紝value 涓哄け璐ュ師鍥� */
+ failureNames?: Record<string, string>;
+}
+
+/** 涓嬭浇搴撳瓨鐜版湁閲忓鍏ユā鏉� */
+export function importMaterialStockTemplate() {
+ return requestClient.download('/mes/wm/material-stock/import-template');
+}
+
+/** 瀵煎叆搴撳瓨鐜版湁閲� */
+export function importMaterialStock(file: File) {
+ return requestClient.upload<MesWmMaterialStockApi.ImportResult>(
+ '/mes/wm/material-stock/import',
+ { file },
+ );
+}
+
/** 搴撳瓨璋冩暣锛堣嚜瀹氫箟鍑哄叆搴擄級- 鍏ュ簱 */
export interface MaterialStockAdjustInParams {
itemId: number; // 鐗╂枡ID
diff --git a/src/api/mes/wm/productissue/line/index.ts b/src/api/mes/wm/productissue/line/index.ts
index 45167c2..9b79b2f 100644
--- a/src/api/mes/wm/productissue/line/index.ts
+++ b/src/api/mes/wm/productissue/line/index.ts
@@ -17,6 +17,16 @@
batchId?: number; // 鎵规缂栧彿
remark?: string; // 澶囨敞
}
+
+ /** 棰嗘枡鍑哄簱鍗曞彲棰嗙墿鏂欙紙褰撳墠宸ュ崟 BOM 鐗╂枡锛� */
+ export interface BomItem {
+ itemId?: number; // 鐗╂枡缂栧彿
+ itemCode?: string; // 鐗╂枡缂栫爜
+ itemName?: string; // 鐗╂枡鍚嶇О
+ itemSpecification?: string; // 瑙勬牸鍨嬪彿
+ unitMeasureName?: string; // 鍗曚綅鍚嶇О
+ requiredQuantity?: number; // 宸ュ崟 BOM 闇�姹傞噺
+ }
}
/** 鏌ヨ棰嗘枡鍑哄簱鍗曡鍒嗛〉 */
@@ -51,3 +61,19 @@
export function deleteProductIssueLine(id: number) {
return requestClient.delete(`/mes/wm/product-issue-line/delete?id=${id}`);
}
+
+/** 鏌ヨ棰嗘枡鍑哄簱鍗曞彲棰嗙墿鏂欏垪琛紙褰撳墠宸ュ崟 BOM 鐗╂枡锛� */
+export function getBomItemList(issueId: number) {
+ return requestClient.get<MesWmProductIssueLineApi.BomItem[]>(
+ '/mes/wm/product-issue-line/bom-item-list',
+ { params: { issueId } },
+ );
+}
+
+/** 鏌ヨ鍙��鏂欑殑棰嗘枡鍗曡鍒楄〃锛堟寜鐢熶骇璁㈠崟杩囨护锛屼粎杩斿洖宸插畬鎴愰鏂欑殑璁板綍锛� */
+export function getReturnableIssueLines(workOrderId: number) {
+ return requestClient.get<MesWmProductIssueLineApi.ProductIssueLine[]>(
+ '/mes/wm/product-issue-line/returnable-list',
+ { params: { workOrderId } },
+ );
+}
diff --git a/src/api/mes/wm/returnissue/line/index.ts b/src/api/mes/wm/returnissue/line/index.ts
index 05bef07..d1bb469 100644
--- a/src/api/mes/wm/returnissue/line/index.ts
+++ b/src/api/mes/wm/returnissue/line/index.ts
@@ -7,6 +7,7 @@
export interface ReturnIssueLine {
id?: number; // 琛岀紪鍙�
issueId?: number; // 閫�鏂欏崟缂栧彿
+ issueLineId?: number; // 棰嗘枡鍗曡缂栧彿锛堝叧鑱旀潵婧愰鏂欒褰曪級
itemId?: number; // 鐗╂枡缂栧彿
itemCode?: string; // 鐗╂枡缂栫爜
itemName?: string; // 鐗╂枡鍚嶇О
diff --git a/src/api/mes/wm/returnvendor/index.ts b/src/api/mes/wm/returnvendor/index.ts
index c38e631..af94123 100644
--- a/src/api/mes/wm/returnvendor/index.ts
+++ b/src/api/mes/wm/returnvendor/index.ts
@@ -14,7 +14,7 @@
vendorCode?: string; // 渚涘簲鍟嗙紪鐮�
vendorName?: string; // 渚涘簲鍟嗗悕绉�
vendorNickname?: string; // 渚涘簲鍟嗙畝绉�
- returnDate?: number; // 閫�璐ф棩鏈�
+ returnDate?: number | string; // 閫�璐ф棩鏈�
returnReason?: string; // 閫�璐у師鍥�
transportCode?: string; // 杩愬崟鍙�
transportTelephone?: string; // 鑱旂郴鐢佃瘽
diff --git a/src/api/srm/supplier/index.ts b/src/api/srm/supplier/index.ts
index 60b176a..e4e7c97 100644
--- a/src/api/srm/supplier/index.ts
+++ b/src/api/srm/supplier/index.ts
@@ -31,6 +31,11 @@
return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/simple-list');
}
+/** 鑾峰彇渚涘簲鍟嗘。妗堝垪琛紙涓嶈繃婊ゅ噯鍏ョ姸鎬侊級 */
+export function getSupplierList() {
+ return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/list');
+}
+
/** 鑾峰彇渚涘簲鍟嗚鎯� */
export function getSupplier(id: number) {
return requestClient.get<SrmSupplierApi.SupplierVO>(`/srm/supplier/get?id=${id}`);
@@ -50,3 +55,21 @@
export function deleteSupplier(id: number) {
return requestClient.delete(`/srm/supplier/delete?id=${id}`);
}
+
+/** 瀵煎嚭渚涘簲鍟嗘。妗� Excel锛堟寜鍒楄〃鏌ヨ鏉′欢锛� */
+export function exportSupplierExcel(params: any) {
+ return requestClient.download('/srm/supplier/export-excel', { params });
+}
+
+/** 涓嬭浇渚涘簲鍟嗘。妗堝鍏ユā鏉� */
+export function getSupplierImportTemplate() {
+ return requestClient.download('/srm/supplier/get-import-template');
+}
+
+/** 瀵煎叆渚涘簲鍟嗘。妗� Excel */
+export function importSupplierExcel(data: {
+ file: File;
+ updateSupport: boolean;
+}) {
+ return requestClient.upload('/srm/supplier/import-excel', data);
+}
diff --git a/src/api/srm/supplierApply/index.ts b/src/api/srm/supplierApply/index.ts
index 35a48fa..ccfcf06 100644
--- a/src/api/srm/supplierApply/index.ts
+++ b/src/api/srm/supplierApply/index.ts
@@ -8,27 +8,32 @@
applyNo?: string;
supplierId?: number;
supplierName: string;
- contactName?: string;
- contactMobile?: string;
- contactEmail?: string;
+ supplierType?: string;
+ companyNature?: string;
+ registeredCapital?: number;
+ businessLicenseNo?: string;
+ taxNo?: string;
address?: string;
bankName?: string;
bankAccount?: string;
- taxNo?: string;
- businessLicense?: string;
- taxCertificate?: string;
- orgCodeCertificate?: string;
+ contactName?: string;
+ contactMobile?: string;
+ contactEmail?: string;
+ applyUserName?: string;
+ applyTime?: string;
applyStatus?: number;
applyStatusLabel?: string;
auditComment?: string;
+ processInstanceId?: string;
remark?: string;
createTime?: string;
}
- export interface ReviewVO {
- id: number;
- passed: boolean;
- comment?: string;
+ /** 鍗忓悓鍔炲叕瀹℃壒娴佺▼瀹氫箟 */
+ export interface ProcessDefinition {
+ id: string;
+ key: string;
+ name: string;
}
}
@@ -57,22 +62,16 @@
return requestClient.delete(`/srm/supplier-apply/delete?id=${id}`);
}
-/** 鎻愪氦瀹℃牳 */
-export function submitApply(id: number) {
- return requestClient.put(`/srm/supplier-apply/submit?id=${id}`);
+/** 鑾峰彇瀹℃壒娴佺▼鍒楄〃锛堝崗鍚屽姙鍏凡鍙戝竷娴佺▼锛� */
+export function getApproveProcessList() {
+ return requestClient.get<SrmSupplierApplyApi.ProcessDefinition[]>(
+ '/srm/supplier-apply/approve-process-list',
+ );
}
-/** 閲囪喘瀹℃牳 */
-export function reviewPurchase(data: SrmSupplierApplyApi.ReviewVO) {
- return requestClient.put('/srm/supplier-apply/review-purchase', data);
-}
-
-/** 璐ㄩ噺瀹℃牳 */
-export function reviewQuality(data: SrmSupplierApplyApi.ReviewVO) {
- return requestClient.put('/srm/supplier-apply/review-quality', data);
-}
-
-/** 璐㈠姟瀹℃牳 */
-export function reviewFinance(data: SrmSupplierApplyApi.ReviewVO) {
- return requestClient.put('/srm/supplier-apply/review-finance', data);
+/** 鎻愪氦鍑嗗叆鐢宠瀹℃壒锛堝惎鍔ㄥ崗鍚屽姙鍏祦绋嬶級 */
+export function submitApply(id: number, processDefinitionKey: string) {
+ return requestClient.put('/srm/supplier-apply/submit', null, {
+ params: { id, processDefinitionKey },
+ });
}
diff --git a/src/api/srm/supplierCertificate/index.ts b/src/api/srm/supplierCertificate/index.ts
index 9908b1c..2504662 100644
--- a/src/api/srm/supplierCertificate/index.ts
+++ b/src/api/srm/supplierCertificate/index.ts
@@ -1,5 +1,6 @@
import type { PageParam, PageResult } from '@vben/request';
+import type { SystemStorageApi } from '#/api/system/storage';
import { requestClient } from '#/api/request';
export namespace SrmSupplierCertificateApi {
@@ -16,6 +17,8 @@
status?: number;
remark?: string;
createTime?: string;
+ blobIds?: number[];
+ attachmentList?: SystemStorageApi.StorageBlob[];
}
}
diff --git a/src/api/srm/tender/index.ts b/src/api/srm/tender/index.ts
index a44c5b9..8b6d54d 100644
--- a/src/api/srm/tender/index.ts
+++ b/src/api/srm/tender/index.ts
@@ -1,5 +1,6 @@
import type { PageParam, PageResult } from '@vben/request';
+import type { SystemStorageApi } from '#/api/system/storage';
import { requestClient } from '#/api/request';
export namespace SrmTenderApi {
@@ -23,6 +24,8 @@
purchaseOrderNo?: string;
biddingMode?: string;
createTime?: string;
+ blobIds?: number[];
+ attachmentList?: SystemStorageApi.StorageBlob[];
}
export interface TenderMaterialVO {
@@ -105,6 +108,7 @@
id: number;
tenderDesc?: string;
qualificationRequirements?: string;
+ blobIds?: number[];
}
}
@@ -194,6 +198,11 @@
return requestClient.put(`/srm/tender-bid/withdraw?id=${id}`);
}
+export function updateBidTotal(id: number, bidTotalAmount?: number | null) {
+ const params = bidTotalAmount == null ? '' : `&bidTotalAmount=${bidTotalAmount}`;
+ return requestClient.put(`/srm/tender-bid/update-total?id=${id}${params}`);
+}
+
// ========== 鎶ヤ环绠$悊 ==========
export function getQuoteList(bidId: number) {
diff --git a/src/api/system/company-config/index.ts b/src/api/system/company-config/index.ts
new file mode 100644
index 0000000..dbd1405
--- /dev/null
+++ b/src/api/system/company-config/index.ts
@@ -0,0 +1,27 @@
+import { requestClient } from '#/api/request';
+
+export namespace SystemCompanyConfigApi {
+ /** 鍏徃鍝佺墝淇℃伅 */
+ export interface CompanyConfig {
+ id?: number;
+ companyName?: string;
+ logoUrl?: string;
+ logoBlobId?: number;
+ loginTitle?: string;
+ loginDescription?: string;
+ }
+}
+
+/** 鑾峰緱鍏徃鍝佺墝淇℃伅锛堝厤鐧诲綍鍏紑锛岀櫥褰曢〉 / 搴旂敤鍚姩鏃朵娇鐢級 */
+export async function getCompanyConfig() {
+ return requestClient.get<SystemCompanyConfigApi.CompanyConfig | null>(
+ '/system/company-config/get',
+ );
+}
+
+/** 淇濆瓨鍏徃鍝佺墝淇℃伅 */
+export async function saveCompanyConfig(
+ data: SystemCompanyConfigApi.CompanyConfig,
+) {
+ return requestClient.post<number>('/system/company-config/save', data);
+}
diff --git a/src/api/system/database/index.ts b/src/api/system/database/index.ts
new file mode 100644
index 0000000..261d7f2
--- /dev/null
+++ b/src/api/system/database/index.ts
@@ -0,0 +1,50 @@
+import { requestClient } from '#/api/request';
+
+function downloadBlob(blob: Blob, headers?: any) {
+ let filename = 'backup.sql';
+ if (headers) {
+ const disposition = headers['content-disposition'] || headers['Content-Disposition'];
+ if (disposition && disposition.indexOf('filename=') !== -1) {
+ const matches = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
+ if (matches != null && matches[1]) {
+ filename = matches[1].replace(/['"]/g, '');
+ }
+ }
+ }
+
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.style.display = 'none';
+ link.href = url;
+ link.setAttribute('download', decodeURIComponent(filename));
+ document.body.append(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+}
+
+/** 涓嬭浇鏁版嵁搴撳浠芥枃浠� */
+export function backupDatabase(onDownloadProgress?: (progressEvent: any) => void) {
+ return requestClient.request('/system/database/backup', {
+ method: 'GET',
+ responseType: 'blob',
+ timeout: 300000, // 5鍒嗛挓
+ onDownloadProgress,
+ responseReturn: 'raw',
+ }).then((res: any) => {
+ const blob = res.data || res;
+ if (blob instanceof Blob) {
+ downloadBlob(blob, res.headers);
+ }
+ return res;
+ }).catch((err: any) => {
+ if (err instanceof Blob) {
+ downloadBlob(err);
+ return err;
+ } else if (err?.response?.data instanceof Blob) {
+ downloadBlob(err.response.data, err.response.headers);
+ return err;
+ }
+ throw err;
+ });
+}
diff --git a/src/api/system/dept/index.ts b/src/api/system/dept/index.ts
index d6403d7..7d20bf2 100644
--- a/src/api/system/dept/index.ts
+++ b/src/api/system/dept/index.ts
@@ -50,3 +50,16 @@
export async function deleteDeptList(ids: number[]) {
return requestClient.delete(`/system/dept/delete-list?ids=${ids.join(',')}`);
}
+
+/** 涓嬭浇閮ㄩ棬瀵煎叆妯℃澘 */
+export function importDeptTemplate() {
+ return requestClient.download('/system/dept/get-import-template');
+}
+
+/** 瀵煎叆閮ㄩ棬 */
+export function importDept(file: File, updateSupport: boolean) {
+ return requestClient.upload('/system/dept/import', {
+ file,
+ updateSupport,
+ });
+}
diff --git a/src/api/system/post/index.ts b/src/api/system/post/index.ts
index 8b32ad2..3032a56 100644
--- a/src/api/system/post/index.ts
+++ b/src/api/system/post/index.ts
@@ -61,3 +61,16 @@
params,
});
}
+
+/** 涓嬭浇宀椾綅瀵煎叆妯℃澘 */
+export function importPostTemplate() {
+ return requestClient.download('/system/post/get-import-template');
+}
+
+/** 瀵煎叆宀椾綅 */
+export function importPost(file: File, updateSupport: boolean) {
+ return requestClient.upload('/system/post/import', {
+ file,
+ updateSupport,
+ });
+}
diff --git a/src/api/system/storage/index.ts b/src/api/system/storage/index.ts
index 994d3ad..d1c3557 100644
--- a/src/api/system/storage/index.ts
+++ b/src/api/system/storage/index.ts
@@ -45,6 +45,17 @@
);
}
+/** 鍏叡鏂囦欢涓婁紶锛堟案涔呮湁鏁堬紝杩斿洖姘镐箙鍏紑 previewURL锛岀敤浜庣櫥褰曢〉绛夊厤鐧诲綍灞曠ず鐨勫満鏅級 */
+export function publicUploadFile(files: globalThis.File[]) {
+ const formData = new FormData();
+ files.forEach((file) => formData.append('files', file));
+ return requestClient.post<SystemStorageApi.StorageBlob[]>(
+ '/system/storage-blob/public-upload',
+ formData,
+ { headers: { 'Content-Type': 'multipart/form-data' } },
+ );
+}
+
/** 鏌ヨ闄勪欢鍒楄〃 */
export function listAttachments(params: SystemStorageApi.AttachmentListParams) {
return requestClient.get<SystemStorageApi.StorageBlob[]>(
diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts
index 1f49036..208d4b9 100644
--- a/src/api/system/user/index.ts
+++ b/src/api/system/user/index.ts
@@ -114,3 +114,10 @@
params: { nickname },
});
}
+
+/** 閫氳繃瑙掕壊鏍囪瘑鑾峰彇鐢ㄦ埛绮剧畝淇℃伅鍒楄〃 */
+export function getSimpleUserListByRoleCode(code: Object) {
+ return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-role-code', {
+ params: code ,
+ });
+}
diff --git a/src/bootstrap.ts b/src/bootstrap.ts
index 2e22e62..bbfb549 100644
--- a/src/bootstrap.ts
+++ b/src/bootstrap.ts
@@ -18,6 +18,9 @@
import App from './app.vue';
import { router } from './router';
+import { getCompanyConfig } from '#/api/system/company-config';
+import { applyCompanyConfig } from '#/utils/company-config';
+
async function bootstrap(namespace: string) {
// 鍒濆鍖栫粍浠堕�傞厤鍣�
await initComponentAdapter();
@@ -49,6 +52,17 @@
// 閰嶇疆 pinia-store
await initStores(app, { namespace });
+ // 鍔犺浇鍏徃鍝佺墝淇℃伅锛堜粠鍚庣锛屽厤鐧诲綍鎺ュ彛锛夛紝鑱斿姩鐧诲綍椤�/鑿滃崟鏍�/缃戦〉鏍囬銆�
+ // 娉細姝ゆ椂 pinia 灏氭湭鍦ㄧ粍浠跺婵�娲伙紝杩欓噷鐢ㄧ函鍑芥暟锛屼笉渚濊禆 store銆�
+ try {
+ const companyConfig = await getCompanyConfig();
+ if (companyConfig) {
+ applyCompanyConfig(companyConfig);
+ }
+ } catch {
+ // 鍚庣涓嶅彲鐢ㄦ垨鏈厤缃紝淇濇寔缂栬瘧鏈熼粯璁�
+ }
+
// 瀹夎鏉冮檺鎸囦护
registerAccessDirective(app);
@@ -71,7 +85,8 @@
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
- (routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
+ (routeTitle ? `${$t(routeTitle)} - ` : '') +
+ preferences.copyright.companyName;
useTitle(pageTitle);
}
});
diff --git a/src/components/form-create/helpers.ts b/src/components/form-create/helpers.ts
index 12d07a6..9e827de 100644
--- a/src/components/form-create/helpers.ts
+++ b/src/components/form-create/helpers.ts
@@ -28,7 +28,10 @@
}
/** 瑙g爜琛ㄥ崟 Conf */
-export function decodeConf(conf: string) {
+export function decodeConf(conf?: string | null) {
+ if (!conf) {
+ return {};
+ }
return formCreate.parseJson(conf);
}
@@ -43,9 +46,9 @@
}
/** 瑙g爜琛ㄥ崟 Fields */
-export function decodeFields(fields: string[]) {
+export function decodeFields(fields?: string[] | null) {
const rule: Rule[] = [];
- fields.forEach((item) => {
+ (fields || []).forEach((item) => {
rule.push(formCreate.parseJson(item));
});
return rule;
diff --git a/src/components/upload/file-upload.vue b/src/components/upload/file-upload.vue
index 6296579..8920f4d 100644
--- a/src/components/upload/file-upload.vue
+++ b/src/components/upload/file-upload.vue
@@ -26,7 +26,7 @@
disabled: false,
drag: false,
helpText: '',
- maxSize: 2,
+ maxSize: 50,
maxNumber: 1,
accept: () => [],
multiple: false,
@@ -292,14 +292,14 @@
:progress="{ showInfo: true }"
:show-upload-list="{
showPreviewIcon: true,
- showRemoveIcon: true,
+ showRemoveIcon: !disabled,
showDownloadIcon,
}"
@remove="handleRemove"
@preview="handlePreview"
@reject="handleExceed"
>
- <div v-if="drag" class="upload-drag-area">
+ <div v-if="drag && !disabled" class="upload-drag-area">
<p class="ant-upload-drag-icon">
<IconifyIcon icon="lucide:cloud-upload" />
</p>
@@ -308,7 +308,9 @@
鏀寔{{ accept.join('/') }}鏍煎紡鏂囦欢锛屼笉瓒呰繃{{ maxSize }}MB
</p>
</div>
- <div v-else-if="fileList && fileList.length < maxNumber">
+ <div
+ v-else-if="!disabled && fileList && fileList.length < maxNumber"
+ >
<Button>
<IconifyIcon icon="lucide:cloud-upload" />
{{ $t('ui.upload.upload') }}
diff --git a/src/components/upload/image-upload.vue b/src/components/upload/image-upload.vue
index 78b7aa0..c894825 100644
--- a/src/components/upload/image-upload.vue
+++ b/src/components/upload/image-upload.vue
@@ -32,7 +32,7 @@
disabled: false,
listType: 'picture-card',
helpText: '',
- maxSize: 2,
+ maxSize: 50,
maxNumber: 1,
accept: () => defaultImageAccepts,
multiple: false,
diff --git a/src/components/upload/use-upload.ts b/src/components/upload/use-upload.ts
index 3076fe1..66c55db 100644
--- a/src/components/upload/use-upload.ts
+++ b/src/components/upload/use-upload.ts
@@ -103,12 +103,18 @@
) {
const formData = new FormData();
formData.append('files', file);
- const blobs = await requestClient.post('/system/storage-blob/upload', formData, {
+ // 浣跨敤鍏叡涓婁紶鎺ュ彛锛岃繑鍥炴案涔呭叕寮� previewURL
+ const blobs = await requestClient.post('/system/storage-blob/public-upload', formData, {
onUploadProgress,
headers: { 'Content-Type': 'multipart/form-data' },
});
const blob = Array.isArray(blobs) ? blobs[0] : blobs;
- return blob ? { url: blob.url || blob.previewURL, name: blob.name, id: blob.id } : {};
+ // 瀛樺偍瀹屾暣 previewURL锛堝惈 publicKey锛夛紝纭繚鍙闂�
+ return blob ? {
+ url: blob.previewURL || blob.url,
+ name: blob.name || blob.originalFilename,
+ id: blob.id
+ } : {};
}
return {
diff --git a/src/layouts/auth.vue b/src/layouts/auth.vue
index ad41d1f..51dc083 100644
--- a/src/layouts/auth.vue
+++ b/src/layouts/auth.vue
@@ -1,16 +1,28 @@
<script lang="ts" setup>
-import { computed } from 'vue';
+import { computed, onMounted } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
+import { useCompanyConfigStore } from '#/store/company-config';
declare const __APP_LOGO__: string;
-const appName = computed(() => preferences.app.name);
-const logo = computed(() => __APP_LOGO__ || preferences.logo.source);
+const companyConfigStore = useCompanyConfigStore();
+
+onMounted(() => {
+ // 鐧诲綍椤靛睍绀� loginTitle锛氬惎鍔ㄥ姞杞借蛋鐨勬槸绾嚱鏁帮紙涓嶅~ store锛夛紝杩涘叆鐧诲綍椤垫椂鎷夊彇涓�娆�
+ companyConfigStore.loadFromServer();
+});
+
+const appName = computed(() => preferences.copyright.companyName);
+// 鐧诲綍椤� logo 浼樺厛鍙栨暟鎹簱閰嶇疆锛坧references.logo.source锛夛紝鏈厤缃椂鍥為��缂栬瘧鏈熼粯璁� __APP_LOGO__
+const logo = computed(() => preferences.logo.source || __APP_LOGO__);
const logoDark = computed(() => preferences.logo.sourceDark);
+// 鐧诲綍椤垫爣棰�/鎻忚堪浣跨敤 i18n 鍥哄畾鏂囨锛屼笉闅忓叕鍙搁厤缃彉鍖�
+const pageTitle = computed(() => $t('authentication.pageTitle'));
+const pageDescription = computed(() => $t('authentication.pageDesc'));
</script>
<template>
@@ -18,8 +30,8 @@
:app-name="appName"
:logo="logo"
:logo-dark="logoDark"
- :page-description="$t('authentication.pageDesc')"
- :page-title="$t('authentication.pageTitle')"
+ :page-description="pageDescription"
+ :page-title="pageTitle"
>
<!-- 鑷畾涔夊伐鍏锋爮 -->
<!-- <template #toolbar></template> -->
diff --git a/src/main.ts b/src/main.ts
index e208cd2..b692f75 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -5,6 +5,7 @@
declare const __APP_LOGO__: string;
declare const __APP_FAVICON__: string;
+declare const __APP_NAME__: string;
/**
* 搴旂敤鍒濆鍖栧畬鎴愪箣鍚庡啀杩涜椤甸潰鍔犺浇娓叉煋
@@ -27,6 +28,13 @@
if (__APP_LOGO__) {
preferencesManager.updatePreferences({ logo: { source: __APP_LOGO__ } });
}
+ // 寮哄埗瑕嗙洊鍏徃鍚嶇О锛岀粫杩� localStorage 缂撳瓨
+ if (__APP_NAME__) {
+ preferencesManager.updatePreferences({
+ app: { name: __APP_NAME__ },
+ copyright: { companyName: __APP_NAME__ },
+ });
+ }
if (__APP_FAVICON__) {
const link = document.querySelector('link[rel="icon"]');
if (link) {
diff --git a/src/packages/@core/base/icons/src/lucide.ts b/src/packages/@core/base/icons/src/lucide.ts
index 01ea54d..63687b1 100644
--- a/src/packages/@core/base/icons/src/lucide.ts
+++ b/src/packages/@core/base/icons/src/lucide.ts
@@ -27,6 +27,7 @@
CloudUpload,
Copy,
CornerDownLeft,
+ DatabaseBackup,
Download,
Ellipsis,
Eraser,
diff --git a/src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue b/src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue
index 8898556..fbaa8a3 100644
--- a/src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue
+++ b/src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue
@@ -5,17 +5,22 @@
import { TabsTrigger } from 'reka-ui';
+import { cn } from '../../../../../base/shared/src/utils';
+
import { Tabs, TabsContent, TabsList } from '../../ui';
import TabsIndicator from './tabs-indicator.vue';
interface Props {
defaultValue?: string;
tabs?: SegmentedItem[];
+ /** 鏄惁鍏佽妯悜婊氬姩锛歵ab 杈冨鏃跺惎鐢紝鍚� tab 鎸夊唴瀹瑰搴︽帓鍒楋紝瓒呭嚭閮ㄥ垎鍙粦鍔� */
+ scrollable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
defaultValue: '',
tabs: () => [],
+ scrollable: false,
});
const activeTab = defineModel<string>();
@@ -25,12 +30,18 @@
});
const tabsStyle = computed(() => {
+ if (props.scrollable) {
+ return undefined;
+ }
return {
'grid-template-columns': `repeat(${props.tabs.length}, minmax(0, 1fr))`,
};
});
const tabsIndicatorStyle = computed(() => {
+ if (props.scrollable) {
+ return undefined;
+ }
return {
width: `${(100 / props.tabs.length).toFixed(0)}%`,
};
@@ -45,14 +56,22 @@
<Tabs v-model="activeTab" :default-value="getDefaultValue">
<TabsList
:style="tabsStyle"
- class="bg-accent outline-heavy! relative grid w-full outline-2!"
+ :class="
+ cn(
+ 'bg-accent outline-heavy! relative outline-2!',
+ // 鍙粴鍔ㄦā寮忥細宸﹀榻愶紙瑕嗙洊 reka-ui 鐨� justify-center锛屽惁鍒欏唴瀹硅秴瀹芥椂宸︿晶琚尋鍑轰笖鏃犳硶婊氬姩锛夈�佸叏瀹姐�佹í鍚戞粴鍔�
+ scrollable
+ ? 'flex! w-full! overflow-x-auto justify-start!'
+ : 'grid w-full',
+ )
+ "
>
- <TabsIndicator :style="tabsIndicatorStyle" />
+ <TabsIndicator v-if="!scrollable" :style="tabsIndicatorStyle" />
<template v-for="tab in tabs" :key="tab.value">
<TabsTrigger
:value="tab.value"
:class="activeClass(tab.value)"
- class="hover:text-primary z-20 inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
+ class="hover:text-primary z-20 inline-flex shrink-0 items-center justify-center rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
>
{{ tab.label }}
</TabsTrigger>
diff --git a/src/packages/constants/src/biz-erp-enum.ts b/src/packages/constants/src/biz-erp-enum.ts
index 34bab52..c23356b 100644
--- a/src/packages/constants/src/biz-erp-enum.ts
+++ b/src/packages/constants/src/biz-erp-enum.ts
@@ -2,6 +2,7 @@
PURCHASE_ORDER: 10,
PURCHASE_IN: 11,
PURCHASE_RETURN: 12,
+ PURCHASE_INVOICE: 13,
SALE_ORDER: 20,
SALE_OUT: 21,
SALE_RETURN: 22,
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 848c41d..354f4e4 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -104,6 +104,26 @@
ABNORMAL: 2,
} as const;
+/** MES 璁¢噺鍣ㄥ叿鐘舵�佹灇涓� */
+export const MesDvMeteringStatusEnum = {
+ IN_USE: 1, // 鍦ㄧ敤
+ STOPPED: 2, // 鍋滅敤
+ SCRAPPED: 3, // 鎶ュ簾
+} as const;
+
+/** MES 璁¢噺妫�娴嬬粨璁烘灇涓� */
+export const MesDvMeteringCheckResultEnum = {
+ QUALIFIED: 1, // 鍚堟牸
+ UNQUALIFIED: 2, // 涓嶅悎鏍�
+} as const;
+
+/** MES 璁¢噺鍣ㄥ叿鍒版湡鐘舵�佹灇涓� */
+export const MesDvMeteringExpireStatusEnum = {
+ OVERDUE: 1, // 宸茶繃鏈�
+ WITHIN: 2, // 鍗冲皢鍒版湡
+ NORMAL: 3, // 姝e父
+} as const;
+
/** MES 缁翠慨宸ュ崟鐘舵�佹灇涓� */
export const MesDvRepairStatusEnum = {
PREPARE: MesOrderStatusConstants.DRAFT,
@@ -222,6 +242,13 @@
CONFIRMED: 1, // 宸茬‘璁�
FINISHED: 2, // 宸插畬鎴�
CANCELED: 3, // 宸插彇娑�
+} as const;
+
+/** MES 鐢熶骇宸ュ崟鐢熶骇鐘舵�佹灇涓撅紙瀵归綈鍚庣 MesProWorkOrderProductionStatusEnum锛� */
+export const MesProWorkOrderProductionStatusEnum = {
+ WAITING: 0, // 寰呯敓浜�
+ PRODUCING: 1, // 鐢熶骇涓�
+ FINISHED: 2, // 鐢熶骇瀹屾垚
} as const;
/** MES 宸ュ崟绫诲瀷鏋氫妇 */
@@ -516,11 +543,12 @@
RIGHT: 2, // 鍙宠ˉ榻�
} as const;
-/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓撅紙绾挎�ф祦杞細寰呭彂甯冣啋寰呬笂浼犫啋寰呭鏍糕啋寰呭綊妗b啋宸插綊妗o級 */
+/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓撅紙绾挎�ф祦杞細寰呭彂甯冣啋寰呬笂浼犫啋寰呭鏍糕啋瀹℃壒涓啋寰呭綊妗b啋宸插綊妗o級 */
export const MesPdProjectStatusEnum = {
PENDING_PUBLISH: 0, // 寰呭彂甯�
PENDING_UPLOAD: 1, // 寰呬笂浼�
PENDING_AUDIT: 2, // 寰呭鏍�
+ APPROVING: 5, // 瀹℃壒涓�
PENDING_ARCHIVE: 3, // 寰呭綊妗�
ARCHIVED: 4, // 宸插綊妗�
} as const;
diff --git a/src/packages/constants/src/biz-wms-enum.ts b/src/packages/constants/src/biz-wms-enum.ts
index 51df2da..abc02e8 100644
--- a/src/packages/constants/src/biz-wms-enum.ts
+++ b/src/packages/constants/src/biz-wms-enum.ts
@@ -57,14 +57,14 @@
return prefix + result;
}
-/** 鐩樼偣鏉′欢绫诲瀷鏋氫妇 */
+/** 鐩樼偣鏉′欢绫诲瀷鏋氫妇锛堜笌鍚庣 MesBizTypeConstants / 瀛楀吀鍊间繚鎸佷竴鑷达級 */
export const WlsStockTakingParamTypeEnum = {
- WAREHOUSE: 1, // 浠撳簱
- LOCATION: 2, // 搴撳尯
- AREA: 3, // 搴撲綅
- ITEM: 4, // 鐗╂枡
- BATCH: 5, // 鎵规
- QUALITY_STATUS: 6, // 璐ㄩ噺鐘舵��
+ WAREHOUSE: 102, // 浠撳簱
+ LOCATION: 103, // 搴撳尯
+ AREA: 104, // 搴撲綅
+ ITEM: 600, // 鐗╂枡
+ BATCH: 107, // 鎵规
+ QUALITY_STATUS: 900, // 璐ㄩ噺鐘舵��
} as const;
/** 鐩樼偣绫诲瀷鏋氫妇 */
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index ab5df28..2a62e5a 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -123,6 +123,10 @@
CRM_PRODUCT_STATUS: 'crm_product_status', // CRM 鍟嗗搧鐘舵��
CRM_PRODUCT_UNIT: 'crm_product_unit', // CRM 浜у搧鍗曚綅
CRM_RECEIVABLE_RETURN_TYPE: 'crm_receivable_return_type', // CRM 鍥炴鐨勮繕娆炬柟寮�
+ CRM_INVOICE_TYPE: 'crm_invoice_type', // CRM 鍙戠エ绫诲瀷
+ CRM_SALES_TARGET_PERIOD_TYPE: 'crm_sales_target_period_type', // CRM 閿�鍞洰鏍囧懆鏈熺被鍨�
+ CRM_SALES_TARGET_DIMENSION: 'crm_sales_target_dimension', // CRM 閿�鍞洰鏍囩淮搴�
+ CRM_SALES_TARGET_STATUS: 'crm_sales_target_status', // CRM 閿�鍞洰鏍囩姸鎬�
} as const;
/** ========== AFTERSALES - 鍞悗鏈嶅姟妯″潡 ========== */
@@ -230,8 +234,12 @@
MES_DV_REPAIR_RESULT: 'mes_dv_repair_result', // MES 缁翠慨缁撴灉
MES_DV_CHECK_RECORD_STATUS: 'mes_dv_check_record_status', // MES 鐐规璁板綍鐘舵��
MES_DV_CHECK_RESULT: 'mes_dv_check_result', // MES 鐐规缁撴灉
+ MES_DV_METERING_CATEGORY: 'mes_dv_metering_category', // MES 璁¢噺鍣ㄥ叿绫诲埆
+ MES_DV_METERING_STATUS: 'mes_dv_metering_status', // MES 璁¢噺鍣ㄥ叿鐘舵��
+ MES_DV_METERING_CHECK_RESULT: 'mes_dv_metering_check_result', // MES 璁¢噺妫�娴嬬粨璁�
MES_PRO_LINK_TYPE: 'mes_pro_link_type', // MES 宸ュ簭鍏崇郴绫诲瀷
MES_PRO_WORK_ORDER_STATUS: 'mes_pro_work_order_status', // MES 鐢熶骇宸ュ崟鐘舵��
+ ORDER_PRODUCTION_STATUS: 'order_production_status', // 鐢熶骇璁㈠崟鐘舵��
MES_PRO_WORK_ORDER_TYPE: 'mes_pro_work_order_type', // MES 宸ュ崟绫诲瀷
MES_PRO_WORK_ORDER_SOURCE_TYPE: 'mes_pro_work_order_source_type', // MES 宸ュ崟鏉ユ簮绫诲瀷
MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 鐢熶骇浠诲姟鐘舵��
@@ -320,6 +328,10 @@
HRM_SALARY_STATUS: 'hrm_salary_status', // HRM 钖祫鐘舵��
HRM_HANDOVER_STATUS: 'hrm_handover_status', // HRM 浜ゆ帴鐘舵��
HRM_PAYMENT_METHOD: 'hrm_payment_method', // HRM 鍙戞斁鏂瑰紡
+ HRM_CONTRACT_TYPE: 'hrm_contract_type', // HRM 鍚堝悓绫诲瀷
+ HRM_CONTRACT_TERM_TYPE: 'hrm_contract_term_type', // HRM 鍚堝悓鏈熼檺绫诲瀷
+ HRM_CONTRACT_STATUS: 'hrm_contract_status', // HRM 鍚堝悓鐘舵��
+ HRM_CONTRACT_TERMINATE_STATUS: 'hrm_contract_terminate_status', // HRM 鍚堝悓瑙i櫎缁堟鐘舵��
} as const;
/** 瀛楀吀绫诲瀷鏋氫妇 - 缁熶竴瀵煎嚭 */
diff --git a/src/packages/effects/layouts/src/basic/header/header.vue b/src/packages/effects/layouts/src/basic/header/header.vue
index f0d020a..1d319ef 100644
--- a/src/packages/effects/layouts/src/basic/header/header.vue
+++ b/src/packages/effects/layouts/src/basic/header/header.vue
@@ -9,6 +9,7 @@
import { VbenFullScreen, VbenIconButton } from '../../../../../@core/ui-kit/shadcn-ui/src';
import {
+ DatabaseBackupButton,
GlobalSearch,
LanguageToggle,
PreferencesButton,
@@ -56,6 +57,12 @@
}
// 鍋忓ソ璁剧疆蹇嵎鍔熻兘
if (preferencesButtonPosition.value.header) {
+ // 鏁版嵁搴撳浠芥寜閽�
+ list.push({
+ index: REFERENCE_VALUE + 5,
+ name: 'database-backup',
+ });
+
list.push({
index: REFERENCE_VALUE + 10,
name: 'preferences',
@@ -188,6 +195,9 @@
/>
</template>
+ <template v-else-if="slot.name === 'database-backup'">
+ <DatabaseBackupButton class="mr-1" />
+ </template>
<template v-else-if="slot.name === 'preferences'">
<PreferencesButton
class="mr-1"
diff --git a/src/packages/effects/layouts/src/basic/layout.vue b/src/packages/effects/layouts/src/basic/layout.vue
index 58139d0..10467c3 100644
--- a/src/packages/effects/layouts/src/basic/layout.vue
+++ b/src/packages/effects/layouts/src/basic/layout.vue
@@ -291,7 +291,7 @@
:collapsed="logoCollapsed"
:src="preferences.logo.source"
:src-dark="preferences.logo.sourceDark"
- :text="preferences.app.name"
+ :text="preferences.copyright.companyName"
:theme="logoTheme"
@click="clickLogo"
>
@@ -382,7 +382,7 @@
<VbenLogo
v-if="preferences.logo.enable"
:fit="preferences.logo.fit"
- :text="preferences.app.name"
+ :text="preferences.copyright.companyName"
:theme="sidebarThemeSub"
>
<template v-if="$slots['logo-text']" #text>
diff --git a/src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue b/src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue
new file mode 100644
index 0000000..5cc98f1
--- /dev/null
+++ b/src/packages/effects/layouts/src/widgets/database-backup/database-backup.vue
@@ -0,0 +1,70 @@
+<script lang="ts" setup>
+import { DatabaseBackup } from '../../../../../icons/src';
+import { VbenIconButton } from '../../../../../@core/ui-kit/shadcn-ui/src';
+import { useVbenModal } from '../../../../../@core/ui-kit/popup-ui/src';
+import { backupDatabase } from '../../../../../../api/system/database';
+import { ref } from 'vue';
+
+const isDownloading = ref(false);
+const downloadProgress = ref(0);
+
+const [Modal, modalApi] = useVbenModal({
+ onConfirm: async () => {
+ modalApi.close();
+ try {
+ isDownloading.value = true;
+ downloadProgress.value = 0;
+ await backupDatabase((progressEvent) => {
+ if (progressEvent.total) {
+ downloadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+ }
+ });
+ } catch (error: any) {
+ // Ignore if it's the Blob being thrown by the response interceptor
+ if (!(error instanceof Blob) && !(error?.response?.data instanceof Blob)) {
+ console.error('Backup failed:', error);
+ }
+ } finally {
+ isDownloading.value = false;
+ downloadProgress.value = 0;
+ }
+ },
+});
+
+function handleOpen() {
+ modalApi.open();
+}
+</script>
+
+<template>
+ <div class="relative">
+ <VbenIconButton
+ v-access:code="['system:database:backup']"
+ class="hover:animate-[shrink_0.3s_ease-in-out]"
+ @click="handleOpen"
+ >
+ <DatabaseBackup class="size-4 text-foreground" />
+ </VbenIconButton>
+
+ <Modal
+ title="鎻愮ず"
+ content-class="px-8 min-h-10"
+ footer-class="border-none mb-3 mr-3"
+ header-class="border-none"
+ >
+ 纭瑕佷笅杞芥暟鎹簱鐨勫浠芥枃浠跺悧锛�
+ </Modal>
+
+ <teleport to="body">
+ <div
+ v-if="isDownloading"
+ class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm"
+ >
+ <div class="flex flex-col items-center space-y-4 rounded-lg bg-background p-6 shadow-lg">
+ <div class="size-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
+ <p class="text-lg font-medium text-foreground">姝e湪涓嬭浇澶囦唤: {{ downloadProgress }}%</p>
+ </div>
+ </div>
+ </teleport>
+ </div>
+</template>
diff --git a/src/packages/effects/layouts/src/widgets/database-backup/index.ts b/src/packages/effects/layouts/src/widgets/database-backup/index.ts
new file mode 100644
index 0000000..f5a75e6
--- /dev/null
+++ b/src/packages/effects/layouts/src/widgets/database-backup/index.ts
@@ -0,0 +1 @@
+export { default as DatabaseBackupButton } from './database-backup.vue';
diff --git a/src/packages/effects/layouts/src/widgets/index.ts b/src/packages/effects/layouts/src/widgets/index.ts
index 665cdb9..7bb762c 100644
--- a/src/packages/effects/layouts/src/widgets/index.ts
+++ b/src/packages/effects/layouts/src/widgets/index.ts
@@ -12,3 +12,4 @@
export * from './theme-toggle';
export * from './timezone';
export * from './user-dropdown';
+export * from './database-backup';
diff --git a/src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue b/src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue
new file mode 100644
index 0000000..6a7d235
--- /dev/null
+++ b/src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue
@@ -0,0 +1,151 @@
+<script setup lang="ts">
+import { message } from 'ant-design-vue';
+import { Button, Input, Upload } from 'ant-design-vue';
+import { onMounted, ref } from 'vue';
+
+// 娉ㄦ剰锛氭湰鏂囦欢浣嶄簬 @vben 鐨� packages 鍐呴儴锛屾棤娉曚娇鐢ㄥ簲鐢ㄥ眰鍒悕 #/锛岄渶鐢ㄧ浉瀵硅矾寰勶紙涓庡悓鐩綍 custom.vue 淇濇寔涓�鑷达級
+import { publicUploadFile } from '../../../../../../../../api/system/storage';
+import { $t } from '../../../../../../../locales/src';
+import { useCompanyConfigStore } from '../../../../../../../../store/company-config';
+
+defineOptions({
+ name: 'PreferenceCompanyConfig',
+});
+
+const companyConfigStore = useCompanyConfigStore();
+
+const companyName = ref(companyConfigStore.companyName);
+const loginTitle = ref(companyConfigStore.loginTitle);
+const loginDescription = ref(companyConfigStore.loginDescription);
+const logoUrl = ref(companyConfigStore.logoUrl);
+const uploading = ref(false);
+
+// 鎵撳紑鍖哄潡鏃朵粠鍚庣鎷夊彇鏈�鏂板叕鍙搁厤缃苟鍥炲~琛ㄥ崟锛坰tore 鍒濆�煎彲鑳戒负绌猴紝鍥犲惎鍔ㄥ姞杞借蛋鐨勬槸绾嚱鏁帮級
+onMounted(async () => {
+ await companyConfigStore.loadFromServer();
+ companyName.value = companyConfigStore.companyName;
+ loginTitle.value = companyConfigStore.loginTitle;
+ loginDescription.value = companyConfigStore.loginDescription;
+ logoUrl.value = companyConfigStore.logoUrl;
+});
+
+/** 涓婁紶 logo锛堟案涔呭叕寮�锛屼緵鐧诲綍椤靛厤鐧诲綍灞曠ず锛� */
+async function handleBeforeUpload(file: File) {
+ uploading.value = true;
+ try {
+ const blobs = await publicUploadFile([file]);
+ const blob = blobs?.[0];
+ if (blob?.previewURL || blob?.url) {
+ logoUrl.value = blob.previewURL || blob.url || '';
+ message.success($t('preferences.company.uploadSuccess'));
+ } else {
+ message.error($t('preferences.company.uploadFailed'));
+ }
+ } catch {
+ message.error($t('preferences.company.uploadFailed'));
+ } finally {
+ uploading.value = false;
+ }
+ // 杩斿洖 false锛岄樆姝㈢粍浠堕粯璁や笂浼狅紙宸查�氳繃鎺ュ彛涓婁紶锛�
+ return false;
+}
+
+async function handleSave() {
+ if (!companyName.value.trim()) {
+ message.warning($t('preferences.company.companyNameRequired'));
+ return;
+ }
+ try {
+ await companyConfigStore.save({
+ companyName: companyName.value.trim(),
+ logoUrl: logoUrl.value || undefined,
+ loginTitle: loginTitle.value,
+ loginDescription: loginDescription.value,
+ });
+ message.success($t('preferences.company.saveSuccess'));
+ } catch {
+ message.error($t('preferences.company.saveFailed'));
+ }
+}
+</script>
+
+<template>
+ <div class="flex flex-col">
+ <!-- 鍏徃鍚嶇О -->
+ <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
+ <span class="shrink-0 text-sm text-foreground/80">
+ {{ $t('preferences.company.companyName') }}
+ </span>
+ <Input
+ v-model:value="companyName"
+ :placeholder="$t('preferences.company.companyNamePlaceholder')"
+ class="w-41.25"
+ allow-clear
+ />
+ </div>
+
+ <!-- 鐧诲綍椤垫爣棰� -->
+ <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
+ <span class="shrink-0 text-sm text-foreground/80">
+ {{ $t('preferences.company.loginTitle') }}
+ </span>
+ <Input
+ v-model:value="loginTitle"
+ :placeholder="$t('preferences.company.loginTitlePlaceholder')"
+ class="w-41.25"
+ allow-clear
+ />
+ </div>
+
+ <!-- 鐧诲綍椤垫弿杩� -->
+ <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
+ <span class="shrink-0 text-sm text-foreground/80">
+ {{ $t('preferences.company.loginDescription') }}
+ </span>
+ <Input
+ v-model:value="loginDescription"
+ :placeholder="$t('preferences.company.loginDescriptionPlaceholder')"
+ class="w-41.25"
+ allow-clear
+ />
+ </div>
+
+ <!-- 鍏徃 Logo -->
+ <div class="my-2 flex w-full items-center justify-between gap-4 rounded-md px-2 py-2">
+ <span class="shrink-0 text-sm text-foreground/80">
+ {{ $t('preferences.company.logo') }}
+ </span>
+ <div class="flex items-center gap-2">
+ <Upload
+ :show-upload-list="false"
+ accept="image/*"
+ :before-upload="handleBeforeUpload"
+ >
+ <Button size="small" :loading="uploading">
+ {{ $t('preferences.company.uploadLogo') }}
+ </Button>
+ </Upload>
+ <img
+ v-if="logoUrl"
+ :src="logoUrl"
+ alt="company-logo"
+ class="h-9 w-9 rounded border border-border object-contain"
+ />
+ <span v-else class="text-xs text-muted-foreground">
+ {{ $t('preferences.company.logoEmpty') }}
+ </span>
+ </div>
+ </div>
+
+ <div v-if="uploading" class="px-2 pb-1 text-xs text-muted-foreground">
+ {{ $t('preferences.company.uploading') }}
+ </div>
+
+ <!-- 淇濆瓨 -->
+ <div class="mt-3 px-2">
+ <Button type="primary" class="w-full" @click="handleSave">
+ {{ $t('preferences.company.save') }}
+ </Button>
+ </div>
+ </div>
+</template>
diff --git a/src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts b/src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts
index b1b8eac..5b62459 100644
--- a/src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts
+++ b/src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts
@@ -1,4 +1,5 @@
export { default as Block } from './block.vue';
+export { default as Company } from './company/company.vue';
export { default as Custom } from './custom/custom.vue';
export { default as Animation } from './general/animation.vue';
export { default as General } from './general/general.vue';
diff --git a/src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue b/src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
index 12f4076..5b3a05e 100644
--- a/src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
+++ b/src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
@@ -43,6 +43,7 @@
Breadcrumb,
BuiltinTheme,
ColorMode,
+ Company,
Content,
Copyright,
Custom,
@@ -256,6 +257,10 @@
label: $t('preferences.general'),
value: 'general',
},
+ {
+ label: $t('preferences.company.title'),
+ value: 'company',
+ },
];
if (showCustomTab.value) {
@@ -352,6 +357,7 @@
<VbenSegmented
v-model="activeTab"
:tabs="tabs"
+ scrollable
:class="{
'sticky-tabs-header': appEnableStickyPreferencesNavigationBar,
}"
@@ -526,6 +532,11 @@
/>
</Block>
</template>
+ <template #company>
+ <Block :title="$t('preferences.company.title')">
+ <Company />
+ </Block>
+ </template>
<template #custom>
<Block :title="customTabTitle">
<Custom
diff --git a/src/packages/locales/src/langs/en-US/preferences.json b/src/packages/locales/src/langs/en-US/preferences.json
index d027227..853651a 100644
--- a/src/packages/locales/src/langs/en-US/preferences.json
+++ b/src/packages/locales/src/langs/en-US/preferences.json
@@ -229,5 +229,24 @@
"placeholder": "Please enter the default report title"
}
}
+ },
+ "company": {
+ "title": "Company Info",
+ "companyName": "Company Name",
+ "companyNamePlaceholder": "Please enter company name",
+ "companyNameRequired": "Company name is required",
+ "loginTitle": "Login Page Title",
+ "loginTitlePlaceholder": "Enter login page title (optional)",
+ "loginDescription": "Login Page Description",
+ "loginDescriptionPlaceholder": "Enter login page description (optional)",
+ "logo": "Company Logo",
+ "uploadLogo": "Upload Logo",
+ "uploadSuccess": "Logo uploaded successfully",
+ "uploadFailed": "Logo upload failed",
+ "uploading": "Uploading...",
+ "logoEmpty": "No logo set",
+ "save": "Save",
+ "saveSuccess": "Company info saved successfully",
+ "saveFailed": "Failed to save company info"
}
}
diff --git a/src/packages/locales/src/langs/zh-CN/preferences.json b/src/packages/locales/src/langs/zh-CN/preferences.json
index 8cbc607..aa36730 100644
--- a/src/packages/locales/src/langs/zh-CN/preferences.json
+++ b/src/packages/locales/src/langs/zh-CN/preferences.json
@@ -229,5 +229,24 @@
"placeholder": "璇疯緭鍏ユ姤琛ㄩ粯璁ゆ爣棰�"
}
}
+ },
+ "company": {
+ "title": "鍏徃淇℃伅",
+ "companyName": "鍏徃鍚嶇О",
+ "companyNamePlaceholder": "璇疯緭鍏ュ叕鍙稿悕绉�",
+ "companyNameRequired": "鍏徃鍚嶇О涓嶈兘涓虹┖",
+ "loginTitle": "鐧诲綍椤垫爣棰�",
+ "loginTitlePlaceholder": "璇疯緭鍏ョ櫥褰曢〉鏍囬锛堝彲閫夛級",
+ "loginDescription": "鐧诲綍椤垫弿杩�",
+ "loginDescriptionPlaceholder": "璇疯緭鍏ョ櫥褰曢〉鎻忚堪锛堝彲閫夛級",
+ "logo": "鍏徃 Logo",
+ "uploadLogo": "涓婁紶 Logo",
+ "uploadSuccess": "Logo 涓婁紶鎴愬姛",
+ "uploadFailed": "Logo 涓婁紶澶辫触",
+ "uploading": "涓婁紶涓�...",
+ "logoEmpty": "鏈缃� Logo",
+ "save": "淇濆瓨",
+ "saveSuccess": "鍏徃淇℃伅淇濆瓨鎴愬姛",
+ "saveFailed": "鍏徃淇℃伅淇濆瓨澶辫触"
}
}
diff --git a/src/preferences.ts b/src/preferences.ts
index fbaa7b4..4da3a4e 100644
--- a/src/preferences.ts
+++ b/src/preferences.ts
@@ -4,6 +4,7 @@
} from './packages/preferences/src';
declare const __APP_LOGO__: string;
+declare const __APP_NAME__: string;
interface WebAntdPreferencesExtension {
defaultTableSize: number;
@@ -22,7 +23,7 @@
app: {
/** 鍚庣璺敱妯″紡 */
accessMode: 'backend',
- name: import.meta.env.VITE_APP_TITLE,
+ name: __APP_NAME__ || import.meta.env.VITE_APP_TITLE,
enableRefreshToken: true,
},
footer: {
@@ -31,7 +32,7 @@
fixed: false,
},
copyright: {
- companyName: import.meta.env.VITE_APP_TITLE,
+ companyName: __APP_NAME__ || import.meta.env.VITE_APP_TITLE,
companySiteLink: '#',
},
logo: {
diff --git a/src/router/guard.ts b/src/router/guard.ts
index 8b88c83..88a80ee 100644
--- a/src/router/guard.ts
+++ b/src/router/guard.ts
@@ -60,8 +60,7 @@
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
- userStore.userInfo?.homePath ||
- preferences.app.defaultHomePath,
+ '/analytics',
);
}
return true;
@@ -132,7 +131,7 @@
userStore.setUserRoles(userRoles);
const redirectPath = (from.query.redirect ??
(to.path === preferences.app.defaultHomePath
- ? userInfo?.homePath || preferences.app.defaultHomePath
+ ? '/analytics'
: to.fullPath)) as string;
return {
diff --git a/src/router/routes/modules/crm.ts b/src/router/routes/modules/crm.ts
index cdd75fe..c463b0b 100644
--- a/src/router/routes/modules/crm.ts
+++ b/src/router/routes/modules/crm.ts
@@ -83,6 +83,15 @@
},
component: () => import('#/views/crm/product/detail/index.vue'),
},
+ {
+ path: 'invoice/detail/:id',
+ name: 'CrmInvoiceDetail',
+ meta: {
+ title: '寮�绁ㄨ鎯�',
+ activePath: '/crm/invoice',
+ },
+ component: () => import('#/views/crm/invoice/detail/index.vue'),
+ },
],
},
];
diff --git a/src/router/routes/modules/mes.ts b/src/router/routes/modules/mes.ts
index de6c512..54d4328 100644
--- a/src/router/routes/modules/mes.ts
+++ b/src/router/routes/modules/mes.ts
@@ -29,6 +29,16 @@
},
component: () => import('#/views/mes/pd/project/detail/index.vue'),
},
+ {
+ path: 'pro/feedback/detail/:id',
+ name: 'MesProFeedbackDetail',
+ meta: {
+ title: '鐢熶骇鎶ュ伐璇︽儏',
+ activePath: '/mes/pro/feedback',
+ },
+ props: true,
+ component: () => import('#/views/mes/pro/feedback/detail/index.vue'),
+ },
],
},
];
diff --git a/src/store/auth.ts b/src/store/auth.ts
index 7d09f0e..3643cf0 100644
--- a/src/store/auth.ts
+++ b/src/store/auth.ts
@@ -86,9 +86,7 @@
// oxlint-disable-next-line no-unused-expressions
onSuccess
? await onSuccess?.()
- : await router.push(
- userInfo.homePath || preferences.app.defaultHomePath,
- );
+ : await router.push('/analytics');
}
if (userInfo?.nickname) {
diff --git a/src/store/company-config.ts b/src/store/company-config.ts
new file mode 100644
index 0000000..1668f86
--- /dev/null
+++ b/src/store/company-config.ts
@@ -0,0 +1,71 @@
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+
+import {
+ getCompanyConfig,
+ saveCompanyConfig as saveCompanyConfigApi,
+} from '#/api/system/company-config';
+import { applyCompanyConfig } from '#/utils/company-config';
+
+/**
+ * 鍏徃鍝佺墝淇℃伅鍏ㄥ眬閰嶇疆
+ *
+ * 鍚姩鏃朵粠鍚庣鍔犺浇锛屼繚瀛樻椂鍐欏洖鍚庣锛涘苟灏嗗叕鍙稿悕/logo 鍚屾鍒� preferences锛�
+ * 浠庤�岃仈鍔ㄧ櫥褰曢〉銆佽彍鍗曟爮銆佺綉椤垫爣棰樼瓑鎵�鏈夋秷璐圭偣銆�
+ */
+export const useCompanyConfigStore = defineStore('companyConfig', () => {
+ const companyName = ref('');
+ const logoUrl = ref('');
+ const loginTitle = ref('');
+ const loginDescription = ref('');
+
+ /** 灏嗗叕鍙镐俊鎭簲鐢ㄥ埌 preferences锛岃仈鍔ㄧ櫥褰曢〉/鑿滃崟鏍�/鏍囬 */
+ function applyToPreferences() {
+ applyCompanyConfig({
+ companyName: companyName.value,
+ logoUrl: logoUrl.value,
+ });
+ }
+
+ /** 浠庡悗绔姞杞藉叕鍙搁厤缃苟搴旂敤 */
+ async function loadFromServer() {
+ try {
+ const config = await getCompanyConfig();
+ if (config) {
+ companyName.value = config.companyName ?? '';
+ logoUrl.value = config.logoUrl ?? '';
+ loginTitle.value = config.loginTitle ?? '';
+ loginDescription.value = config.loginDescription ?? '';
+ applyToPreferences();
+ }
+ } catch {
+ // 鍚庣涓嶅彲鐢ㄦ垨鏈厤缃紝淇濇寔缂栬瘧鏈熼粯璁�
+ }
+ }
+
+ /** 淇濆瓨鍏徃閰嶇疆鍒板悗绔苟搴旂敤 */
+ async function save(data: {
+ companyName: string;
+ logoUrl?: string;
+ logoBlobId?: number;
+ loginTitle?: string;
+ loginDescription?: string;
+ }) {
+ await saveCompanyConfigApi(data);
+ companyName.value = data.companyName;
+ logoUrl.value = data.logoUrl ?? '';
+ loginTitle.value = data.loginTitle ?? '';
+ loginDescription.value = data.loginDescription ?? '';
+ applyToPreferences();
+ }
+
+ return {
+ companyName,
+ logoUrl,
+ loginTitle,
+ loginDescription,
+ applyToPreferences,
+ loadFromServer,
+ save,
+ };
+});
diff --git a/src/store/index.ts b/src/store/index.ts
index 269586e..10d8c3d 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1 +1,2 @@
export * from './auth';
+export * from './company-config';
diff --git a/src/types/app.d.ts b/src/types/app.d.ts
index d65d0ad..8067547 100644
--- a/src/types/app.d.ts
+++ b/src/types/app.d.ts
@@ -1,2 +1,3 @@
declare const __APP_LOGO__: string;
declare const __APP_FAVICON__: string;
+declare const __APP_NAME__: string;
diff --git a/src/utils/company-config.ts b/src/utils/company-config.ts
new file mode 100644
index 0000000..c44ccae
--- /dev/null
+++ b/src/utils/company-config.ts
@@ -0,0 +1,35 @@
+import { preferencesManager } from '@vben/preferences';
+
+/** 鍏徃鍝佺墝淇℃伅锛堜笌鍚庣 system_company_config 琛ㄥ瓧娈靛搴旓級 */
+export interface CompanyConfigData {
+ companyName?: string;
+ logoUrl?: string;
+ loginTitle?: string;
+ loginDescription?: string;
+}
+
+/**
+ * 灏嗗叕鍙稿搧鐗屼俊鎭簲鐢ㄥ埌鍏ㄥ眬锛�
+ * - 鍏徃鍚� 鈫� preferences.app.name / copyright.companyName锛堣仈鍔ㄨ彍鍗曟爮銆佺櫥褰曢〉銆佺綉椤垫爣棰樸�佺増鏉冿級
+ * - logo 鈫� preferences.logo.source锛堣仈鍔ㄧ櫥褰曢〉銆佷晶杈规爮 logo锛�
+ * - 鏈夊叕鍙稿悕鏃讹紝璁剧疆 document.title锛堝熀纭�鏍囬锛宒ynamicTitle 寮�鍚椂璺敱鏍囬鍦ㄦ鍩虹涓婃嫾鎺ワ級
+ */
+export function applyCompanyConfig(config: CompanyConfigData) {
+ const updates: Record<string, any> = {};
+ if (config.logoUrl) {
+ updates.logo = { source: config.logoUrl };
+ // 鍚屾缃戦〉鏍囬鍥炬爣锛坒avicon锛変负涓婁紶鐨� logo
+ const faviconLink = document.querySelector('link[rel="icon"]');
+ faviconLink?.setAttribute('href', config.logoUrl);
+ }
+ if (config.companyName) {
+ updates.app = { name: config.companyName };
+ updates.copyright = { companyName: config.companyName };
+ }
+ if (Object.keys(updates).length > 0) {
+ preferencesManager.updatePreferences(updates);
+ }
+ if (config.companyName) {
+ document.title = config.companyName;
+ }
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 0a4c03c..cdedaf7 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -30,3 +30,18 @@
});
return index;
};
+
+/**
+ * 鍘绘帀琛ㄦ牸鍒楃殑鍥哄畾锛坒ixed锛夐厤缃�
+ *
+ * 鐢ㄤ簬銆屾煡鐪嬪鎵规槑缁� / 璇︽儏銆嶇瓑鍙鍦烘櫙锛氭鏃惰〃鏍煎鍣ㄨ緝绐勶紝
+ * 鍥哄畾鍒椾細鎸ゅ崰鍙瀹藉害锛屾妸浜у搧鍚嶇О銆佸崟浣嶃�佸娉ㄧ瓑鍒楅伄浣忋��
+ *
+ * @param columns 鍘熷琛ㄦ牸鍒楅厤缃�
+ */
+export function withoutFixedColumns<T>(columns?: null | T[]): T[] {
+ return (columns ?? []).map((column) => {
+ const { fixed: _fixed, ...rest } = column as Recordable<any>;
+ return rest as unknown as T;
+ });
+}
diff --git a/src/views/aftersales/problem/data.ts b/src/views/aftersales/problem/data.ts
new file mode 100644
index 0000000..f6000f9
--- /dev/null
+++ b/src/views/aftersales/problem/data.ts
@@ -0,0 +1,252 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { AftersalesProblemApi } from '#/api/aftersales/problem';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type ProblemFormType = 'create' | 'update' | 'detail';
+
+/** 闂绫诲瀷閫夐」锛堝榻� AfterSaleIssueTypeEnum锛� */
+export const ISSUE_TYPE_OPTIONS = [
+ { label: '涓�鑸棶棰�', value: 1 },
+ { label: '缁翠慨闂', value: 2 },
+ { label: '閫�璐ч棶棰�', value: 3 },
+];
+
+/** 涓ラ噸绋嬪害閫夐」 */
+export const SEVERITY_LEVEL_OPTIONS = [
+ { label: '杞诲井', value: 0 },
+ { label: '涓�鑸�', value: 1 },
+ { label: '涓ラ噸', value: 2 },
+ { label: '鑷村懡', value: 3 },
+];
+
+/** 鏍瑰洜鍒嗙被閫夐」锛堝榻� AfterSaleRootCauseCategoryEnum锛� */
+export const ROOT_CAUSE_CATEGORY_OPTIONS = [
+ { label: '璁捐缂洪櫡', value: 1 },
+ { label: '鐗╂枡缂洪櫡', value: 2 },
+ { label: '宸ヨ壓缂洪櫡', value: 3 },
+ { label: '瑁呴厤缂洪櫡', value: 4 },
+ { label: '杩愯緭鎹熷潖', value: 5 },
+ { label: '鍖呰缂洪櫡', value: 6 },
+ { label: '浣跨敤涓嶅綋', value: 7 },
+ { label: '杞欢缂洪櫡', value: 8 },
+ { label: '鍏朵粬', value: 9 },
+];
+
+/** 闂鐘舵�侀�夐」锛堝榻� AfterSaleProblemStatusEnum锛� */
+export const PROBLEM_STATUS_OPTIONS = [
+ { label: '鑽夌', value: 0 },
+ { label: '宸插彂甯�', value: 1 },
+ { label: '宸插仠鐢�', value: 2 },
+];
+
+/** 闂搴撹〃鍗� */
+export function useProblemFormSchema(
+ formType: ProblemFormType,
+): VbenFormSchema[] {
+ const readonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'title',
+ label: '闂鏍囬',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ラ棶棰樻爣棰�' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'issueType',
+ label: '闂绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: ISSUE_TYPE_OPTIONS,
+ placeholder: '璇烽�夋嫨闂绫诲瀷',
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'issueCategory',
+ label: '闂鍒嗙被',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡傚姛鑳芥晠闅溿�佸瑙傜己闄�',
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'severityLevel',
+ label: '涓ラ噸绋嬪害',
+ component: 'Select',
+ componentProps: {
+ options: SEVERITY_LEVEL_OPTIONS,
+ placeholder: '璇烽�夋嫨',
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'rootCauseCategory',
+ label: '鏍瑰洜鍒嗙被',
+ component: 'Select',
+ componentProps: {
+ options: ROOT_CAUSE_CATEGORY_OPTIONS,
+ placeholder: '璇烽�夋嫨鏍瑰洜鍒嗙被',
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'itemCode',
+ label: '鐗╂枡缂栫爜',
+ component: 'Input',
+ componentProps: { placeholder: '鏉ユ簮浜庡伐鍗曟槑缁�', disabled: readonly },
+ },
+ {
+ fieldName: 'itemName',
+ label: '鐗╂枡鍚嶇О',
+ component: 'Input',
+ componentProps: { placeholder: '鏉ユ簮浜庡伐鍗曟槑缁�', disabled: readonly },
+ },
+ {
+ fieldName: 'batchCode',
+ label: '鎵规鍙�',
+ component: 'Input',
+ componentProps: { placeholder: '鏉ユ簮浜庡伐鍗曟槑缁�', disabled: readonly },
+ },
+ {
+ fieldName: 'description',
+ label: '闂鎻忚堪',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ rows: 3,
+ placeholder: '闂鐜拌薄涓庤儗鏅�',
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'rootCause',
+ label: '鏍瑰洜鍒嗘瀽',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ rows: 3,
+ placeholder: '缂洪櫡浜х敓鐨勬牴鏈師鍥�',
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'solution',
+ label: '瑙e喅鏂规',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ rows: 3,
+ placeholder: '澶勭悊鎺柦/SOP锛屼緵鍚庣画宸ュ崟鍙傝��',
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: { rows: 2, placeholder: '澶囨敞', disabled: readonly },
+ },
+ {
+ fieldName: 'blobIds',
+ label: '闄勪欢',
+ component: 'FileUpload',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ disabled: readonly,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 闂搴撴悳绱㈣〃鍗� */
+export function useProblemSearchFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'keyword',
+ label: '鍏抽敭瀛�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '鍗曞彿/鏍囬/鎻忚堪/鏂规',
+ },
+ },
+ {
+ fieldName: 'itemCode',
+ label: '鐗╂枡缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ョ墿鏂欑紪鐮�',
+ },
+ },
+ {
+ fieldName: 'issueCategory',
+ label: '闂鍒嗙被',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ラ棶棰樺垎绫�',
+ },
+ },
+ {
+ fieldName: 'issueType',
+ label: '闂绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: ISSUE_TYPE_OPTIONS,
+ placeholder: '璇烽�夋嫨',
+ },
+ },
+ {
+ fieldName: 'rootCauseCategory',
+ label: '鏍瑰洜鍒嗙被',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: ROOT_CAUSE_CATEGORY_OPTIONS,
+ placeholder: '璇烽�夋嫨',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: PROBLEM_STATUS_OPTIONS,
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 闂搴撳垪琛ㄥ瓧娈� */
+export function useProblemGridColumns(): VxeTableGridOptions<AftersalesProblemApi.Problem>['columns'] {
+ return [
+ { field: 'no', title: '闂鍗曞彿', minWidth: 170, slots: { default: 'no' } },
+ { field: 'title', title: '闂鏍囬', minWidth: 180 },
+ { field: 'sourceTicketNo', title: '鏉ユ簮宸ュ崟', minWidth: 150 },
+ { field: 'itemName', title: '鐗╂枡鍚嶇О', minWidth: 120 },
+ { field: 'itemCode', title: '鐗╂枡缂栫爜', minWidth: 120 },
+ { field: 'issueType', title: '闂绫诲瀷', width: 100, slots: { default: 'issueType' } },
+ { field: 'severityLevel', title: '涓ラ噸绋嬪害', width: 100, slots: { default: 'severityLevel' } },
+ { field: 'rootCauseCategory', title: '鏍瑰洜鍒嗙被', width: 100, slots: { default: 'rootCauseCategory' } },
+ { field: 'status', title: '鐘舵��', width: 90, slots: { default: 'status' } },
+ { field: 'ownerUserName', title: '娌夋穩浜�', minWidth: 100 },
+ { field: 'updateTime', title: '鏇存柊鏃堕棿', width: 170, formatter: 'formatDate' },
+ { title: '鎿嶄綔', width: 220, fixed: 'right', slots: { default: 'actions' } },
+ ];
+}
diff --git a/src/views/aftersales/problem/index.vue b/src/views/aftersales/problem/index.vue
new file mode 100644
index 0000000..372c34e
--- /dev/null
+++ b/src/views/aftersales/problem/index.vue
@@ -0,0 +1,214 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { AftersalesProblemApi } from '#/api/aftersales/problem';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteProblem,
+ disableProblem,
+ getProblemPage,
+ publishProblem,
+} from '#/api/aftersales/problem';
+
+import {
+ PROBLEM_STATUS_OPTIONS,
+ ROOT_CAUSE_CATEGORY_OPTIONS,
+ useProblemGridColumns,
+ useProblemSearchFormSchema,
+} from './data';
+import ProblemForm from './modules/form.vue';
+
+const statusLabelMap = Object.fromEntries(
+ PROBLEM_STATUS_OPTIONS.map((o) => [o.value, o.label]),
+);
+const rootCauseLabelMap = Object.fromEntries(
+ ROOT_CAUSE_CATEGORY_OPTIONS.map((o) => [o.value, o.label]),
+);
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: ProblemForm,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleDetail(row: AftersalesProblemApi.Problem) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+function handleEdit(row: AftersalesProblemApi.Problem) {
+ formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+async function handleDelete(row: AftersalesProblemApi.Problem) {
+ await deleteProblem(row.id!);
+ message.success('鍒犻櫎鎴愬姛');
+ handleRefresh();
+}
+
+async function handlePublish(row: AftersalesProblemApi.Problem) {
+ await publishProblem(row.id!);
+ message.success('鍙戝竷鎴愬姛');
+ handleRefresh();
+}
+
+async function handleDisable(row: AftersalesProblemApi.Problem) {
+ await disableProblem(row.id!);
+ message.success('鍋滅敤鎴愬姛');
+ handleRefresh();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useProblemSearchFormSchema(),
+ },
+ gridOptions: {
+ columns: useProblemGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getProblemPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<AftersalesProblemApi.Problem>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+
+ <Grid table-title="鍞悗闂搴撳垪琛�">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鏂板闂',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['aftersales:problem:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+
+ <template #no="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.no }}
+ </Button>
+ </template>
+
+ <template #issueType="{ row }">
+ <Tag v-if="row.issueType === 1" color="blue">涓�鑸棶棰�</Tag>
+ <Tag v-else-if="row.issueType === 2" color="orange">缁翠慨闂</Tag>
+ <Tag v-else-if="row.issueType === 3" color="red">閫�璐ч棶棰�</Tag>
+ <span v-else>-</span>
+ </template>
+
+ <template #severityLevel="{ row }">
+ <Tag v-if="row.severityLevel === 0" color="green">杞诲井</Tag>
+ <Tag v-else-if="row.severityLevel === 1" color="blue">涓�鑸�</Tag>
+ <Tag v-else-if="row.severityLevel === 2" color="orange">涓ラ噸</Tag>
+ <Tag v-else-if="row.severityLevel === 3" color="red">鑷村懡</Tag>
+ <span v-else>-</span>
+ </template>
+
+ <template #rootCauseCategory="{ row }">
+ <span>{{ rootCauseLabelMap[row.rootCauseCategory!] || '-' }}</span>
+ </template>
+
+ <template #status="{ row }">
+ <Tag
+ :color="
+ row.status === 1
+ ? 'green'
+ : row.status === 0
+ ? 'default'
+ : 'orange'
+ "
+ >
+ {{ statusLabelMap[row.status!] || '-' }}
+ </Tag>
+ </template>
+
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鏌ョ湅',
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: '缂栬緫',
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['aftersales:problem:update'],
+ ifShow: row.status === 0,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鍙戝竷',
+ type: 'link',
+ auth: ['aftersales:problem:publish'],
+ ifShow: row.status === 0 || row.status === 2,
+ popConfirm: {
+ title: '纭鍙戝竷璇ラ棶棰橈紵',
+ confirm: handlePublish.bind(null, row),
+ },
+ },
+ {
+ label: '鍋滅敤',
+ type: 'link',
+ auth: ['aftersales:problem:disable'],
+ ifShow: row.status === 1,
+ popConfirm: {
+ title: '纭鍋滅敤璇ラ棶棰橈紵',
+ confirm: handleDisable.bind(null, row),
+ },
+ },
+ {
+ label: '鍒犻櫎',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['aftersales:problem:delete'],
+ ifShow: row.status === 0,
+ popConfirm: {
+ title: `纭鍒犻櫎闂銆�${row.no}銆戯紵`,
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/aftersales/problem/modules/form.vue b/src/views/aftersales/problem/modules/form.vue
new file mode 100644
index 0000000..37ec0ec
--- /dev/null
+++ b/src/views/aftersales/problem/modules/form.vue
@@ -0,0 +1,139 @@
+<script lang="ts" setup>
+import type { ProblemFormType } from '../data';
+
+import type { AftersalesProblemApi } from '#/api/aftersales/problem';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { Button, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createProblem,
+ getProblem,
+ updateProblem,
+} from '#/api/aftersales/problem';
+
+import { useProblemFormSchema } from '../data';
+
+defineOptions({ name: 'AftersalesProblemForm' });
+
+const emit = defineEmits(['success']);
+
+const formType = ref<ProblemFormType>('create');
+const id = ref<number>();
+const submitting = ref(false);
+
+const isEdit = computed(() => formType.value !== 'detail');
+const title = computed(() => {
+ if (formType.value === 'detail') return '鏌ョ湅鍞悗闂';
+ if (id.value) return '缂栬緫鍞悗闂';
+ return '鏂板鍞悗闂';
+});
+
+// 涓昏〃鍗�
+const [Form, formApi] = useVbenForm({
+ commonConfig: { componentProps: { class: 'w-full' }, formItemClass: 'col-span-1', labelWidth: 100 },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-2',
+});
+
+/** 鎶婇棶棰樺璞℃槧灏勬垚琛ㄥ崟鍊硷紙闄勪欢杞负 FileUpload 缁撴瀯锛� */
+function problemToForm(p: AftersalesProblemApi.Problem): Record<string, unknown> {
+ const form: Record<string, unknown> = { ...p };
+ if (p.attachmentList?.length) {
+ form.blobIds = p.attachmentList.map((item) => ({
+ uid: String(item.id),
+ name: item.name || '',
+ url: item.url || '',
+ status: 'done',
+ id: item.id,
+ }));
+ }
+ return form;
+}
+
+/** 鎶婅〃鍗曢噷鐨勯檮浠跺�艰鏁存垚 blobId 鏁扮粍 */
+function normalizeBlobIds(value: unknown): number[] {
+ const arr = Array.isArray(value) ? value : [];
+ return arr
+ .map((x) =>
+ typeof x === 'number'
+ ? x
+ : x && typeof x === 'object' && 'id' in (x as Record<string, unknown>)
+ ? Number((x as Record<string, unknown>).id)
+ : Number(x),
+ )
+ .filter((n) => Number.isFinite(n));
+}
+
+// Modal
+const [Modal, modalApi] = useVbenModal({
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ id.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{ formType: ProblemFormType; id?: number }>();
+ formType.value = data.formType;
+ id.value = data.id;
+ formApi.setState({ schema: useProblemFormSchema(formType.value) });
+ formApi.setDisabled(formType.value === 'detail');
+ if (data.id) {
+ modalApi.lock();
+ try {
+ const detail = await getProblem(data.id);
+ await formApi.setValues(problemToForm(detail));
+ } finally {
+ modalApi.unlock();
+ }
+ } else {
+ await formApi.resetForm();
+ }
+ },
+});
+
+async function handleSubmit() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ submitting.value = true;
+ try {
+ const data = (await formApi.getValues()) as Record<string, unknown>;
+ const blobIds = normalizeBlobIds(data.blobIds);
+ const payload = { ...data, blobIds } as AftersalesProblemApi.Problem;
+ if (id.value) {
+ await updateProblem({ ...payload, id: id.value });
+ } else {
+ await createProblem(payload);
+ }
+ message.success('鎿嶄綔鎴愬姛');
+ await modalApi.close();
+ emit('success');
+ } catch (e: unknown) {
+ message.error((e as Error).message || '鎻愪氦澶辫触');
+ } finally {
+ submitting.value = false;
+ }
+}
+</script>
+
+<template>
+ <Modal :title="title" class="w-2/3">
+ <Form class="mx-4" />
+
+ <template #footer>
+ <div class="flex justify-end gap-2">
+ <Button @click="modalApi.close()">鍙栨秷</Button>
+ <Button v-if="isEdit" type="primary" :loading="submitting" @click="handleSubmit">
+ 纭
+ </Button>
+ <Button v-else type="primary" @click="modalApi.close()">鍏抽棴</Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/aftersales/ticket/detail.vue b/src/views/aftersales/ticket/detail.vue
index b9a6d7e..092ebc2 100644
--- a/src/views/aftersales/ticket/detail.vue
+++ b/src/views/aftersales/ticket/detail.vue
@@ -7,7 +7,17 @@
import { Page } from '@vben/common-ui';
import { AftersaleIssueTypeEnum, AftersaleTicketStatusEnum } from '@vben/constants';
-import { Button, Card, Descriptions, Input, message, Popconfirm, Select, Tag } from 'ant-design-vue';
+import {
+ Button,
+ Card,
+ Descriptions,
+ Input,
+ message,
+ Popconfirm,
+ Select,
+ Switch,
+ Tag,
+} from 'ant-design-vue';
import {
assignTicket,
@@ -17,6 +27,11 @@
getTicket,
resolveTicket,
} from '#/api/aftersales/ticket';
+import type { AftersalesVisitApi } from '#/api/aftersales/visit';
+import {
+ createVisit,
+ getVisitListByTicketId,
+} from '#/api/aftersales/visit';
defineOptions({ name: 'AftersalesTicketDetail' });
@@ -32,6 +47,32 @@
const isPendingReturn = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.PENDING_RETURN);
const isPendingRefund = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.PENDING_REFUND);
const isResolved = computed(() => ticket.value?.status === AftersaleTicketStatusEnum.RESOLVED);
+/** 鍥炶鏄惁宸查�氳繃锛堟弧鎰忎笖鏈鍙戯級 */
+const isVisitPassed = computed(() => ticket.value?.visitStatus === 1);
+
+// 鍥炶璁板綍
+const visits = ref<AftersalesVisitApi.Visit[]>([]);
+const showVisitForm = ref(false);
+const visitForm = ref<AftersalesVisitApi.CreateVisit>({
+ ticketId: 0,
+ visitType: 0,
+ content: '',
+ satisfied: true,
+ recurred: false,
+});
+
+const visitTypeOptions = [
+ { label: '鐢佃瘽鍥炶', value: 0 },
+ { label: '涓婇棬鍥炶', value: 1 },
+ { label: '杩滅▼鍥炶', value: 2 },
+ { label: '鍏朵粬', value: 3 },
+];
+const visitTypeMap: Record<number, string> = {
+ 0: '鐢佃瘽',
+ 1: '涓婇棬',
+ 2: '杩滅▼',
+ 3: '鍏朵粬',
+};
// 鍒嗛厤澶勭悊浜�
const showAssignInput = ref(false);
@@ -86,6 +127,30 @@
} finally {
loading.value = false;
}
+}
+
+async function loadVisits() {
+ visits.value = await getVisitListByTicketId(ticketId.value);
+}
+
+function toggleVisitForm() {
+ showVisitForm.value = !showVisitForm.value;
+ if (showVisitForm.value) {
+ visitForm.value = {
+ ticketId: ticketId.value,
+ visitType: 0,
+ content: '',
+ satisfied: true,
+ recurred: false,
+ };
+ }
+}
+
+async function handleCreateVisit() {
+ await createVisit({ ...visitForm.value, ticketId: ticketId.value });
+ message.success('鍥炶鐧昏鎴愬姛');
+ showVisitForm.value = false;
+ await Promise.all([loadVisits(), loadTicket()]);
}
function toggleAssignInput() {
@@ -161,6 +226,7 @@
onMounted(() => {
loadTicket();
+ loadVisits();
});
</script>
@@ -195,6 +261,14 @@
</Descriptions.Item>
<Descriptions.Item label="璐熻矗浜�">{{ ticket.ownerUserName || '-' }}</Descriptions.Item>
<Descriptions.Item label="澶勭悊浜�">{{ ticket.handlerUserName || '-' }}</Descriptions.Item>
+ <Descriptions.Item label="鍥炶鐘舵��">
+ <Tag v-if="ticket.visitStatus === 1" color="green">宸插洖璁块�氳繃</Tag>
+ <Tag v-else color="orange">寰呭洖璁�</Tag>
+ </Descriptions.Item>
+ <Descriptions.Item label="闂娌夋穩" :span="2">
+ <template v-if="ticket.problemNo">{{ ticket.problemNo }}</template>
+ <template v-else>-</template>
+ </Descriptions.Item>
<Descriptions.Item label="闂鎻忚堪" :span="3">
{{ ticket.issueDescription || '-' }}
</Descriptions.Item>
@@ -253,6 +327,101 @@
>
{{ file.name || '闄勪欢-' + file.id }}
</a>
+ </div>
+ </Card>
+
+ <!-- 鍥炶璁板綍 -->
+ <Card v-if="ticket" title="鍥炶璁板綍" class="mb-4">
+ <template #extra>
+ <Button
+ v-if="isResolved && !showVisitForm"
+ type="primary"
+ size="small"
+ @click="toggleVisitForm"
+ >
+ 鐧昏鍥炶
+ </Button>
+ </template>
+
+ <div v-if="isResolved" class="mb-3 text-sm text-gray-500">
+ 鐧昏銆屾弧鎰� 涓� 鏈鍙戙�嶇殑鍥炶鍚庯紝宸ュ崟鎵嶅彲褰掓。锛涚櫥璁般�屼笉婊℃剰鎴栧鍙戙�嶄細鑷姩閫�鍥炲鐞嗕腑銆�
+ </div>
+
+ <!-- 鐧昏鍥炶琛ㄥ崟 -->
+ <div
+ v-if="showVisitForm"
+ class="mb-4 flex flex-wrap items-center gap-3 rounded bg-gray-50 p-3"
+ >
+ <div class="flex items-center gap-1">
+ <span class="text-sm text-gray-500">鍥炶鏂瑰紡锛�</span>
+ <Select
+ v-model:value="visitForm.visitType"
+ :options="visitTypeOptions"
+ style="width: 120px"
+ />
+ </div>
+ <div class="flex items-center gap-1">
+ <span class="text-sm text-gray-500">鍥炶鍐呭锛�</span>
+ <Input
+ v-model:value="visitForm.content"
+ placeholder="瀹㈡埛鍙嶉鍐呭"
+ style="width: 260px"
+ />
+ </div>
+ <div class="flex items-center gap-1">
+ <span class="text-sm text-gray-500">鏄惁婊℃剰锛�</span>
+ <Switch
+ v-model:checked="visitForm.satisfied"
+ checked-children="婊℃剰"
+ un-checked-children="涓嶆弧鎰�"
+ />
+ </div>
+ <div class="flex items-center gap-1">
+ <span class="text-sm text-gray-500">鏄惁澶嶅彂锛�</span>
+ <Switch
+ v-model:checked="visitForm.recurred"
+ checked-children="澶嶅彂"
+ un-checked-children="鏈鍙�"
+ />
+ </div>
+ <Button type="primary" size="small" @click="handleCreateVisit">鎻愪氦</Button>
+ <Button size="small" @click="toggleVisitForm">鍙栨秷</Button>
+ </div>
+
+ <!-- 鍥炶璁板綍鍒楄〃 -->
+ <div v-if="visits.length === 0" class="text-sm text-gray-400">鏆傛棤鍥炶璁板綍</div>
+ <div v-else class="overflow-x-auto">
+ <table class="w-full border text-sm">
+ <thead class="bg-gray-50">
+ <tr>
+ <th class="border p-2 text-left">鍥炶鏃堕棿</th>
+ <th class="border p-2 text-left">鏂瑰紡</th>
+ <th class="border p-2 text-left">鍥炶浜�</th>
+ <th class="border p-2 text-center">婊℃剰</th>
+ <th class="border p-2 text-center">澶嶅彂</th>
+ <th class="border p-2 text-left">鍐呭</th>
+ <th class="border p-2 text-left">澶囨敞</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="visit in visits" :key="visit.id">
+ <td class="border p-2">{{ visit.visitTime }}</td>
+ <td class="border p-2">{{ visitTypeMap[visit.visitType!] ?? '-' }}</td>
+ <td class="border p-2">{{ visit.visitorUserName || '-' }}</td>
+ <td class="border p-2 text-center">
+ <Tag :color="visit.satisfied ? 'green' : 'red'">
+ {{ visit.satisfied ? '婊℃剰' : '涓嶆弧鎰�' }}
+ </Tag>
+ </td>
+ <td class="border p-2 text-center">
+ <Tag v-if="visit.recurred" color="orange">澶嶅彂</Tag>
+ <Tag v-else color="default">鏈鍙�</Tag>
+ </td>
+ <td class="border p-2">{{ visit.content || '-' }}</td>
+ <td class="border p-2">{{ visit.remark || '-' }}</td>
+ </tr>
+ </tbody>
+ </table>
</div>
</Card>
@@ -323,13 +492,13 @@
<Button type="primary">瀹岀粨</Button>
</Popconfirm>
- <!-- 鍏抽棴 -->
+ <!-- 褰掓。锛堜粎鍥炶閫氳繃鍚庡彲褰掓。锛涙湭鍥炶璇峰湪鈥滃洖璁胯褰曗�濋噷鐧昏锛� -->
<Popconfirm
- v-if="isResolved"
- title="纭鍏抽棴璇ュ伐鍗曪紵鍏抽棴鍚庝笉鍙仮澶嶃��"
+ v-if="isResolved && isVisitPassed"
+ title="纭褰掓。璇ュ伐鍗曪紵褰掓。鍚庤嚜鍔ㄦ矇娣�涓�鏉¢棶棰橈紝宸ュ崟鍙涓嶅彲鎭㈠銆�"
@confirm="handleClose"
>
- <Button type="primary">鍏抽棴</Button>
+ <Button type="primary">褰掓。</Button>
</Popconfirm>
<!-- 鍙栨秷 -->
diff --git a/src/views/aftersales/ticket/index.vue b/src/views/aftersales/ticket/index.vue
index 269a460..edd0d78 100644
--- a/src/views/aftersales/ticket/index.vue
+++ b/src/views/aftersales/ticket/index.vue
@@ -3,6 +3,7 @@
import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
import { ref } from 'vue';
+import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { AftersaleTicketStatusEnum } from '@vben/constants';
@@ -16,6 +17,16 @@
import { useTicketGridColumns, useTicketGridFormSchema } from './data';
import Form from './modules/form.vue';
+
+const router = useRouter();
+
+/** 鏌ョ湅宸ュ崟锛堣繘鍏ヨ矾鐢辫鎯呴〉锛屽彲鐧昏鍥炶/褰掓。锛� */
+function handleDetail(row: AftersalesTicketApi.Ticket) {
+ router.push({
+ name: 'AftersalesTicketDetail',
+ params: { id: row.id },
+ });
+}
const sceneType = ref('1');
@@ -38,11 +49,6 @@
/** 鍒涘缓宸ュ崟 */
function handleCreate() {
formModalApi.setData({ formType: 'create' }).open();
-}
-
-/** 鏌ョ湅宸ュ崟 */
-function handleDetail(row: AftersalesTicketApi.Ticket) {
- formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
/** 缂栬緫宸ュ崟 */
diff --git a/src/views/basicData/mdm/data.ts b/src/views/basicData/mdm/data.ts
index a9d6ca9..ca625c3 100644
--- a/src/views/basicData/mdm/data.ts
+++ b/src/views/basicData/mdm/data.ts
@@ -333,6 +333,17 @@
},
},
{
+ component: 'Switch',
+ fieldName: 'syncMes',
+ label: '鍚屾鍒癕ES',
+ defaultValue: true,
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(true),
+ },
+ {
component: "RadioGroup",
fieldName: "status",
label: "鐘舵��",
@@ -597,7 +608,7 @@
{
field: "syncedMes",
title: "鍚屾鐘舵��",
- width: 90,
+ width: 100,
fixed: "right",
slots: { default: "syncedMes" },
},
@@ -619,3 +630,27 @@
},
];
}
+
+/** 鐗╂枡瀵煎叆鐨勮〃鍗� */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '鐗╂枡鏁版嵁',
+ component: 'Upload',
+ rules: 'required',
+ help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '鏄惁瑕嗙洊',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勭墿鏂欐暟鎹�',
+ },
+ ];
+}
diff --git a/src/views/basicData/mdm/index.vue b/src/views/basicData/mdm/index.vue
index e59b39f..af385ba 100644
--- a/src/views/basicData/mdm/index.vue
+++ b/src/views/basicData/mdm/index.vue
@@ -8,26 +8,33 @@
import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src';
-import { message, Tag } from 'ant-design-vue';
+import { message, Switch, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteItem,
deleteItemList,
- // exportItem,
+ exportItem,
getItemPage,
updateItemStatus,
+ updateItemSync,
} from '#/api/mdm/item';
import { getItemTypeSimpleList } from '#/api/mes/md/item/type';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
defineOptions({ name: 'MdmItem' });
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -74,15 +81,20 @@
gridApi.query();
}
-/** 瀵煎嚭琛ㄦ牸 */
-// async function handleExport() {
-// const data = await exportItem(await gridApi.formApi.getValues());
-// downloadFileFromBlobPart({ fileName: '鐗╂枡.xls', source: data });
-// }
+/** 瀵煎嚭琛ㄦ牸锛堟寜褰撳墠鍒楄〃鏌ヨ鏉′欢瀵煎嚭鍏ㄩ儴鍛戒腑鏁版嵁锛� */
+async function handleExport() {
+ const data = await exportItem(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '鐗╂枡鏁版嵁.xls', source: data });
+}
/** 鍒涘缓鐗╂枡 */
function handleCreate() {
formModalApi.setData(null).open();
+}
+
+/** 瀵煎叆鐗╂枡 */
+function handleImport() {
+ importModalApi.open();
}
/** 缂栬緫鐗╂枡 */
@@ -135,6 +147,23 @@
handleRefresh();
} finally {
hideLoading();
+ }
+}
+
+const syncingId = ref<number | null>(null);
+
+/** 鍒囨崲鍚屾鐘舵�侊細鍏抽棴寮�鍏充細浠� MES 鍒犻櫎鐗╂枡锛岄渶纭 */
+async function handleSyncChange(row: MdmItemApi.Item, checked: boolean) {
+ if (!checked) {
+ await confirm('鍏抽棴鍚屾灏嗕粠 MES 鍒犻櫎璇ョ墿鏂欏強鍏宠仈閰嶇疆锛圔OM/SOP/SIP/鎵规閰嶇疆锛夛紝纭缁х画锛�');
+ }
+ syncingId.value = row.id!;
+ try {
+ await updateItemSync(row.id!, checked);
+ message.success(checked ? '宸插悓姝ュ埌 MES' : '宸插彇娑堝悓姝�');
+ handleRefresh();
+ } finally {
+ syncingId.value = null;
}
}
@@ -193,6 +222,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<Grid table-title="鐗╂枡鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -204,13 +234,20 @@
auth: ['mdm:item:create'],
onClick: handleCreate,
},
- // {
- // label: $t('ui.actionTitle.export'),
- // type: 'primary',
- // icon: ACTION_ICON.DOWNLOAD,
- // auth: ['mdm:item:export'],
- // onClick: handleExport,
- // },
+ {
+ label: $t('ui.actionTitle.import', ['鐗╂枡']),
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['mdm:item:import'],
+ onClick: handleImport,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mdm:item:export'],
+ onClick: handleExport,
+ },
{
label: $t('ui.actionTitle.deleteBatch'),
type: 'primary',
@@ -232,8 +269,13 @@
<Tag v-else>鍚�</Tag>
</template>
<template #syncedMes="{ row }">
- <Tag v-if="row.syncedMes" color="success">宸插悓姝�</Tag>
- <Tag v-else color="default">鏈悓姝�</Tag>
+ <Switch
+ :checked="row.syncedMes"
+ :loading="syncingId === row.id"
+ checkedChildren="宸插悓姝�"
+ unCheckedChildren="鏈悓姝�"
+ @change="handleSyncChange(row, $event)"
+ />
</template>
<template #actions="{ row }">
<TableAction
diff --git a/src/views/basicData/mdm/modules/form.vue b/src/views/basicData/mdm/modules/form.vue
index c1f5eb7..5d82e00 100644
--- a/src/views/basicData/mdm/modules/form.vue
+++ b/src/views/basicData/mdm/modules/form.vue
@@ -67,6 +67,8 @@
modalApi.lock();
try {
formData.value = await getItem(data.id);
+ // 鍚屾鐘舵�佹槧灏勶細璇︽儏鎺ュ彛杩斿洖 syncedMes锛岃〃鍗曞瓧娈典负 syncMes
+ formData.value.syncMes = formData.value.syncedMes ?? true;
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
diff --git a/src/views/basicData/mdm/modules/import-form.vue b/src/views/basicData/mdm/modules/import-form.vue
new file mode 100644
index 0000000..5cc47a2
--- /dev/null
+++ b/src/views/basicData/mdm/modules/import-form.vue
@@ -0,0 +1,105 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+
+import { h } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, notification, Upload } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { importItem, importItemTemplate } from '#/api/mdm/item';
+import { $t } from '#/locales';
+
+import { useImportFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ formItemClass: 'col-span-2',
+ labelWidth: 120,
+ },
+ layout: 'horizontal',
+ schema: useImportFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = await formApi.getValues();
+ try {
+ const response = (await importItem(data.file, data.updateSupport)) as any;
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+
+ const resultData = response?.data || response || {};
+ const failureList = resultData.failureList || {};
+
+ if (Object.keys(failureList).length > 0) {
+ const errorNodes = Object.entries(failureList).map(([name, reason]) => {
+ return h('div', `${name}: ${reason}`);
+ });
+
+ notification.warning({
+ message: '瀵煎叆瀹屾垚锛岄儴鍒嗙墿鏂欏鍏ュけ璐�',
+ description: () => h('div', errorNodes),
+ duration: null,
+ });
+ } else {
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+/** 涓婁紶鍓� */
+function beforeUpload(file: FileType) {
+ formApi.setFieldValue('file', file);
+ return false;
+}
+
+/** 涓嬭浇妯$増 */
+async function handleDownload() {
+ const data = await importItemTemplate();
+ downloadFileFromBlobPart({ fileName: '鐗╂枡瀵煎叆妯℃澘.xls', source: data });
+}
+</script>
+
+<template>
+ <Modal title="瀵煎叆鐗╂枡" class="w-1/3">
+ <Form class="mx-4">
+ <template #file>
+ <div class="w-full">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :before-upload="beforeUpload"
+ >
+ <Button type="primary"> 閫夋嫨 Excel 鏂囦欢 </Button>
+ </Upload>
+ </div>
+ </template>
+ </Form>
+ <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
+ 鎻愮ず锛氭ā鏉夸腑鈥滅墿鏂欏垎绫荤紪鐮佲�濄�佲�滆閲忓崟浣嶇紪鐮佲�濄�佲�滆緟鍗曚綅缂栫爜鈥濊濉啓瀵瑰簲涓绘暟鎹殑缂栫爜锛屽彲鐣欑┖銆�
+ <div>鈥滄槸鍚︽壒娆$鐞嗏�濄�佲�滄槸鍚﹀悓姝ュ埌MES鈥濆~ true/false銆�</div>
+ <div>鈥滅姸鎬佲�濆~ 0锛堝惎鐢級/1锛堢鐢級銆�</div>
+ </div>
+ <template #prepend-footer>
+ <div class="flex flex-auto items-center">
+ <Button @click="handleDownload"> 涓嬭浇瀵煎叆妯℃澘 </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/basicData/mdm/unit/data.ts b/src/views/basicData/mdm/unit/data.ts
index b9d0be3..6e15fe9 100644
--- a/src/views/basicData/mdm/unit/data.ts
+++ b/src/views/basicData/mdm/unit/data.ts
@@ -128,4 +128,28 @@
slots: { default: 'actions' },
},
];
+}
+
+/** 璁¢噺鍗曚綅瀵煎叆鐨勮〃鍗� */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '鍗曚綅鏁版嵁',
+ component: 'Upload',
+ rules: 'required',
+ help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '鏄惁瑕嗙洊',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勫崟浣嶆暟鎹�',
+ },
+ ];
}
\ No newline at end of file
diff --git a/src/views/basicData/mdm/unit/index.vue b/src/views/basicData/mdm/unit/index.vue
index e2d3a44..4a9fca8 100644
--- a/src/views/basicData/mdm/unit/index.vue
+++ b/src/views/basicData/mdm/unit/index.vue
@@ -3,23 +3,31 @@
import type { MdmUnitApi } from '#/api/mdm/unit';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteUnit,
+ exportUnit,
getUnitPage,
} from '#/api/mdm/unit';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
defineOptions({ name: 'MdmUnit' });
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -31,6 +39,17 @@
/** 鍒涘缓璁¢噺鍗曚綅 */
function handleCreate() {
formModalApi.setData(null).open();
+}
+
+/** 瀵煎叆璁¢噺鍗曚綅 */
+function handleImport() {
+ importModalApi.open();
+}
+
+/** 瀵煎嚭璁¢噺鍗曚綅锛堟寜褰撳墠鍒楄〃鏌ヨ鏉′欢瀵煎嚭鍏ㄩ儴鍛戒腑鏁版嵁锛� */
+async function handleExport() {
+ const data = await exportUnit(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅鏁版嵁.xls', source: data });
}
/** 缂栬緫璁¢噺鍗曚綅 */
@@ -87,6 +106,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<Grid table-title="璁¢噺鍗曚綅鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -98,6 +118,20 @@
auth: ['mdm:unit:create'],
onClick: handleCreate,
},
+ {
+ label: $t('ui.actionTitle.import', ['璁¢噺鍗曚綅']),
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['mdm:unit-measure:import'],
+ onClick: handleImport,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mdm:unit-measure:export'],
+ onClick: handleExport,
+ },
]"
/>
</template>
diff --git a/src/views/basicData/mdm/unit/modules/import-form.vue b/src/views/basicData/mdm/unit/modules/import-form.vue
new file mode 100644
index 0000000..de36691
--- /dev/null
+++ b/src/views/basicData/mdm/unit/modules/import-form.vue
@@ -0,0 +1,105 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+
+import { h } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, notification, Upload } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { importUnit, importUnitTemplate } from '#/api/mdm/unit';
+import { $t } from '#/locales';
+
+import { useImportFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ formItemClass: 'col-span-2',
+ labelWidth: 120,
+ },
+ layout: 'horizontal',
+ schema: useImportFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = await formApi.getValues();
+ try {
+ const response = (await importUnit(data.file, data.updateSupport)) as any;
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+
+ const resultData = response?.data || response || {};
+ const failureList = resultData.failureList || {};
+
+ if (Object.keys(failureList).length > 0) {
+ const errorNodes = Object.entries(failureList).map(([name, reason]) => {
+ return h('div', `${name}: ${reason}`);
+ });
+
+ notification.warning({
+ message: '瀵煎叆瀹屾垚锛岄儴鍒嗚閲忓崟浣嶅鍏ュけ璐�',
+ description: () => h('div', errorNodes),
+ duration: null,
+ });
+ } else {
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+/** 涓婁紶鍓� */
+function beforeUpload(file: FileType) {
+ formApi.setFieldValue('file', file);
+ return false;
+}
+
+/** 涓嬭浇妯$増 */
+async function handleDownload() {
+ const data = await importUnitTemplate();
+ downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅瀵煎叆妯℃澘.xls', source: data });
+}
+</script>
+
+<template>
+ <Modal title="瀵煎叆璁¢噺鍗曚綅" class="w-1/3">
+ <Form class="mx-4">
+ <template #file>
+ <div class="w-full">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :before-upload="beforeUpload"
+ >
+ <Button type="primary"> 閫夋嫨 Excel 鏂囦欢 </Button>
+ </Upload>
+ </div>
+ </template>
+ </Form>
+ <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
+ 鎻愮ず锛氭ā鏉夸腑鐨勨�滀富鍗曚綅缂栫爜鈥濊濉啓涓诲崟浣嶇殑缂栫爜锛堝 PC锛夛紝涓诲崟浣嶆湰韬鍒楃暀绌哄嵆鍙��
+ <div>鈥滄槸鍚︿富鍗曚綅鈥濆~ true/false銆�</div>
+ <div>鈥滅姸鎬佲�濆~ 0锛堝惎鐢級/1锛堢鐢級銆�</div>
+ </div>
+ <template #prepend-footer>
+ <div class="flex flex-auto items-center">
+ <Button @click="handleDownload"> 涓嬭浇瀵煎叆妯℃澘 </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/bpm/businessAttach/data.ts b/src/views/bpm/businessAttach/data.ts
new file mode 100644
index 0000000..f642965
--- /dev/null
+++ b/src/views/bpm/businessAttach/data.ts
@@ -0,0 +1,163 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { z } from '#/adapter/form';
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'processDefinitionKey',
+ label: '娴佺▼瀹氫箟 Key',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ祦绋嬪畾涔� Key锛屽 purchase_approve',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'businessType',
+ label: '涓氬姟绫诲瀷',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨嬶紝濡� erp_purchase_order',
+ },
+ },
+ {
+ fieldName: 'componentPath',
+ label: '涓氬姟璇︽儏缁勪欢璺緞',
+ component: 'Input',
+ componentProps: {
+ placeholder:
+ '璇疯緭鍏ヤ笟鍔¤鎯呯粍浠惰矾寰勶紝濡� /erp/purchase/order/detail/index.vue',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ buttonStyle: 'solid',
+ optionType: 'button',
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'processDefinitionKey',
+ label: '娴佺▼瀹氫箟 Key',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ祦绋嬪畾涔� Key',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'businessType',
+ label: '涓氬姟绫诲瀷',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヤ笟鍔$被鍨�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'createTime',
+ label: '鍒涘缓鏃堕棿',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'id',
+ title: '缂栧彿',
+ minWidth: 80,
+ },
+ {
+ field: 'processDefinitionKey',
+ title: '娴佺▼瀹氫箟 Key',
+ minWidth: 180,
+ },
+ {
+ field: 'businessType',
+ title: '涓氬姟绫诲瀷',
+ minWidth: 160,
+ },
+ {
+ field: 'componentPath',
+ title: '涓氬姟璇︽儏缁勪欢璺緞',
+ minWidth: 260,
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ minWidth: 80,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 160,
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ minWidth: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/bpm/businessAttach/index.vue b/src/views/bpm/businessAttach/index.vue
new file mode 100644
index 0000000..10c506d
--- /dev/null
+++ b/src/views/bpm/businessAttach/index.vue
@@ -0,0 +1,129 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteBusinessAttach,
+ getBusinessAttachPage,
+} from '#/api/bpm/businessAttach';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鍒涘缓涓氬姟闄勪欢閰嶇疆 */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫涓氬姟闄勪欢閰嶇疆 */
+function handleEdit(row: BpmBusinessAttachApi.BusinessAttach) {
+ formModalApi.setData(row).open();
+}
+
+/** 鍒犻櫎涓氬姟闄勪欢閰嶇疆 */
+async function handleDelete(row: BpmBusinessAttachApi.BusinessAttach) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.processDefinitionKey]),
+ duration: 0,
+ });
+ try {
+ await deleteBusinessAttach(row.id as number);
+ message.success($t('ui.actionMessage.deleteSuccess'));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getBusinessAttachPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<BpmBusinessAttachApi.BusinessAttach>,
+});
+</script>
+
+<template>
+ <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Grid table-title="涓氬姟闄勪欢閰嶇疆">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['涓氬姟闄勪欢閰嶇疆']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['bpm:business-attach:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['bpm:business-attach:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['bpm:business-attach:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [
+ row.processDefinitionKey,
+ ]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/bpm/businessAttach/modules/form.vue b/src/views/bpm/businessAttach/modules/form.vue
new file mode 100644
index 0000000..62e57dc
--- /dev/null
+++ b/src/views/bpm/businessAttach/modules/form.vue
@@ -0,0 +1,88 @@
+<script lang="ts" setup>
+import type { BpmBusinessAttachApi } from '#/api/bpm/businessAttach';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createBusinessAttach,
+ getBusinessAttach,
+ updateBusinessAttach,
+} from '#/api/bpm/businessAttach';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<BpmBusinessAttachApi.BusinessAttach>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['涓氬姟闄勪欢閰嶇疆'])
+ : $t('ui.actionTitle.create', ['涓氬姟闄勪欢閰嶇疆']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-2',
+ labelWidth: 140,
+ },
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = (await formApi.getValues()) as BpmBusinessAttachApi.BusinessAttach;
+ try {
+ await (formData.value?.id
+ ? updateBusinessAttach(data)
+ : createBusinessAttach(data));
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ // 鍔犺浇鏁版嵁
+ const data = modalApi.getData<BpmBusinessAttachApi.BusinessAttach>();
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getBusinessAttach(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/3">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/bpm/model/data.ts b/src/views/bpm/model/data.ts
index 59405e7..c1be1bb 100644
--- a/src/views/bpm/model/data.ts
+++ b/src/views/bpm/model/data.ts
@@ -40,6 +40,12 @@
slots: { default: 'deploymentTime' },
},
{
+ field: 'autoApprove',
+ title: '鍏嶅鎵�',
+ minWidth: 110,
+ slots: { default: 'autoApprove' },
+ },
+ {
title: '鎿嶄綔',
width: 150,
fixed: 'right',
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index 91c504f..2ed2171 100644
--- a/src/views/bpm/model/index.vue
+++ b/src/views/bpm/model/index.vue
@@ -1,15 +1,37 @@
<script lang="ts" setup>
import type { ModelCategoryInfo } from '#/api/bpm/model';
-import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue';
+import {
+ computed,
+ onActivated,
+ onMounted,
+ reactive,
+ ref,
+ useTemplateRef,
+ watch,
+} from 'vue';
+import { useAccess } from '@vben/access';
import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { useSortable } from '@vueuse/integrations/useSortable';
-import { Button, Card, Dropdown, Input, Menu, message } from 'ant-design-vue';
+import {
+ Button,
+ Card,
+ Dropdown,
+ Input,
+ Menu,
+ message,
+ Switch,
+ Tooltip,
+} from 'ant-design-vue';
+import {
+ getAutoApproveList,
+ updateGlobalAutoApprove,
+} from '#/api/bpm/autoApprove';
import {
getCategorySimpleList,
updateCategorySortBatch,
@@ -26,6 +48,17 @@
});
const modelListSpinning = ref(false); // 妯″瀷鍒楄〃鍔犺浇鐘舵��
+
+/** 鍏ㄥ眬鍏嶅鎵规�诲紑鍏冲崰鐢ㄧ殑娴佺▼ Key锛堜笌鍚庣 BpmProcessAutoApproveService.GLOBAL_KEY 淇濇寔涓�鑷达級 */
+const GLOBAL_AUTO_APPROVE_KEY = '*';
+const globalAutoApprove = ref(false); // 鍏ㄥ眬鍏嶅鎵规�诲紑鍏�
+const globalAutoApproveLoading = ref(false); // 鎬诲紑鍏虫彁浜ょ姸鎬�
+
+const { hasAccessByCodes } = useAccess();
+/** 鍏嶅鎵瑰紑鍏崇殑鏉冮檺锛氭棤鏉冮檺鏃跺紑鍏崇疆鐏帮紝閬垮厤鐐逛簡鎵嶆姤閿� */
+const hasPermiAutoApprove = computed(() => {
+ return hasAccessByCodes(['bpm:process-auto-approve:update']);
+});
const saveSortLoading = ref(false); // 淇濆瓨鎺掑簭鐘舵��
const categoryGroup = ref<ModelCategoryInfo[]>([]); // 鎸夌収 category 鍒嗙粍鐨勬暟鎹�
@@ -59,8 +92,22 @@
async function getList() {
modelListSpinning.value = true;
try {
- const modelList = await getModelList(queryParams.name);
- const categoryList = await getCategorySimpleList();
+ const [modelList, categoryList, autoApproveList] = await Promise.all([
+ getModelList(queryParams.name),
+ getCategorySimpleList(),
+ getAutoApproveList(),
+ ]);
+ // 鍏嶅鎵归厤缃寜銆屾祦绋� Key銆嶅缓绔嬫槧灏勶紝鍚堝苟鍒版ā鍨嬭鏁版嵁涓婏紝渚涘垪琛ㄥ唴鐨勫厤瀹℃壒寮�鍏冲垪鐩存帴璇诲彇
+ const autoApproveMap = new Map<string, boolean>();
+ autoApproveList.forEach((config) => {
+ autoApproveMap.set(config.processDefinitionKey, config.autoApprove);
+ });
+ modelList.forEach((model: any) => {
+ model.autoApprove = autoApproveMap.get(model.key) ?? false;
+ });
+ // 鍏ㄥ眬鎬诲紑鍏充互鐗规畩 Key 瀛樻斁鍦ㄥ悓涓�鏉¢厤缃噷
+ globalAutoApprove.value =
+ autoApproveMap.get(GLOBAL_AUTO_APPROVE_KEY) ?? false;
// 鎸夌収 category 鑱氬悎
categoryGroup.value = categoryList.map((category: any) => ({
...category,
@@ -76,6 +123,11 @@
}
/** 鍒濆鍖� */
+onMounted(() => {
+ getList();
+});
+
+/** 婵�娲绘椂鍒锋柊锛坘eep-alive 缂撳瓨鍦烘櫙涓嬩粠缂栬緫椤佃繑鍥炴椂瑙﹀彂锛� */
onActivated(() => {
getList();
});
@@ -137,6 +189,28 @@
sortableInstance.value.option('disabled', true);
}
}
+
+/** 鍒囨崲鍏ㄥ眬鍏嶅鎵规�诲紑鍏筹細寮�鍚悗鎵�鏈夋祦绋嬩竴寰嬪厤瀹℃壒 */
+async function handleGlobalAutoApproveChange(checked: boolean) {
+ globalAutoApprove.value = checked;
+ globalAutoApproveLoading.value = true;
+ try {
+ await updateGlobalAutoApprove(checked);
+ message.success(
+ checked
+ ? '宸插紑鍚叏灞�鍏嶅鎵癸細鎵�鏈夋祦绋嬫彁浜ゅ悗灏嗙洿鎺ラ�氳繃'
+ : '宸插叧闂叏灞�鍏嶅鎵癸細鎭㈠涓烘寜鍚勬祦绋嬪紑鍏冲垽瀹�',
+ );
+ // 閲嶆柊鎷夊彇鍒楄〃锛屽悓姝ュ悇娴佺▼寮�鍏崇殑灞曠ず
+ await getList();
+ } catch (error) {
+ // 澶辫触鍥炴粴
+ globalAutoApprove.value = !checked;
+ console.error('璁剧疆鍏ㄥ眬鍏嶅鎵瑰紑鍏冲け璐�', error);
+ } finally {
+ globalAutoApproveLoading.value = false;
+ }
+}
</script>
<template>
@@ -151,6 +225,16 @@
>
<template #extra>
<div v-if="!isCategorySorting">
+ <Tooltip title="寮�鍚悗鎵�鏈夋祦绋嬩竴寰嬪厤瀹℃壒锛堜紭鍏堜簬鍚勬祦绋嬭嚜宸辩殑寮�鍏筹級">
+ <span class="mr-1">鍏ㄩ儴鍏嶅鎵�</span>
+ </Tooltip>
+ <Switch
+ :checked="globalAutoApprove"
+ :loading="globalAutoApproveLoading"
+ :disabled="!hasPermiAutoApprove"
+ class="mr-3"
+ @change="(checked: any) => handleGlobalAutoApproveChange(checked)"
+ />
<Input
v-model:value="queryParams.name"
placeholder="鎼滅储娴佺▼"
diff --git a/src/views/bpm/model/modules/category-draggable-model.vue b/src/views/bpm/model/modules/category-draggable-model.vue
index 4071c1e..4950b9c 100644
--- a/src/views/bpm/model/modules/category-draggable-model.vue
+++ b/src/views/bpm/model/modules/category-draggable-model.vue
@@ -21,11 +21,13 @@
Dropdown,
Menu,
message,
+ Switch,
Tag,
Tooltip,
} from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { updateAutoApproveByKey } from '#/api/bpm/autoApprove';
import { deleteCategory } from '#/api/bpm/category';
import {
cleanModel,
@@ -64,6 +66,7 @@
const isModelSorting = ref(false);
const originalData = ref<BpmModelApi.Model[]>([]);
const modelList = ref<BpmModelApi.Model[]>([]);
+const autoApproveLoadingId = ref<null | number>(null); // 鍏嶅鎵瑰紑鍏筹細姝e湪鎻愪氦鐨勬ā鍨嬬紪鍙�
// TODO @jason锛氬彲浠ュ叏閮ㄥ睍寮�涔堬紵 @鑺嬭壙 涓婃璁ㄨ銆傚ソ鍍忔槸鍥犱负鎬ц兘闂鎵嶅彧灞曞紑绗竴涓垎绫�
const isExpand = ref(props.isFirst); // 鏍规嵁鏄惁涓虹涓�涓垎绫�, 鏉ヨ缃垵濮嬪睍寮�鐘舵��
@@ -81,6 +84,10 @@
});
const hasPermiDeploy = computed(() => {
return hasAccessByCodes(['bpm:model:deploy']);
+});
+/** 鍏嶅鎵瑰紑鍏崇殑鏉冮檺锛氭棤鏉冮檺鏃跺紑鍏崇疆鐏帮紝閬垮厤鐐逛簡鎵嶆姤閿� */
+const hasPermiAutoApprove = computed(() => {
+ return hasAccessByCodes(['bpm:process-auto-approve:update']);
});
const [Grid, gridApi] = useVbenVxeGrid({
@@ -287,6 +294,32 @@
message.success(`鍙戝竷[${row.name}]娴佺▼鎴愬姛`);
// 鍒锋柊鍒楄〃
emit('success');
+}
+
+/** 鍒囨崲鍏嶅鎵瑰紑鍏筹細寮�鍚悗璇ユ祦绋嬫彁浜ゆ椂涓嶅啀杩涘叆浜哄伐瀹℃壒锛岀敱绯荤粺鑷姩閫氳繃 */
+async function handleAutoApproveChange(checked: boolean, row: any) {
+ const oldValue = row.autoApprove;
+ // 涔愯鏇存柊锛岄伩鍏嶇瓑寰呮帴鍙f湡闂村紑鍏崇姸鎬佸洖璺�
+ row.autoApprove = checked;
+ gridApi.setGridOptions({ data: modelList.value });
+ autoApproveLoadingId.value = row.id;
+ try {
+ await updateAutoApproveByKey(row.key, checked);
+ message.success(
+ checked
+ ? `宸插紑鍚痆${row.name}]鍏嶅鎵癸細璇ユ祦绋嬫彁浜ゅ悗灏嗙敱绯荤粺鑷姩閫氳繃`
+ : `宸插叧闂璠${row.name}]鍏嶅鎵癸細鎭㈠涓烘甯镐汉宸ュ鎵筦,
+ );
+ // 閲嶆柊鎷夊彇鍒楄〃锛屼繚璇佸墠绔姸鎬佷笌鍚庣涓�鑷�
+ emit('success');
+ } catch (error) {
+ // 澶辫触鍥炴粴
+ row.autoApprove = oldValue;
+ gridApi.setGridOptions({ data: modelList.value });
+ console.error('璁剧疆鍏嶅鎵瑰紑鍏冲け璐�', error);
+ } finally {
+ autoApproveLoadingId.value = null;
+ }
}
/** '鏇村'鎿嶄綔鎸夐挳 */
@@ -661,6 +694,28 @@
</Tag>
</div>
</template>
+ <template #autoApprove="{ row }">
+ <div class="flex items-center gap-1">
+ <Switch
+ :checked="row.autoApprove"
+ :loading="autoApproveLoadingId === row.id"
+ :disabled="!hasPermiAutoApprove"
+ size="small"
+ @change="
+ (checked: any) => handleAutoApproveChange(checked, row)
+ "
+ />
+ <Tooltip
+ v-if="!row.processDefinition"
+ title="娴佺▼灏氭湭鍙戝竷锛屽厤瀹℃壒寮�鍏充笉浼氱敓鏁堬紝璇峰厛鍙戝竷娴佺▼"
+ >
+ <IconifyIcon
+ icon="lucide:alert-circle"
+ class="text-base text-orange-400"
+ />
+ </Tooltip>
+ </div>
+ </template>
<template #actions="{ row }">
<div class="flex items-center space-x-0">
<Button
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
index f4d8288..4ac3c9e 100644
--- a/src/views/bpm/processInstance/detail/index.vue
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -4,6 +4,8 @@
import { nextTick, onMounted, ref, shallowRef, watch } from 'vue';
+import { useRoute } from 'vue-router';
+
import { Page, useVbenModal } from '@vben/common-ui';
import {
BpmFieldPermissionType,
@@ -41,11 +43,16 @@
defineOptions({ name: 'BpmProcessInstanceDetail' });
+const route = useRoute();
+
const props = defineProps<{
activityId?: string; // 娴佺▼娲诲姩缂栧彿锛岀敤浜庢妱閫佹煡鐪�
id: string; // 娴佺▼瀹炰緥鐨勭紪鍙�
taskId?: string; // 浠诲姟缂栧彿
}>();
+
+/** 鍙鏌ョ湅妯″紡锛氫粠涓氬姟妯″潡锛堝閿�鍞鍗曞鎵硅繘搴︼級璺宠浆鏃堕殣钘忓鎵规搷浣滄寜閽紝浠呮煡鐪� */
+const viewOnly = route.query.viewOnly === '1';
const processInstanceLoading = ref(false); // 娴佺▼瀹炰緥鐨勫姞杞戒腑
const processInstance = ref<BpmProcessInstanceApi.ProcessInstance>(); // 娴佺▼瀹炰緥
@@ -362,7 +369,7 @@
</div>
<template #actions>
- <div class="px-4">
+ <div v-if="!viewOnly" class="px-4">
<ProcessInstanceOperationButton
ref="operationButtonRef"
:process-instance="processInstance"
@@ -374,6 +381,9 @@
@success="refresh"
/>
</div>
+ <div v-else class="px-4 py-2 text-sm text-muted-foreground">
+ <span>鍙鏌ョ湅妯″紡锛氬鎵规搷浣滆鍦ㄣ�屽崗鍚屽姙鍏� 鈫� 寰呭姙浠诲姟銆嶄腑澶勭悊</span>
+ </div>
</template>
</Card>
<!-- 鎵撳嵃瀵硅瘽妗� -->
diff --git a/src/views/bpm/task/done/data.ts b/src/views/bpm/task/done/data.ts
index 3c5e9f6..66aae84 100644
--- a/src/views/bpm/task/done/data.ts
+++ b/src/views/bpm/task/done/data.ts
@@ -146,7 +146,7 @@
},
{
title: '鎿嶄綔',
- width: 120,
+ width: 180,
fixed: 'right',
slots: { default: 'actions' },
},
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
index 753eda8..4bf1c3a 100644
--- a/src/views/bpm/task/done/index.vue
+++ b/src/views/bpm/task/done/index.vue
@@ -7,6 +7,7 @@
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { reverseApprove } from '#/api/erp/purchase/order';
import { getTaskDonePage, withdrawTask } from '#/api/bpm/task';
import { router } from '#/router';
@@ -34,6 +35,28 @@
try {
await withdrawTask(row.id);
message.success('鎾ゅ洖鎴愬姛');
+ await gridApi.query();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鏄惁閲囪喘璁㈠崟瀹℃壒浠诲姟锛堢敤浜庢樉绀�"鍙嶅鎵�"銆侀殣钘忛�氱敤"鎾ゅ洖"锛� */
+function isPurchaseOrderApprove(row: BpmTaskApi.Task) {
+ return (row.processInstance?.processDefinitionId || '').startsWith(
+ 'purchase_order_approve',
+ );
+}
+
+/** 鍙嶅鎵归噰璐鍗曪紙宸插鏍� -> 鏈鏍革紝鍦ㄥ崗鍚屽姙鍏凡鍔炰换鍔′腑鎿嶄綔锛� */
+async function handleReverseApprove(row: BpmTaskApi.Task) {
+ const hideLoading = message.loading({
+ content: '鍙嶅鎵逛腑...',
+ duration: 0,
+ });
+ try {
+ await reverseApprove(row.processInstance.id);
+ message.success('鍙嶅鎵规垚鍔燂紝璁㈠崟宸查噸鏂拌繘鍏ュ緟鍔炲鎵�');
await gridApi.query();
} finally {
hideLoading();
@@ -77,10 +100,22 @@
<TableAction
:actions="[
{
+ label: '鍙嶅鎵�',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.AUDIT,
+ ifShow: () => isPurchaseOrderApprove(row),
+ popConfirm: {
+ title: '纭畾瑕佸弽瀹℃壒璇ラ噰璐鍗曞悧锛熻鍗曞皢閲嶆柊杩涘叆寰呭姙瀹℃壒',
+ confirm: handleReverseApprove.bind(null, row),
+ },
+ },
+ {
label: '鎾ゅ洖',
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
+ ifShow: () => !isPurchaseOrderApprove(row),
popConfirm: {
title: '纭畾瑕佹挙鍥炶浠诲姟鍚楋紵',
confirm: handleWithdraw.bind(null, row),
diff --git a/src/views/crm/backlog/modules/receivable-audit-list.vue b/src/views/crm/backlog/modules/receivable-audit-list.vue
index 83aeff5..a2a80be 100644
--- a/src/views/crm/backlog/modules/receivable-audit-list.vue
+++ b/src/views/crm/backlog/modules/receivable-audit-list.vue
@@ -5,22 +5,29 @@
import { useRouter } from 'vue-router';
-import { Button } from 'ant-design-vue';
+import { Button, message } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getReceivablePage } from '#/api/crm/receivable';
+import { getReceivablePage, updateReceivableStatus } from '#/api/crm/receivable';
import { useGridColumns } from '#/views/crm/receivable/data';
import { AUDIT_STATUS } from '../data';
const { push } = useRouter();
-/** 鏌ョ湅瀹℃壒 */
-function handleProcessDetail(row: CrmReceivableApi.Receivable) {
- push({
- name: 'BpmProcessInstanceDetail',
- query: { id: row.processInstanceId },
+/** 瀹℃牳 */
+async function handleAudit(row: CrmReceivableApi.Receivable) {
+ const hideLoading = message.loading({
+ content: '瀹℃牳涓�...',
+ duration: 0,
});
+ try {
+ await updateReceivableStatus(row.id!, 20);
+ message.success('瀹℃牳鎴愬姛');
+ gridApi.query();
+ } finally {
+ hideLoading();
+ }
}
/** 鎵撳紑鍥炴璇︽儏 */
@@ -38,7 +45,7 @@
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
}
-const [Grid] = useVbenVxeGrid({
+const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: [
{
@@ -98,7 +105,13 @@
</Button>
</template>
<template #actions="{ row }">
- <Button type="link" @click="handleProcessDetail(row)">鏌ョ湅瀹℃壒</Button>
+ <Button
+ v-if="row.auditStatus === 10"
+ type="link"
+ @click="handleAudit(row)"
+ >
+ 瀹℃牳
+ </Button>
</template>
</Grid>
</template>
diff --git a/src/views/crm/business/detail/index.vue b/src/views/crm/business/detail/index.vue
index 258d021..a303220 100644
--- a/src/views/crm/business/detail/index.vue
+++ b/src/views/crm/business/detail/index.vue
@@ -87,7 +87,9 @@
/** 杞Щ鍟嗘満 */
function handleTransfer() {
- transferModalApi.setData({ bizType: BizTypeEnum.CRM_BUSINESS }).open();
+ transferModalApi
+ .setData({ id: businessId.value, bizType: BizTypeEnum.CRM_BUSINESS })
+ .open();
}
/** 鏇存柊鍟嗘満鐘舵�佹搷浣� */
diff --git a/src/views/crm/clue/detail/index.vue b/src/views/crm/clue/detail/index.vue
index 1b20c46..0966eb1 100644
--- a/src/views/crm/clue/detail/index.vue
+++ b/src/views/crm/clue/detail/index.vue
@@ -78,7 +78,9 @@
/** 杞Щ绾跨储 */
function handleTransfer() {
- transferModalApi.setData({ bizType: BizTypeEnum.CRM_CLUE }).open();
+ transferModalApi
+ .setData({ id: clueId.value, bizType: BizTypeEnum.CRM_CLUE })
+ .open();
}
/** 杞寲涓哄鎴� */
diff --git a/src/views/crm/contact/detail/index.vue b/src/views/crm/contact/detail/index.vue
index 1513e6e..f11d7fe 100644
--- a/src/views/crm/contact/detail/index.vue
+++ b/src/views/crm/contact/detail/index.vue
@@ -80,7 +80,9 @@
/** 杞Щ鑱旂郴浜� */
function handleTransfer() {
- transferModalApi.setData({ id: contactId.value }).open();
+ transferModalApi
+ .setData({ id: contactId.value, bizType: BizTypeEnum.CRM_CONTACT })
+ .open();
}
/** 鍔犺浇鏁版嵁 */
diff --git a/src/views/crm/contract/components/data.ts b/src/views/crm/contract/components/data.ts
index c314faa..9305a46 100644
--- a/src/views/crm/contract/components/data.ts
+++ b/src/views/crm/contract/components/data.ts
@@ -53,6 +53,18 @@
},
},
{
+ title: '宸插紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'totalInvoicePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '鏈紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'unInvoicePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
title: '璐熻矗浜�',
field: 'ownerUserName',
minWidth: 150,
diff --git a/src/views/crm/contract/data.ts b/src/views/crm/contract/data.ts
index 2903856..7dabc09 100644
--- a/src/views/crm/contract/data.ts
+++ b/src/views/crm/contract/data.ts
@@ -415,6 +415,18 @@
},
},
{
+ title: '宸插紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'totalInvoicePrice',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '鏈紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'unInvoicePrice',
+ minWidth: 140,
+ formatter: 'formatAmount2',
+ },
+ {
title: '鏈�鍚庤窡杩涙椂闂�',
field: 'contactLastTime',
minWidth: 180,
diff --git a/src/views/crm/contract/detail/approve/index.vue b/src/views/crm/contract/detail/approve/index.vue
new file mode 100644
index 0000000..adc3157
--- /dev/null
+++ b/src/views/crm/contract/detail/approve/index.vue
@@ -0,0 +1,190 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmContractApi } from '#/api/crm/contract';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, nextTick, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ formatDateTime,
+} from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getContract } from '#/api/crm/contract';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'CrmContractApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<CrmContractApi.Contract>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+/** 鍚堝悓浜у搧鏄庣粏锛堝鎵硅鎯呭彧璇诲垪锛� */
+function useProductColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ { field: 'itemName', title: '浜у搧鍚嶇О', minWidth: 180 },
+ { field: 'itemCode', title: '鐗╂枡缂栫爜', minWidth: 120 },
+ { field: 'itemUnitName', title: '鍗曚綅', minWidth: 80, formatter: ({ cellValue }) => textRender(cellValue) },
+ {
+ field: 'contractPrice',
+ title: '鍚堝悓浠锋牸锛堝厓锛�',
+ minWidth: 120,
+ formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'count',
+ title: '鏁伴噺',
+ minWidth: 100,
+ formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'totalPrice',
+ title: '鍚堣閲戦锛堝厓锛�',
+ minWidth: 120,
+ formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+ },
+ ];
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useProductColumns(),
+ data: [],
+ minHeight: 200,
+ autoResize: true,
+ border: true,
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ pagerConfig: {
+ enabled: false,
+ },
+ toolbarConfig: {
+ enabled: false,
+ },
+ },
+});
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鍚堝悓缂栧彿', field: 'no', render: textRender },
+ { label: '鍚堝悓鍚嶇О', field: 'name', render: textRender },
+ { label: '瀹㈡埛鍚嶇О', field: 'customerName', render: textRender },
+ { label: '鍟嗘満鍚嶇О', field: 'businessName', render: textRender },
+ {
+ label: '鍚堝悓閲戦锛堝厓锛�',
+ field: 'totalPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '瀹氶噾閲戦锛堝厓锛�',
+ field: 'depositPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '鍥炴閲戦锛堝厓锛�',
+ field: 'totalReceivablePrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '宸插紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'totalInvoicePrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '鏈紑绁ㄩ噾棰濓紙鍏冿級',
+ field: 'unInvoicePrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '涓嬪崟鏃堕棿',
+ field: 'orderDate',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '鍚堝悓寮�濮嬫椂闂�',
+ field: 'startTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '鍚堝悓缁撴潫鏃堕棿',
+ field: 'endTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '瀹㈡埛绛剧害浜�', field: 'signContactName', render: textRender },
+ { label: '鍏徃绛剧害浜�', field: 'signUserName', render: textRender },
+ { label: '閿�鍞鍗�', field: 'orderNo', render: textRender },
+ { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '鍚堝悓鐘舵��',
+ field: 'auditStatus',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getContract(Number(props.id));
+ if (formData.value?.products?.length) {
+ await nextTick();
+ await gridApi.grid.reloadData(formData.value.products);
+ }
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 鍚堝悓鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鍚堝悓鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ <!-- 浜у搧鏄庣粏 -->
+ <div
+ v-if="formData?.products?.length"
+ class="rounded-lg border border-border bg-background p-4"
+ >
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">浜у搧鏄庣粏</span>
+ </div>
+ <Grid />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/crm/contract/detail/data.ts b/src/views/crm/contract/detail/data.ts
index 63bc602..ae82b00 100644
--- a/src/views/crm/contract/detail/data.ts
+++ b/src/views/crm/contract/detail/data.ts
@@ -35,6 +35,11 @@
render: (val) => erpPriceInputFormatter(val) as string,
},
{
+ field: 'totalInvoicePrice',
+ label: '宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
field: 'ownerUserName',
label: '璐熻矗浜�',
},
@@ -66,6 +71,16 @@
render: (val) => erpPriceInputFormatter(val) as string,
},
{
+ field: 'totalInvoicePrice',
+ label: '宸插紑绁ㄩ噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
+ field: 'unInvoicePrice',
+ label: '鏈紑绁ㄩ噾棰濓紙鍏冿級',
+ render: (val) => erpPriceInputFormatter(val) as string,
+ },
+ {
field: 'depositPrice',
label: '瀹氶噾閲戦锛堝厓锛�',
render: (val) => erpPriceInputFormatter(val) as string,
diff --git a/src/views/crm/contract/detail/index.vue b/src/views/crm/contract/detail/index.vue
index 43242b5..972e42f 100644
--- a/src/views/crm/contract/detail/index.vue
+++ b/src/views/crm/contract/detail/index.vue
@@ -85,7 +85,9 @@
/** 杞Щ鍚堝悓 */
function handleTransfer() {
- transferModalApi.setData({ bizType: BizTypeEnum.CRM_CONTRACT }).open();
+ transferModalApi
+ .setData({ id: contractId.value, bizType: BizTypeEnum.CRM_CONTRACT })
+ .open();
}
/** 鍔犺浇鏁版嵁 */
diff --git a/src/views/crm/contract/index.vue b/src/views/crm/contract/index.vue
index cd385ed..b4f4f4b 100644
--- a/src/views/crm/contract/index.vue
+++ b/src/views/crm/contract/index.vue
@@ -23,12 +23,18 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
const { push } = useRouter();
const sceneType = ref('1');
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -51,6 +57,11 @@
...formValues,
});
downloadFileFromBlobPart({ fileName: '鍚堝悓.xls', source: data });
+}
+
+/** 瀵煎叆鍚堝悓 */
+function handleImport() {
+ importModalApi.open();
}
/** 鍒涘缓鍚堝悓 */
@@ -205,6 +216,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<Grid>
<template #toolbar-actions>
<Tabs class="w-full" @change="handleChangeSceneType">
@@ -230,6 +242,13 @@
auth: ['crm:contract:export'],
onClick: handleExport,
},
+ {
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['crm:contract:import'],
+ onClick: handleImport,
+ },
]"
/>
</template>
diff --git a/src/views/crm/contract/modules/import-form.vue b/src/views/crm/contract/modules/import-form.vue
new file mode 100644
index 0000000..0cd990d
--- /dev/null
+++ b/src/views/crm/contract/modules/import-form.vue
@@ -0,0 +1,155 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/crm/contract';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import { importContract, importContractTemplate } from '#/api/crm/contract';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importContract(fileList.value[0] as File);
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importContractTemplate();
+ downloadFileFromBlobPart({ fileName: '鍚堝悓瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureList ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆鍚堝悓" class="w-[720px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+ <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涘繀濉垪锛氬悎鍚屽悕绉般�佸鎴峰悕绉般�佷骇鍝佺紪鐮併�佹暟閲忋�傗�滃鎴峰悕绉扳�濋渶涓� CRM 瀹㈡埛涓�鑷达紱鈥滀骇鍝佺紪鐮佲�濇寜鐗╂枡缂栫爜濉啓锛堚�滀骇鍝佸悕绉扳�濆垪浠呬緵瀵圭収锛夛紱鈥滃悎鍚屽崟浠�(鍏�)鈥濈暀绌哄垯鍙栫墿鏂欓攢鍞环銆�</p>
+ <p>涓�寮犲悎鍚屽彲鍚鏉℃槑缁嗭細鐩稿悓鈥滃悎鍚岀紪鍙封�濈殑澶氳浼氬悎骞朵负涓�寮犲悎鍚岋紙涓昏〃瀛楁鍙栬缁勯琛岋級锛涒�滃悎鍚岀紪鍙封�濈暀绌烘椂姣忎竴琛屽悇鐢熸垚涓�寮犳柊鍚堝悓銆傚悎鍚岀紪鍙峰凡瀛樺湪鏃朵細瀵煎叆澶辫触銆�</p>
+ <p>鍚堝悓閲戦鎸夆�滄槑缁嗗崟浠访楁暟閲� 鈭� 浼樻儬鐜団�濊嚜鍔ㄨ绠楋紱瀹氶噾閲戦鍙~锛涜礋璐d汉 = 褰撳墠鐧诲綍鐢ㄦ埛锛涚姸鎬佷负鈥滄湭鎻愪氦鈥濓紝鍙湪鍒楄〃涓户缁紪杈戞垨鎻愪氦瀹℃壒銆�</p>
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆鍚堝悓鏁版嵁锛岃绋嶅��...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex gap-4">
+ <span class="text-green-600">鏂板锛歿{ createCount }} 鏉�</span>
+ <span class="text-blue-600">瑕嗙洊鏇存柊锛歿{ updateCount }} 鏉�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 鏉�</span>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureList"
+ :key="name"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/crm/customer/detail/index.vue b/src/views/crm/customer/detail/index.vue
index 526affd..301ee0c 100644
--- a/src/views/crm/customer/detail/index.vue
+++ b/src/views/crm/customer/detail/index.vue
@@ -94,9 +94,11 @@
formModalApi.setData({ id: customerId.value }).open();
}
-/** 杞Щ绾跨储 */
+/** 杞Щ瀹㈡埛 */
function handleTransfer() {
- transferModalApi.setData({ id: customerId.value }).open();
+ transferModalApi
+ .setData({ id: customerId.value, bizType: BizTypeEnum.CRM_CUSTOMER })
+ .open();
}
/** 閿佸畾瀹㈡埛 */
diff --git a/src/views/crm/invoice/data.ts b/src/views/crm/invoice/data.ts
new file mode 100644
index 0000000..c58ff5c
--- /dev/null
+++ b/src/views/crm/invoice/data.ts
@@ -0,0 +1,315 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+
+import { getContractSimpleList } from '#/api/crm/contract';
+import { getSimpleUserList } from '#/api/system/user';
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+
+import { markRaw } from 'vue';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ const userStore = useUserStore();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '寮�绁ㄧ紪鍙�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '淇濆瓨鏃惰嚜鍔ㄧ敓鎴�',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'ownerUserId',
+ label: '璐熻矗浜�',
+ component: 'ApiSelect',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => !!values.id,
+ },
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '璇烽�夋嫨璐熻矗浜�',
+ allowClear: true,
+ },
+ defaultValue: userStore.userInfo?.id,
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛鍚嶇О',
+ component: markRaw(CrmCustomerSelect),
+ rules: 'required',
+ componentProps: (_values, form) => ({
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ onChange: () => {
+ form.setFieldValue('contractId', undefined);
+ form.setFieldValue('price', undefined);
+ },
+ }),
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => !!values.id,
+ },
+ },
+ {
+ fieldName: 'contractId',
+ label: '鍚堝悓鍚嶇О',
+ component: 'Select',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['customerId'],
+ disabled: (values) => !values.customerId || !!values.id,
+ async componentProps(values, form) {
+ if (!values.customerId) {
+ return {
+ options: [],
+ placeholder: '璇峰厛閫夋嫨瀹㈡埛',
+ };
+ }
+ const contracts = await getContractSimpleList(values.customerId);
+ const approvedContracts = contracts.filter(
+ (item) => item.auditStatus === 20,
+ );
+ return {
+ options: approvedContracts.map((item) => {
+ const remain =
+ (item.totalPrice ?? 0) - (item.totalInvoicePrice ?? 0);
+ return {
+ label: `${item.name}锛埪�${item.totalPrice ?? 0}锛屽彲寮�绁� 楼${remain.toFixed(2)}锛塦,
+ value: item.id,
+ };
+ }),
+ placeholder: '璇烽�夋嫨鍚堝悓',
+ onChange: () => {
+ form.setFieldValue('price', undefined);
+ },
+ } as any;
+ },
+ },
+ },
+ {
+ fieldName: 'invoiceType',
+ label: '鍙戠エ绫诲瀷',
+ component: 'Select',
+ rules: 'required',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_INVOICE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍙戠エ绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'invoiceNo',
+ label: '鍙戠エ鍙风爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ彂绁ㄥ彿鐮�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'invoiceTitle',
+ label: '鍙戠エ鎶ご',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ彂绁ㄦ姮澶�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'price',
+ label: '寮�绁ㄩ噾棰濓紙鍏冿級',
+ component: 'InputNumber',
+ rules: 'required',
+ componentProps: {
+ class: '!w-full',
+ placeholder: '璇疯緭鍏ュ紑绁ㄩ噾棰�',
+ min: 0,
+ precision: 2,
+ },
+ },
+ {
+ fieldName: 'invoiceTime',
+ label: '寮�绁ㄦ棩鏈�',
+ component: 'DatePicker',
+ rules: 'required',
+ componentProps: {
+ placeholder: '璇烽�夋嫨寮�绁ㄦ棩鏈�',
+ showTime: false,
+ valueFormat: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD',
+ },
+ },
+ {
+ fieldName: 'blobIds',
+ label: '鍙戠エ闄勪欢',
+ component: 'FileUpload',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ accept: ['jpg', 'jpeg', 'png', 'pdf'],
+ helpText: '鏀寔jpg銆乯peg銆乸ng銆乸df鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃50MB',
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 4,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '寮�绁ㄧ紪鍙�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ紑绁ㄧ紪鍙�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'customerId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨瀹㈡埛',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '寮�绁ㄧ紪鍙�',
+ field: 'no',
+ minWidth: 160,
+ fixed: 'left',
+ slots: { default: 'no' },
+ },
+ {
+ title: '瀹㈡埛鍚嶇О',
+ field: 'customerName',
+ minWidth: 150,
+ slots: { default: 'customerName' },
+ },
+ {
+ title: '鍚堝悓缂栧彿',
+ field: 'contract',
+ minWidth: 160,
+ slots: { default: 'contractNo' },
+ },
+ {
+ title: '鍙戠エ绫诲瀷',
+ field: 'invoiceType',
+ minWidth: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_INVOICE_TYPE },
+ },
+ },
+ {
+ title: '鍙戠エ鍙风爜',
+ field: 'invoiceNo',
+ minWidth: 150,
+ },
+ {
+ title: '寮�绁ㄩ噾棰濓紙鍏冿級',
+ field: 'price',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '鍓╀綑鍙洖娆鹃噾棰濓紙鍚鎵逛腑锛�',
+ field: 'remainingInvoicePrice',
+ minWidth: 180,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '寮�绁ㄦ棩鏈�',
+ field: 'invoiceTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '澶囨敞',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '璐熻矗浜�',
+ field: 'ownerUserName',
+ minWidth: 150,
+ },
+ {
+ title: '鎵�灞為儴闂�',
+ field: 'ownerUserDeptName',
+ minWidth: 150,
+ },
+ {
+ title: '鏇存柊鏃堕棿',
+ field: 'updateTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鍒涘缓浜�',
+ field: 'creatorName',
+ minWidth: 150,
+ },
+ {
+ title: '闄勪欢',
+ field: 'attachmentList',
+ minWidth: 100,
+ fixed: 'right',
+ slots: { default: 'attachment' },
+ },
+ {
+ title: '瀹℃壒鐘舵��',
+ field: 'auditStatus',
+ minWidth: 100,
+ fixed: 'right',
+ slots: { default: 'auditStatus' },
+ },
+ {
+ title: '鎿嶄綔',
+ field: 'actions',
+ minWidth: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/crm/invoice/detail/approve/index.vue b/src/views/crm/invoice/detail/approve/index.vue
new file mode 100644
index 0000000..ec1530c
--- /dev/null
+++ b/src/views/crm/invoice/detail/approve/index.vue
@@ -0,0 +1,105 @@
+<script lang="ts" setup>
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { getInvoice } from '#/api/crm/invoice';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'CrmInvoiceApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<CrmInvoiceApi.Invoice>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '寮�绁ㄧ紪鍙�', field: 'no', render: textRender },
+ { label: '瀹㈡埛鍚嶇О', field: 'customerName', render: textRender },
+ {
+ label: '鍚堝悓缂栧彿',
+ field: 'contract',
+ render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'),
+ },
+ {
+ label: '鍙戠エ绫诲瀷',
+ field: 'invoiceType',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_INVOICE_TYPE, value: val }),
+ },
+ { label: '鍙戠エ鍙风爜', field: 'invoiceNo', render: textRender },
+ { label: '鍙戠エ鎶ご', field: 'invoiceTitle', render: textRender },
+ {
+ label: '寮�绁ㄩ噾棰濓紙鍏冿級',
+ field: 'price',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '鍓╀綑鍙洖娆鹃噾棰濓紙鍏冿級',
+ field: 'remainingInvoicePrice',
+ render: (val) => erpPriceInputFormatter(val ?? 0) || '-',
+ },
+ {
+ label: '寮�绁ㄦ棩鏈�',
+ field: 'invoiceTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'auditStatus',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
+ },
+ { label: '璐熻矗浜�', field: 'ownerUserName', render: textRender },
+ { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getInvoice(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 寮�绁ㄥ熀鏈俊鎭� -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">寮�绁ㄥ熀鏈俊鎭�</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/crm/invoice/detail/data.ts b/src/views/crm/invoice/detail/data.ts
new file mode 100644
index 0000000..0b2e323
--- /dev/null
+++ b/src/views/crm/invoice/detail/data.ts
@@ -0,0 +1,134 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 璇︽儏椤电殑瀛楁 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ },
+ {
+ field: 'price',
+ label: '寮�绁ㄩ噾棰濓紙鍏冿級',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'remainingInvoicePrice',
+ label: '鍓╀綑鍙洖娆鹃噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val ?? 0),
+ },
+ {
+ field: 'invoiceTime',
+ label: '寮�绁ㄦ棩鏈�',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'invoiceType',
+ label: '鍙戠エ绫诲瀷',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_INVOICE_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'invoiceTitle',
+ label: '鍙戠エ鎶ご',
+ },
+ {
+ field: 'invoiceNo',
+ label: '鍙戠エ鍙风爜',
+ },
+ {
+ field: 'ownerUserName',
+ label: '璐熻矗浜�',
+ },
+ ];
+}
+
+/** 璇︽儏椤电殑鍩虹瀛楁 */
+export function useDetailBaseSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'no',
+ label: '寮�绁ㄧ紪鍙�',
+ },
+ {
+ field: 'customerName',
+ label: '瀹㈡埛鍚嶇О',
+ },
+ {
+ field: 'contract',
+ label: '鍚堝悓缂栧彿',
+ render: (val, data) =>
+ val && data?.contract?.no ? data?.contract?.no : '',
+ },
+ {
+ field: 'invoiceType',
+ label: '鍙戠エ绫诲瀷',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_INVOICE_TYPE,
+ value: val,
+ }),
+ },
+ {
+ field: 'invoiceNo',
+ label: '鍙戠エ鍙风爜',
+ },
+ {
+ field: 'invoiceTitle',
+ label: '鍙戠エ鎶ご',
+ },
+ {
+ field: 'price',
+ label: '寮�绁ㄩ噾棰�',
+ render: (val) => erpPriceInputFormatter(val),
+ },
+ {
+ field: 'remainingInvoicePrice',
+ label: '鍓╀綑鍙洖娆鹃噾棰濓紙鍚鎵逛腑锛�',
+ render: (val) => erpPriceInputFormatter(val ?? 0),
+ },
+ {
+ field: 'invoiceTime',
+ label: '寮�绁ㄦ棩鏈�',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'remark',
+ label: '澶囨敞',
+ },
+ ];
+}
+
+/** 绯荤粺淇℃伅瀛楁 */
+export function useDetailSystemSchema(): DescriptionItemSchema[] {
+ return [
+ {
+ field: 'ownerUserName',
+ label: '璐熻矗浜�',
+ },
+ {
+ field: 'creatorName',
+ label: '鍒涘缓浜�',
+ },
+ {
+ field: 'createTime',
+ label: '鍒涘缓鏃堕棿',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ field: 'updateTime',
+ label: '鏇存柊鏃堕棿',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ];
+}
diff --git a/src/views/crm/invoice/detail/index.vue b/src/views/crm/invoice/detail/index.vue
new file mode 100644
index 0000000..daf9836
--- /dev/null
+++ b/src/views/crm/invoice/detail/index.vue
@@ -0,0 +1,128 @@
+<script setup lang="ts">
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+import type { SystemOperateLogApi } from '#/api/system/operate-log';
+
+import { onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { useTabs } from '@vben/hooks';
+
+import { Card, Tabs } from 'ant-design-vue';
+
+import { getInvoice } from '#/api/crm/invoice';
+import { getOperateLogPage } from '#/api/crm/operateLog';
+import { BizTypeEnum } from '#/api/crm/permission';
+import { useDescription } from '#/components/description';
+import { OperateLog } from '#/components/operate-log';
+import { ACTION_ICON, TableAction } from '#/components/table-action';
+import { $t } from '#/locales';
+import { PermissionList } from '#/views/crm/permission';
+
+import InvoiceForm from '../modules/form.vue';
+import { useDetailSchema } from './data';
+import Info from './modules/info.vue';
+
+const props = defineProps<{ id?: number }>();
+
+const route = useRoute();
+const router = useRouter();
+const tabs = useTabs();
+
+const loading = ref(false);
+const invoiceId = ref(0);
+const invoice = ref<CrmInvoiceApi.Invoice>({} as CrmInvoiceApi.Invoice);
+const logList = ref<SystemOperateLogApi.OperateLog[]>([]);
+const permissionListRef = ref<InstanceType<typeof PermissionList>>();
+
+const [Descriptions] = useDescription({
+ bordered: false,
+ column: 4,
+ schema: useDetailSchema(),
+});
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: InvoiceForm,
+ destroyOnClose: true,
+});
+
+/** 鍔犺浇寮�绁ㄨ鎯� */
+async function loadInvoiceDetail() {
+ loading.value = true;
+ try {
+ invoice.value = await getInvoice(invoiceId.value);
+ const res = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_INVOICE,
+ bizId: invoiceId.value,
+ });
+ logList.value = res.list;
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 杩斿洖鍒楄〃椤� */
+function handleBack() {
+ tabs.closeCurrentTab();
+ router.push({ name: 'CrmInvoice' });
+}
+
+/** 缂栬緫寮�绁� */
+function handleEdit() {
+ formModalApi.setData({ invoice: { id: invoiceId.value } }).open();
+}
+
+/** 鍔犺浇鏁版嵁 */
+onMounted(() => {
+ invoiceId.value = Number(props.id || route.params.id);
+ loadInvoiceDetail();
+});
+</script>
+
+<template>
+ <Page auto-content-height :title="invoice?.no" :loading="loading">
+ <FormModal @success="loadInvoiceDetail" />
+ <template #extra>
+ <TableAction
+ :actions="[
+ {
+ label: '杩斿洖',
+ type: 'default',
+ icon: 'lucide:arrow-left',
+ onClick: handleBack,
+ },
+ {
+ label: $t('ui.actionTitle.edit'),
+ type: 'primary',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:invoice:update'],
+ ifShow: permissionListRef?.validateWrite,
+ onClick: handleEdit,
+ },
+ ]"
+ />
+ </template>
+ <Card class="min-h-[10%]">
+ <Descriptions :data="invoice" />
+ </Card>
+ <Card class="mt-4 min-h-[60%]">
+ <Tabs>
+ <Tabs.TabPane tab="璇︾粏璧勬枡" key="1" :force-render="true">
+ <Info :invoice="invoice" />
+ </Tabs.TabPane>
+ <Tabs.TabPane tab="鎿嶄綔鏃ュ織" key="2" :force-render="true">
+ <OperateLog :log-list="logList" />
+ </Tabs.TabPane>
+ <Tabs.TabPane tab="鍥㈤槦鎴愬憳" key="3" :force-render="true">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="invoiceId"
+ :biz-type="BizTypeEnum.CRM_INVOICE"
+ :show-action="true"
+ @quit-team="handleBack"
+ />
+ </Tabs.TabPane>
+ </Tabs>
+ </Card>
+ </Page>
+</template>
diff --git a/src/views/crm/invoice/detail/modules/info.vue b/src/views/crm/invoice/detail/modules/info.vue
new file mode 100644
index 0000000..9134afd
--- /dev/null
+++ b/src/views/crm/invoice/detail/modules/info.vue
@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+
+import { Divider } from 'ant-design-vue';
+
+import { useDescription } from '#/components/description';
+
+import { useDetailBaseSchema, useDetailSystemSchema } from '../data';
+
+const props = defineProps<{
+ invoice: CrmInvoiceApi.Invoice;
+}>();
+
+const [BaseDescriptions] = useDescription({
+ title: '鍩烘湰淇℃伅',
+ bordered: false,
+ column: 4,
+ class: 'mx-4',
+ schema: useDetailBaseSchema(),
+});
+
+const [SystemDescriptions] = useDescription({
+ title: '绯荤粺淇℃伅',
+ bordered: false,
+ column: 3,
+ class: 'mx-4',
+ schema: useDetailSystemSchema(),
+});
+</script>
+
+<template>
+ <div>
+ <BaseDescriptions :data="invoice" />
+ <Divider />
+ <div v-if="props.invoice?.attachmentList?.length" class="mx-4 mb-4">
+ <h4 class="mb-2 text-base font-medium">鍙戠エ闄勪欢</h4>
+ <div
+ v-for="file in props.invoice.attachmentList"
+ :key="file.id"
+ class="flex items-center gap-2 py-1"
+ >
+ <a :href="file.url" target="_blank" rel="noopener noreferrer">
+ {{ file.name || '闄勪欢' }}
+ </a>
+ </div>
+ </div>
+ <SystemDescriptions :data="invoice" />
+ </div>
+</template>
diff --git a/src/views/crm/invoice/index.vue b/src/views/crm/invoice/index.vue
new file mode 100644
index 0000000..6adafea
--- /dev/null
+++ b/src/views/crm/invoice/index.vue
@@ -0,0 +1,259 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Tag, Tabs } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteInvoice,
+ exportInvoice,
+ getInvoicePage,
+ submitInvoice,
+} from '#/api/crm/invoice';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const { push } = useRouter();
+const sceneType = ref('1');
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 澶勭悊鍦烘櫙绫诲瀷鐨勫垏鎹� */
+function handleChangeSceneType(key: number | string) {
+ sceneType.value = key.toString();
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportInvoice({
+ sceneType: sceneType.value,
+ ...formValues,
+ });
+ downloadFileFromBlobPart({ fileName: '寮�绁�.xls', source: data });
+}
+
+/** 鍒涘缓寮�绁� */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫寮�绁� */
+function handleEdit(row: CrmInvoiceApi.Invoice) {
+ formModalApi.setData({ invoice: row }).open();
+}
+
+/** 鍒犻櫎寮�绁� */
+async function handleDelete(row: CrmInvoiceApi.Invoice) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.no]),
+ duration: 0,
+ });
+ try {
+ await deleteInvoice(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鎻愪氦瀹℃牳 */
+async function handleSubmit(row: CrmInvoiceApi.Invoice) {
+ const hideLoading = message.loading({
+ content: '鎻愪氦瀹℃牳涓�...',
+ duration: 0,
+ });
+ try {
+ await submitInvoice(row.id!);
+ message.success('鎻愪氦瀹℃牳鎴愬姛');
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鏌ョ湅寮�绁ㄨ鎯� */
+function handleDetail(row: CrmInvoiceApi.Invoice) {
+ push({ name: 'CrmInvoiceDetail', params: { id: row.id } });
+}
+
+/** 鏌ョ湅瀹㈡埛璇︽儏 */
+function handleCustomerDetail(row: CrmInvoiceApi.Invoice) {
+ push({ name: 'CrmCustomerDetail', params: { id: row.customerId } });
+}
+
+/** 鏌ョ湅鍚堝悓璇︽儏 */
+function handleContractDetail(row: CrmInvoiceApi.Invoice) {
+ push({ name: 'CrmContractDetail', params: { id: row.contractId } });
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: CrmInvoiceApi.Invoice) {
+ push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: row.processInstanceId },
+ });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getInvoicePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ sceneType: sceneType.value,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<CrmInvoiceApi.Invoice>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Grid>
+ <template #toolbar-actions>
+ <Tabs class="w-full" @change="handleChangeSceneType">
+ <Tabs.TabPane tab="鎴戣礋璐g殑" key="1" />
+ <Tabs.TabPane tab="鎴戝弬涓庣殑" key="2" />
+ <Tabs.TabPane tab="涓嬪睘璐熻矗鐨�" key="3" />
+ </Tabs>
+ </template>
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鏂板寮�绁ㄧ敵璇�',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['crm:invoice:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['crm:invoice:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #no="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.no }}
+ </Button>
+ </template>
+ <template #customerName="{ row }">
+ <Button type="link" @click="handleCustomerDetail(row)">
+ {{ row.customerName }}
+ </Button>
+ </template>
+ <template #contractNo="{ row }">
+ <Button
+ v-if="row.contract"
+ type="link"
+ @click="handleContractDetail(row)"
+ >
+ {{ row.contract.no }}
+ </Button>
+ <span v-else>--</span>
+ </template>
+ <template #attachment="{ row }">
+ <Tag v-if="!row.hasAttachment" color="warning">
+ 鏈笂浼�
+ </Tag>
+ <Tag
+ v-else
+ color="success"
+ style="cursor: pointer"
+ @click="handleDetail(row)"
+ >
+ 宸蹭笂浼�
+ </Tag>
+ </template>
+ <template #auditStatus="{ row }">
+ <Tag v-if="row.auditStatus === 0" color="default">鏈彁浜�</Tag>
+ <Tag v-if="row.auditStatus === 10" color="warning">瀹℃壒涓�</Tag>
+ <Tag v-if="row.auditStatus === 20" color="success">瀹℃牳閫氳繃</Tag>
+ <Tag v-if="row.auditStatus === 30" color="error">瀹℃牳涓嶉�氳繃</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:invoice:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦瀹℃牳',
+ type: 'link',
+ auth: ['crm:invoice:update'],
+ onClick: handleSubmit.bind(null, row),
+ ifShow: row.auditStatus === 0,
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ auth: ['crm:invoice:update'],
+ onClick: handleProcessDetail.bind(null, row),
+ ifShow: row.auditStatus !== 0 && !!row.processInstanceId,
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['crm:invoice:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/crm/invoice/modules/form.vue b/src/views/crm/invoice/modules/form.vue
new file mode 100644
index 0000000..1cc9274
--- /dev/null
+++ b/src/views/crm/invoice/modules/form.vue
@@ -0,0 +1,128 @@
+<script lang="ts" setup>
+import type { CrmInvoiceApi } from '#/api/crm/invoice';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { getContractSimpleList } from '#/api/crm/contract';
+import {
+ createInvoice,
+ getInvoice,
+ updateInvoice,
+} from '#/api/crm/invoice';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type InvoicePrefillData = Partial<
+ Pick<CrmInvoiceApi.Invoice, 'contractId' | 'customerId'>
+>;
+
+type InvoiceFormModalData = InvoicePrefillData & {
+ invoice?: Pick<CrmInvoiceApi.Invoice, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmInvoiceApi.Invoice>>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['寮�绁�'])
+ : $t('ui.actionTitle.create', ['寮�绁�']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+/** 鏋勫缓鏂板寮�绁ㄧ殑棰勫~琛ㄥ崟 */
+async function buildCreateFormData(
+ prefill: InvoicePrefillData,
+): Promise<Partial<CrmInvoiceApi.Invoice>> {
+ const values: Partial<CrmInvoiceApi.Invoice> = {
+ contractId: prefill?.contractId,
+ customerId: prefill?.customerId,
+ };
+ if (values.customerId && values.contractId) {
+ const contracts = await getContractSimpleList(values.customerId);
+ const contract = contracts.find((item) => item.id === values.contractId);
+ if (contract) {
+ values.invoiceTitle = contract.customerName;
+ }
+ }
+ return values;
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as CrmInvoiceApi.Invoice;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateInvoice(data)
+ : createInvoice(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formData.value = undefined;
+ await formApi.resetForm();
+ const data = modalApi.getData() as null | InvoiceFormModalData;
+ if (!data) {
+ return;
+ }
+ const { invoice } = data;
+ const prefill = data.customerId || data.contractId ? data : undefined;
+ modalApi.lock();
+ try {
+ if (invoice?.id) {
+ formData.value = await getInvoice(invoice.id!);
+ // 鐢� attachmentList 浠f浛 blobIds锛岃涓婁紶缁勪欢鑳藉睍绀烘枃浠跺悕鍜岄瑙堥摼鎺�
+ if (formData.value?.attachmentList?.length) {
+ (formData.value as Record<string, unknown>).blobIds =
+ formData.value.attachmentList;
+ }
+ } else if (prefill) {
+ formData.value = await buildCreateFormData(prefill);
+ }
+ if (formData.value) {
+ await formApi.setValues(formData.value as any);
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/crm/permission/modules/list.vue b/src/views/crm/permission/modules/list.vue
index 3fd9111..ba12843 100644
--- a/src/views/crm/permission/modules/list.vue
+++ b/src/views/crm/permission/modules/list.vue
@@ -57,6 +57,7 @@
}) {
if (records.some((item) => item.level === PermissionLevelEnum.OWNER)) {
message.warning('涓嶈兘閫夋嫨璐熻矗浜猴紒');
+ checkedRows.value = [];
gridApi.grid.setAllCheckboxRow(false);
return;
}
@@ -114,17 +115,23 @@
}
// 鎻愮ず骞惰繑鍥炴垚鍔�
message.success($t('ui.actionMessage.operationSuccess'));
+ checkedRows.value = [];
+ gridApi.grid.setAllCheckboxRow(false);
handleRefresh();
return true;
}
/** 閫�鍑哄洟闃� */
async function handleQuit() {
+ if (checkedRows.value.length > 0) {
+ return handleDelete();
+ }
+
const permission = gridApi.grid
.getData()
.find(
(item) =>
- item.id === userStore.userInfo?.id &&
+ item.userId === userStore.userInfo?.id &&
item.level === PermissionLevelEnum.OWNER,
);
if (permission) {
@@ -134,7 +141,7 @@
const userPermission = gridApi.grid
.getData()
- .find((item) => item.id === userStore.userInfo?.id);
+ .find((item) => item.userId === userStore.userInfo?.id);
if (!userPermission) {
message.warning('浣犱笉鏄洟闃熸垚鍛橈紒');
return;
diff --git a/src/views/crm/permission/modules/transfer-form.vue b/src/views/crm/permission/modules/transfer-form.vue
index 96b0461..5c7d04e 100644
--- a/src/views/crm/permission/modules/transfer-form.vue
+++ b/src/views/crm/permission/modules/transfer-form.vue
@@ -103,12 +103,12 @@
return;
}
// 鍔犺浇鏁版嵁
- const data = modalApi.getData<{ bizType: number }>();
- if (!data || !data.bizType) {
+ const data = modalApi.getData<{ bizType: number; id: number }>();
+ if (!data?.bizType || !data.id) {
return;
}
bizType.value = data.bizType;
- await formApi.setFieldValue('id', data.bizType);
+ await formApi.setFieldValue('id', data.id);
},
});
</script>
diff --git a/src/views/crm/receivable/components/data.ts b/src/views/crm/receivable/components/data.ts
index 4f3f0e1..4aa273b 100644
--- a/src/views/crm/receivable/components/data.ts
+++ b/src/views/crm/receivable/components/data.ts
@@ -53,16 +53,6 @@
minWidth: 150,
},
{
- title: '鍥炴鐘舵��',
- field: 'auditStatus',
- minWidth: 100,
- fixed: 'right',
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
- },
- },
- {
title: '鎿嶄綔',
field: 'actions',
width: 130,
diff --git a/src/views/crm/receivable/data.ts b/src/views/crm/receivable/data.ts
index 300d02b..e401e1f 100644
--- a/src/views/crm/receivable/data.ts
+++ b/src/views/crm/receivable/data.ts
@@ -6,6 +6,7 @@
import { useUserStore } from '@vben/stores';
import { getContractSimpleList } from '#/api/crm/contract';
+import { getInvoiceSimpleList } from '#/api/crm/invoice';
import {
getReceivablePlan,
getReceivablePlanSimpleList,
@@ -63,6 +64,7 @@
placeholder: '璇烽�夋嫨瀹㈡埛',
onChange: () => {
form.setFieldValue('contractId', undefined);
+ form.setFieldValue('invoiceId', undefined);
form.setFieldValue('planId', undefined);
form.setFieldValue('price', undefined);
form.setFieldValue('returnTime', undefined);
@@ -97,6 +99,7 @@
})),
placeholder: '璇烽�夋嫨鍚堝悓',
onChange: (value: number) => {
+ form.setFieldValue('invoiceId', undefined);
form.setFieldValue('planId', undefined);
form.setFieldValue('returnTime', undefined);
form.setFieldValue('returnType', undefined);
@@ -113,10 +116,53 @@
},
},
{
+ fieldName: 'invoiceId',
+ label: '鍏宠仈寮�绁�',
+ component: 'Select',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['contractId', 'invoiceId'],
+ disabled: (values) => !values.contractId || !!values.id,
+ async componentProps(values, form) {
+ if (!values.contractId) {
+ return {
+ options: [],
+ placeholder: '璇峰厛閫夋嫨鍚堝悓',
+ };
+ }
+ const invoices = await getInvoiceSimpleList(values.contractId);
+ return {
+ options: invoices.map((item) => {
+ const remain = item.remainingInvoicePrice ?? item.price ?? 0;
+ return {
+ label: `${item.no} / ${item.invoiceNo ?? '-'} / 楼${item.price ?? 0} / 鍓╀綑鍙洖娆� 楼${remain.toFixed(2)}`,
+ value: item.id,
+ };
+ }),
+ placeholder: '璇烽�夋嫨鍏宠仈寮�绁�',
+ onChange: (value: number) => {
+ form.setFieldValue('planId', undefined);
+ form.setFieldValue('returnTime', undefined);
+ form.setFieldValue('returnType', undefined);
+ const invoice = invoices.find((item) => item.id === value);
+ if (invoice) {
+ form.setFieldValue(
+ 'price',
+ invoice.remainingInvoicePrice ?? invoice.price ?? undefined,
+ );
+ form.setFieldValue('customerId', invoice.customerId);
+ }
+ },
+ } as any;
+ },
+ },
+ },
+ {
fieldName: 'planId',
label: '鍥炴鏈熸暟',
component: 'Select',
rules: 'required',
+ help: '姝ゅ弬鏁板彲鍦ㄥ洖娆捐鍒掗厤缃�',
dependencies: {
triggerFields: ['contractId'],
disabled: (values) => !values.contractId || values.id,
@@ -246,6 +292,12 @@
slots: { default: 'contractNo' },
},
{
+ title: '鍏宠仈寮�绁�',
+ field: 'invoice',
+ minWidth: 200,
+ slots: { default: 'invoiceNo' },
+ },
+ {
title: '鍥炴鏃ユ湡',
field: 'returnTime',
minWidth: 150,
@@ -303,16 +355,6 @@
title: '鍒涘缓浜�',
field: 'creatorName',
minWidth: 150,
- },
- {
- title: '鍥炴鐘舵��',
- field: 'auditStatus',
- minWidth: 100,
- fixed: 'right',
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
- },
},
{
title: '鎿嶄綔',
diff --git a/src/views/crm/receivable/detail/approve/index.vue b/src/views/crm/receivable/detail/approve/index.vue
new file mode 100644
index 0000000..cc3c795
--- /dev/null
+++ b/src/views/crm/receivable/detail/approve/index.vue
@@ -0,0 +1,112 @@
+<script lang="ts" setup>
+import type { CrmReceivableApi } from '#/api/crm/receivable';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { getReceivable } from '#/api/crm/receivable';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'CrmReceivableApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<CrmReceivableApi.Receivable>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+/** 鍏宠仈寮�绁ㄥ睍绀� */
+function invoiceRender(_val: unknown, data?: CrmReceivableApi.Receivable) {
+ if (!data?.invoice?.no) {
+ return '-';
+ }
+ return `${data.invoice.no} / ${data.invoice.invoiceNo ?? '-'} / 楼${
+ data.invoice.price ?? 0
+ }`;
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鍥炴缂栧彿', field: 'no', render: textRender },
+ { label: '瀹㈡埛鍚嶇О', field: 'customerName', render: textRender },
+ {
+ label: '鍚堝悓缂栧彿',
+ field: 'contract',
+ render: (_val, data) => (data?.contract?.no ? data.contract.no : '-'),
+ },
+ { label: '鍏宠仈寮�绁�', field: 'invoice', render: invoiceRender },
+ {
+ label: '鍥炴鏃ユ湡',
+ field: 'returnTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '鍥炴閲戦锛堝厓锛�',
+ field: 'price',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '鍥炴鏂瑰紡',
+ field: 'returnType',
+ render: (val) =>
+ h(DictTag, {
+ type: DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE,
+ value: val,
+ }),
+ },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'auditStatus',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.CRM_AUDIT_STATUS, value: val }),
+ },
+ { label: '璐熻矗浜�', field: 'ownerUserName', render: textRender },
+ { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getReceivable(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 鍥炴鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鍥炴鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/crm/receivable/detail/data.ts b/src/views/crm/receivable/detail/data.ts
index dcceec5..8d1ad4d 100644
--- a/src/views/crm/receivable/detail/data.ts
+++ b/src/views/crm/receivable/detail/data.ts
@@ -55,6 +55,14 @@
val && data?.contract?.no ? data?.contract?.no : '',
},
{
+ field: 'invoice',
+ label: '鍏宠仈寮�绁�',
+ render: (val, data) =>
+ val && data?.invoice?.no
+ ? `${data.invoice.no} / ${data.invoice.invoiceNo ?? '-'} / 楼${data.invoice.price ?? 0}`
+ : '',
+ },
+ {
field: 'returnTime',
label: '鍥炴鏃ユ湡',
render: (val) => formatDateTime(val) as string,
diff --git a/src/views/crm/receivable/index.vue b/src/views/crm/receivable/index.vue
index 42b319b..289a143 100644
--- a/src/views/crm/receivable/index.vue
+++ b/src/views/crm/receivable/index.vue
@@ -15,7 +15,7 @@
deleteReceivable,
exportReceivable,
getReceivablePage,
- submitReceivable,
+ updateReceivableStatus,
} from '#/api/crm/receivable';
import { $t } from '#/locales';
@@ -76,15 +76,18 @@
}
}
-/** 鎻愪氦瀹℃牳 */
-async function handleSubmit(row: CrmReceivableApi.Receivable) {
+/** 鏇存柊鍥炴鐘舵�侊紙瀹℃牳/鍙嶅鏍革級 */
+async function handleUpdateStatus(
+ row: CrmReceivableApi.Receivable,
+ status: number,
+) {
const hideLoading = message.loading({
- content: '鎻愪氦瀹℃牳涓�...',
+ content: '鏇存柊鐘舵�佷腑...',
duration: 0,
});
try {
- await submitReceivable(row.id!);
- message.success('鎻愪氦瀹℃牳鎴愬姛');
+ await updateReceivableStatus(row.id!, status);
+ message.success('鏇存柊鐘舵�佹垚鍔�');
handleRefresh();
} finally {
hideLoading();
@@ -106,12 +109,9 @@
push({ name: 'CrmContractDetail', params: { id: row.contractId } });
}
-/** 鏌ョ湅瀹℃壒璇︽儏 */
-function handleProcessDetail(row: CrmReceivableApi.Receivable) {
- push({
- name: 'BpmProcessInstanceDetail',
- query: { id: row.processInstanceId },
- });
+/** 鏌ョ湅寮�绁ㄨ鎯� */
+function handleInvoiceDetail(row: CrmReceivableApi.Receivable) {
+ push({ name: 'CrmInvoiceDetail', params: { id: row.invoiceId } });
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -186,6 +186,18 @@
{{ row.customerName }}
</Button>
</template>
+ <template #invoiceNo="{ row }">
+ <template v-if="row.invoice">
+ <Button type="link" @click="handleInvoiceDetail(row)">
+ {{ row.invoice.no }}
+ </Button>
+ <span class="mx-1">/</span>
+ <span>{{ row.invoice.invoiceNo ?? '-' }}</span>
+ <span class="mx-1">/</span>
+ <span>楼{{ row.invoice.price ?? 0 }}</span>
+ </template>
+ <span v-else>--</span>
+ </template>
<template #contractNo="{ row }">
<Button
v-if="row.contract"
@@ -204,21 +216,24 @@
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['crm:receivable:update'],
+ ifShow: row.auditStatus === 10,
onClick: handleEdit.bind(null, row),
},
{
- label: '鎻愪氦瀹℃牳',
+ label: '瀹℃牳',
type: 'link',
+ icon: ACTION_ICON.AUDIT,
auth: ['crm:receivable:update'],
- onClick: handleSubmit.bind(null, row),
- ifShow: row.auditStatus === 0,
+ ifShow: row.auditStatus === 10,
+ onClick: handleUpdateStatus.bind(null, row, 20),
},
{
- label: '鏌ョ湅瀹℃壒',
+ label: '鍙嶅鏍�',
type: 'link',
+ icon: ACTION_ICON.VIEW,
auth: ['crm:receivable:update'],
- onClick: handleProcessDetail.bind(null, row),
- ifShow: row.auditStatus !== 0,
+ ifShow: row.auditStatus === 20,
+ onClick: handleUpdateStatus.bind(null, row, 10),
},
{
label: $t('common.delete'),
diff --git a/src/views/crm/receivable/modules/form.vue b/src/views/crm/receivable/modules/form.vue
index e826402..d8a3672 100644
--- a/src/views/crm/receivable/modules/form.vue
+++ b/src/views/crm/receivable/modules/form.vue
@@ -1,11 +1,11 @@
<script lang="ts" setup>
import type { CrmReceivableApi } from '#/api/crm/receivable';
-import { computed, ref } from 'vue';
+import { computed, ref, watch } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
-import { message } from 'ant-design-vue';
+import { Alert, message } from 'ant-design-vue';
import { getContractSimpleList } from '#/api/crm/contract';
import {
@@ -31,6 +31,7 @@
const emit = defineEmits(['success']);
const formData = ref<Partial<CrmReceivableApi.Receivable>>();
+const showInvoiceHint = ref(false);
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['鍥炴'])
@@ -48,6 +49,14 @@
schema: useFormSchema(),
showDefaultActions: false,
});
+
+/** 鐩戝惉鍚堝悓閫夋嫨锛屾彁绀哄厛寮�绁ㄥ悗鍥炴 */
+watch(
+ () => formApi.form?.values?.contractId,
+ (val) => {
+ showInvoiceHint.value = !!val;
+ },
+);
/** 鏋勫缓鏂板鍥炴鐨勯濉〃鍗� */
async function buildCreateFormData(
@@ -135,6 +144,13 @@
<template>
<Modal :title="getTitle" class="w-2/5">
+ <Alert
+ v-if="showInvoiceHint"
+ type="warning"
+ message="璇峰厛瀹屾垚寮�绁ㄥ苟涓婁紶鍙戠エ闄勪欢锛屽啀杩涜鍥炴鎿嶄綔"
+ show-icon
+ class="mb-4"
+ />
<Form class="mx-4" />
</Modal>
</template>
diff --git a/src/views/crm/receivable/plan/data.ts b/src/views/crm/receivable/plan/data.ts
index cfd71c4..ee60405 100644
--- a/src/views/crm/receivable/plan/data.ts
+++ b/src/views/crm/receivable/plan/data.ts
@@ -10,6 +10,7 @@
import { getSimpleUserList } from '#/api/system/user';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+import dayjs from 'dayjs';
import { markRaw } from 'vue';
/** 鏂板/淇敼鐨勮〃鍗� */
@@ -112,7 +113,7 @@
valueFormat: 'YYYY-MM-DD',
format: 'YYYY-MM-DD',
},
- defaultValue: new Date(),
+ defaultValue: dayjs().format('YYYY-MM-DD'),
},
{
fieldName: 'remindDays',
diff --git a/src/views/crm/receivable/plan/modules/form.vue b/src/views/crm/receivable/plan/modules/form.vue
index 882f313..2110d0d 100644
--- a/src/views/crm/receivable/plan/modules/form.vue
+++ b/src/views/crm/receivable/plan/modules/form.vue
@@ -26,6 +26,7 @@
const [Form, formApi] = useVbenForm({
commonConfig: {
+ labelWidth: 120,
componentProps: {
class: 'w-full',
},
@@ -67,17 +68,19 @@
return;
}
// 鍔犺浇鏁版嵁
+ formData.value = undefined;
+ await formApi.resetForm();
const data = modalApi.getData<CrmReceivablePlanApi.Plan>();
- if (!data || !data.id) {
- // 璁剧疆鍒� values
- await formApi.setValues(data);
+ if (!data) {
return;
}
modalApi.lock();
try {
- formData.value = await getReceivablePlan(data.id);
+ if (data.id) {
+ formData.value = await getReceivablePlan(data.id);
+ }
// 璁剧疆鍒� values
- await formApi.setValues(formData.value);
+ await formApi.setValues(formData.value ?? data);
} finally {
modalApi.unlock();
}
@@ -86,7 +89,7 @@
</script>
<template>
- <Modal :title="getTitle" class="w-2/5">
+ <Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
</Modal>
</template>
diff --git a/src/views/crm/saleQuotation/index.vue b/src/views/crm/saleQuotation/index.vue
index 13297ae..9a221d3 100644
--- a/src/views/crm/saleQuotation/index.vue
+++ b/src/views/crm/saleQuotation/index.vue
@@ -43,7 +43,7 @@
formModalApi.setData(null).open();
}
-/** AI 璇嗗埆褰曞叆 */
+/** 璇嗗埆褰曞叆 */
function handleOcrCreate() {
ocrUploadRef.value?.open();
}
@@ -159,7 +159,7 @@
onClick: handleCreate,
},
{
- label: 'AI 璇嗗埆褰曞叆',
+ label: '璇嗗埆褰曞叆',
type: 'default',
auth: ['crm:sale-quotation:create'],
onClick: handleOcrCreate,
diff --git a/src/views/crm/saleQuotation/modules/form.vue b/src/views/crm/saleQuotation/modules/form.vue
index a52beb5..b2444c4 100644
--- a/src/views/crm/saleQuotation/modules/form.vue
+++ b/src/views/crm/saleQuotation/modules/form.vue
@@ -155,7 +155,7 @@
type="info"
show-icon
class="mx-4 mb-4"
- message="AI 璇嗗埆鍒板鎴峰悕绉帮紝璇峰湪瀹㈡埛閫夋嫨鍣ㄤ腑鎵嬪姩鍖归厤 CRM 瀹㈡埛"
+ message="璇嗗埆鍒板鎴峰悕绉帮紝璇峰湪瀹㈡埛閫夋嫨鍣ㄤ腑鎵嬪姩鍖归厤 CRM 瀹㈡埛"
:description="`璇嗗埆缁撴灉锛�${ocrCustomerName}`"
/>
<Form class="mx-4">
diff --git a/src/views/crm/saleQuotation/modules/ocr-upload.vue b/src/views/crm/saleQuotation/modules/ocr-upload.vue
index db35cb6..2da0826 100644
--- a/src/views/crm/saleQuotation/modules/ocr-upload.vue
+++ b/src/views/crm/saleQuotation/modules/ocr-upload.vue
@@ -49,7 +49,7 @@
onSuccess({ blobId }, file);
// 寮�濮� OCR 璇嗗埆
step.value = 1;
- processingTip.value = '姝e湪杩涜 AI OCR 璇嗗埆锛岃绋嶅��...';
+ processingTip.value = '姝e湪杩涜鎶ヤ环鍗曡瘑鍒紝璇风◢鍊�...';
const ocrData = await ocrSaleQuotation(blobId);
// 妫�鏌ラ敊璇�
if (ocrData.rawText && !ocrData.name && !ocrData.customerName && ocrData.items?.length === 0) {
@@ -101,7 +101,7 @@
</script>
<template>
- <Modal v-model:open="open" title="AI 璇嗗埆褰曞叆鎶ヤ环鍗�" width="680px" :footer="null" @cancel="handleClose">
+ <Modal v-model:open="open" title="璇嗗埆褰曞叆鎶ヤ环鍗�" width="680px" :footer="null" @cancel="handleClose">
<!-- Step 0: 涓婁紶 -->
<template v-if="step === 0">
<Upload.Dragger
diff --git a/src/views/crm/salesTarget/analysis/chart-options.ts b/src/views/crm/salesTarget/analysis/chart-options.ts
new file mode 100644
index 0000000..c3b3466
--- /dev/null
+++ b/src/views/crm/salesTarget/analysis/chart-options.ts
@@ -0,0 +1,130 @@
+import type { EChartsOption } from '@vben/plugins/echarts';
+
+import type { CrmSalesTargetApi } from '#/api/crm/salesTarget';
+
+/** 杈炬垚鍒嗘瀽鎸囨爣 Tab 瀹氫箟 */
+export const ANALYSIS_TABS = [
+ { key: 'contract', label: '鍚堝悓閲戦', unit: '鍏�' },
+ { key: 'receivable', label: '鍥炴閲戦', unit: '鍏�' },
+ { key: 'business', label: '鍟嗘満閲戦', unit: '鍏�' },
+ { key: 'customer', label: '鏂板瀹㈡埛', unit: '涓�' },
+ { key: 'contractCount', label: '鏂板鍚堝悓', unit: '涓�' },
+] as const;
+
+export type AnalysisTabKey = (typeof ANALYSIS_TABS)[number]['key'];
+
+type MetricValue = {
+ target: number;
+ actual: number;
+ rate: number | null;
+};
+
+const METRICS: Record<
+ AnalysisTabKey,
+ { label: string; unit: string; pick: (item: CrmSalesTargetApi.SalesTarget) => MetricValue }
+> = {
+ contract: {
+ label: '鍚堝悓閲戦',
+ unit: '鍏�',
+ pick: (item) => ({
+ target: item.contractTarget ?? 0,
+ actual: item.actualContractPrice ?? 0,
+ rate: item.contractRate ?? null,
+ }),
+ },
+ receivable: {
+ label: '鍥炴閲戦',
+ unit: '鍏�',
+ pick: (item) => ({
+ target: item.receivableTarget ?? 0,
+ actual: item.actualReceivablePrice ?? 0,
+ rate: item.receivableRate ?? null,
+ }),
+ },
+ business: {
+ label: '鍟嗘満閲戦',
+ unit: '鍏�',
+ pick: (item) => ({
+ target: item.businessTarget ?? 0,
+ actual: item.actualBusinessPrice ?? 0,
+ rate: item.businessRate ?? null,
+ }),
+ },
+ customer: {
+ label: '鏂板瀹㈡埛',
+ unit: '涓�',
+ pick: (item) => ({
+ target: item.customerTarget ?? 0,
+ actual: item.actualCustomerCount ?? 0,
+ rate: item.customerRate ?? null,
+ }),
+ },
+ contractCount: {
+ label: '鏂板鍚堝悓',
+ unit: '涓�',
+ pick: (item) => ({
+ target: item.contractCountTarget ?? 0,
+ actual: item.actualContractCount ?? 0,
+ rate: item.contractCountRate ?? null,
+ }),
+ },
+};
+
+function formatNumber(value: number | null | undefined): string {
+ return value == null ? '-' : value.toLocaleString('zh-CN');
+}
+
+/** 鐢熸垚鎸囨爣瀵规瘮鏌辩姸鍥撅紙鐩爣 vs 瀹為檯锛夐厤缃� */
+export function getChartOptions(
+ tabKey: AnalysisTabKey,
+ list: CrmSalesTargetApi.SalesTarget[],
+): EChartsOption {
+ const metric = METRICS[tabKey];
+ const valueName = `${metric.label}锛�${metric.unit}锛塦;
+ return {
+ dataset: {
+ dimensions: ['name', 'target', 'actual', 'rate'],
+ source: list.map((item) => {
+ const value = metric.pick(item);
+ return {
+ name: item.name ?? '--',
+ target: value.target,
+ actual: value.actual,
+ rate: value.rate,
+ };
+ }),
+ },
+ grid: { left: 20, right: 20, bottom: 20, containLabel: true },
+ legend: { top: 10 },
+ series: [
+ { name: '鐩爣', type: 'bar', barMaxWidth: 24 },
+ { name: '瀹為檯', type: 'bar', barMaxWidth: 24 },
+ ],
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: `${metric.label}杈炬垚瀵规瘮` },
+ },
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ formatter: (params: unknown) => {
+ const list = params as Array<{
+ data: { name: string; target: number; actual: number; rate: number | null };
+ }>;
+ const row = list[0]?.data;
+ if (!row) {
+ return '';
+ }
+ return [
+ `<b>${row.name}</b>`,
+ `鐩爣锛�${formatNumber(row.target)}`,
+ `瀹為檯锛�${formatNumber(row.actual)}`,
+ `杈炬垚鐜囷細${row.rate == null ? '-' : `${row.rate}%`}`,
+ ].join('<br/>');
+ },
+ },
+ xAxis: { type: 'category', name: '鐩爣鍚嶇О' },
+ yAxis: { type: 'value', name: valueName },
+ } as EChartsOption;
+}
diff --git a/src/views/crm/salesTarget/analysis/data.ts b/src/views/crm/salesTarget/analysis/data.ts
new file mode 100644
index 0000000..aef38c3
--- /dev/null
+++ b/src/views/crm/salesTarget/analysis/data.ts
@@ -0,0 +1,166 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+/** 杈炬垚鍒嗘瀽鐨勬煡璇㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'periodType',
+ label: '鍛ㄦ湡绫诲瀷',
+ component: 'Select',
+ rules: 'required',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍛ㄦ湡绫诲瀷',
+ },
+ defaultValue: 1,
+ },
+ {
+ fieldName: 'periodValue',
+ label: '鍛ㄦ湡鍊�',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '濡� 2026 / 2026-Q1 / 2026-07',
+ },
+ defaultValue: new Date().getFullYear().toString(),
+ },
+ {
+ fieldName: 'targetDimension',
+ label: '鐩爣缁村害',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_DIMENSION, 'number'),
+ placeholder: '鍏ㄩ儴',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_STATUS, 'number'),
+ placeholder: '鍏ㄩ儴',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 杈炬垚鍒嗘瀽鐨勮〃鏍煎瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '鐩爣鍚嶇О',
+ minWidth: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'targetUserName',
+ title: '鐩爣瀵硅薄',
+ minWidth: 160,
+ slots: { default: 'target' },
+ },
+ {
+ field: 'contractTarget',
+ title: '鍚堝悓閲戦鐩爣',
+ minWidth: 130,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'actualContractPrice',
+ title: '瀹為檯鍚堝悓閲戦',
+ minWidth: 130,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'contractRate',
+ title: '鍚堝悓杈炬垚鐜�(%)',
+ minWidth: 120,
+ slots: { default: 'contractRate' },
+ },
+ {
+ field: 'receivableTarget',
+ title: '鍥炴閲戦鐩爣',
+ minWidth: 130,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'actualReceivablePrice',
+ title: '瀹為檯鍥炴閲戦',
+ minWidth: 130,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'receivableRate',
+ title: '鍥炴杈炬垚鐜�(%)',
+ minWidth: 120,
+ slots: { default: 'receivableRate' },
+ },
+ {
+ field: 'businessTarget',
+ title: '鍟嗘満閲戦鐩爣',
+ minWidth: 130,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'actualBusinessPrice',
+ title: '瀹為檯鍟嗘満閲戦',
+ minWidth: 130,
+ formatter: 'formatAmount2',
+ },
+ {
+ field: 'businessRate',
+ title: '鍟嗘満杈炬垚鐜�(%)',
+ minWidth: 120,
+ slots: { default: 'businessRate' },
+ },
+ {
+ field: 'customerTarget',
+ title: '鏂板瀹㈡埛鐩爣',
+ minWidth: 120,
+ },
+ {
+ field: 'actualCustomerCount',
+ title: '瀹為檯鏂板瀹㈡埛',
+ minWidth: 120,
+ },
+ {
+ field: 'customerRate',
+ title: '瀹㈡埛杈炬垚鐜�(%)',
+ minWidth: 120,
+ slots: { default: 'customerRate' },
+ },
+ {
+ field: 'contractCountTarget',
+ title: '鏂板鍚堝悓鐩爣',
+ minWidth: 120,
+ },
+ {
+ field: 'actualContractCount',
+ title: '瀹為檯鏂板鍚堝悓',
+ minWidth: 120,
+ },
+ {
+ field: 'contractCountRate',
+ title: '鍚堝悓鏁拌揪鎴愮巼(%)',
+ minWidth: 120,
+ slots: { default: 'contractCountRate' },
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ minWidth: 100,
+ fixed: 'right',
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_SALES_TARGET_STATUS },
+ },
+ },
+ ];
+}
diff --git a/src/views/crm/salesTarget/analysis/index.vue b/src/views/crm/salesTarget/analysis/index.vue
new file mode 100644
index 0000000..c20e701
--- /dev/null
+++ b/src/views/crm/salesTarget/analysis/index.vue
@@ -0,0 +1,146 @@
+<script lang="ts" setup>
+import type { EchartsUIType } from '@vben/plugins/echarts';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmSalesTargetApi } from '#/api/crm/salesTarget';
+
+import { onMounted, ref } from 'vue';
+
+import { ContentWrap, Page } from '@vben/common-ui';
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
+
+import { Tabs } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getSalesTargetAnalysis } from '#/api/crm/salesTarget';
+import { $t } from '#/locales';
+
+import {
+ ANALYSIS_TABS,
+ getChartOptions,
+ type AnalysisTabKey,
+} from './chart-options';
+import { useGridColumns, useGridFormSchema } from './data';
+
+const activeTab = ref<string>('contract');
+const chartRef = ref<EchartsUIType>();
+const { renderEcharts } = useEcharts(chartRef);
+
+/** 鎸夊綋鍓嶈〃鍗曞�� + 褰撳墠鎸囨爣 Tab 鍔犺浇鍥捐〃涓庤〃鏍� */
+async function handleLoad() {
+ const formValues = await formApi.getValues();
+ const res = await getSalesTargetAnalysis(formValues);
+ await renderEcharts(getChartOptions(activeTab.value as AnalysisTabKey, res));
+ await gridApi.grid.reloadData(res);
+}
+
+const [QueryForm, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ },
+ schema: useGridFormSchema(),
+ showCollapseButton: true,
+ submitButtonOptions: {
+ content: $t('common.query'),
+ },
+ wrapperClass: 'grid-cols-1 md:grid-cols-4',
+ handleSubmit: handleLoad,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ pagerConfig: {
+ enabled: false,
+ },
+ proxyConfig: {
+ autoLoad: false, // 鍥捐〃涓庤〃鏍肩粺涓�璧� handleLoad 鎵嬪姩鍔犺浇锛岄伩鍏嶇┖鍙傛暟璇锋眰
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ enabled: false,
+ },
+ } as VxeTableGridOptions<CrmSalesTargetApi.SalesTarget>,
+});
+
+/** 鎸囨爣 Tab 鍒囨崲 */
+function handleTabChange(key: string | number) {
+ activeTab.value = String(key);
+ handleLoad();
+}
+
+onMounted(async () => {
+ // 閲嶇疆琛ㄥ崟涓洪粯璁ゅ懆鏈燂紙褰撳墠骞翠唤锛夛紝榛樿鍔犺浇涓�娆¤揪鎴愬垎鏋�
+ await formApi.resetForm();
+ await handleLoad();
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <ContentWrap>
+ <QueryForm />
+ <Tabs
+ v-model:active-key="activeTab"
+ class="w-full"
+ @change="handleTabChange"
+ >
+ <Tabs.TabPane
+ v-for="item in ANALYSIS_TABS"
+ :key="item.key"
+ :tab="item.label"
+ :force-render="true"
+ />
+ </Tabs>
+ <EchartsUI class="mb-4 w-full" height="380px" ref="chartRef" />
+ <Grid>
+ <template #target="{ row }">
+ <span>
+ {{
+ row.targetUserName ||
+ row.targetDeptName ||
+ row.targetItemName ||
+ '--'
+ }}
+ </span>
+ </template>
+ <template #contractRate="{ row }">
+ <span :class="row.contractRate == null ? '' : 'font-medium'">
+ {{ row.contractRate == null ? '--' : `${row.contractRate}%` }}
+ </span>
+ </template>
+ <template #receivableRate="{ row }">
+ <span :class="row.receivableRate == null ? '' : 'font-medium'">
+ {{ row.receivableRate == null ? '--' : `${row.receivableRate}%` }}
+ </span>
+ </template>
+ <template #businessRate="{ row }">
+ <span :class="row.businessRate == null ? '' : 'font-medium'">
+ {{ row.businessRate == null ? '--' : `${row.businessRate}%` }}
+ </span>
+ </template>
+ <template #customerRate="{ row }">
+ <span :class="row.customerRate == null ? '' : 'font-medium'">
+ {{ row.customerRate == null ? '--' : `${row.customerRate}%` }}
+ </span>
+ </template>
+ <template #contractCountRate="{ row }">
+ <span :class="row.contractCountRate == null ? '' : 'font-medium'">
+ {{
+ row.contractCountRate == null
+ ? '--'
+ : `${row.contractCountRate}%`
+ }}
+ </span>
+ </template>
+ </Grid>
+ </ContentWrap>
+ </Page>
+</template>
diff --git a/src/views/crm/salesTarget/data.ts b/src/views/crm/salesTarget/data.ts
new file mode 100644
index 0000000..2d70bb7
--- /dev/null
+++ b/src/views/crm/salesTarget/data.ts
@@ -0,0 +1,351 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+import { MdmItemSelect } from '#/views/basicData/mdm/components';
+
+import { markRaw } from 'vue';
+
+/** 鐩爣缁村害鏋氫妇 */
+export const SALES_TARGET_DIMENSION = {
+ USER: 1, // 涓汉
+ DEPT: 2, // 閮ㄩ棬
+ ITEM: 3, // 浜у搧
+} as const;
+
+/** 鍛ㄦ湡绫诲瀷鏋氫妇 */
+export const SALES_TARGET_PERIOD_TYPE = {
+ YEAR: 1, // 骞�
+ QUARTER: 2, // 瀛e害
+ MONTH: 3, // 鏈�
+} as const;
+
+/** 鍛ㄦ湡鍊艰緭鍏ュ崰浣嶆彁绀� */
+const PERIOD_VALUE_PLACEHOLDER: Record<number, string> = {
+ [SALES_TARGET_PERIOD_TYPE.YEAR]: '璇疯緭鍏ュ勾浠斤紝濡� 2026',
+ [SALES_TARGET_PERIOD_TYPE.QUARTER]: '璇疯緭鍏ュ搴︼紝濡� 2026-Q1',
+ [SALES_TARGET_PERIOD_TYPE.MONTH]: '璇疯緭鍏ユ湀浠斤紝濡� 2026-07',
+};
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鐩爣鍚嶇О',
+ component: 'Input',
+ rules: 'required',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ洰鏍囧悕绉帮紝濡� 2026 骞村害閿�鍞洰鏍�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'periodType',
+ label: '鍛ㄦ湡绫诲瀷',
+ component: 'Select',
+ rules: 'required',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍛ㄦ湡绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'periodValue',
+ label: '鍛ㄦ湡鍊�',
+ component: 'Input',
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['periodType'],
+ async componentProps(values) {
+ const placeholder =
+ PERIOD_VALUE_PLACEHOLDER[values.periodType] ?? '璇疯緭鍏ュ懆鏈熷��';
+ return { placeholder };
+ },
+ },
+ },
+ {
+ fieldName: 'targetDimension',
+ label: '鐩爣缁村害',
+ component: 'Select',
+ rules: 'required',
+ componentProps: (_values, form) => ({
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_DIMENSION, 'number'),
+ placeholder: '璇烽�夋嫨鐩爣缁村害',
+ onChange: () => {
+ form.setFieldValue('targetUserId', undefined);
+ form.setFieldValue('targetDeptId', undefined);
+ form.setFieldValue('targetItemId', undefined);
+ },
+ }),
+ },
+ {
+ fieldName: 'targetUserId',
+ label: '鐩爣鐢ㄦ埛',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ valueField: 'id',
+ placeholder: '璇烽�夋嫨鐩爣鐢ㄦ埛',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['targetDimension'],
+ show: (values) => values.targetDimension === SALES_TARGET_DIMENSION.USER,
+ },
+ },
+ {
+ fieldName: 'targetDeptId',
+ label: '鐩爣閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => {
+ const data = await getSimpleDeptList();
+ return handleTree(data);
+ },
+ labelField: 'name',
+ valueField: 'id',
+ childrenField: 'children',
+ treeDefaultExpandAll: true,
+ placeholder: '璇烽�夋嫨鐩爣閮ㄩ棬',
+ allowClear: true,
+ },
+ dependencies: {
+ triggerFields: ['targetDimension'],
+ show: (values) => values.targetDimension === SALES_TARGET_DIMENSION.DEPT,
+ },
+ },
+ {
+ fieldName: 'targetItemId',
+ label: '鐩爣浜у搧',
+ component: markRaw(MdmItemSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐩爣浜у搧',
+ },
+ dependencies: {
+ triggerFields: ['targetDimension'],
+ show: (values) => values.targetDimension === SALES_TARGET_DIMENSION.ITEM,
+ },
+ },
+ {
+ fieldName: 'contractTarget',
+ label: '鍚堝悓閲戦鐩爣锛堝厓锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ悎鍚岄噾棰濈洰鏍�',
+ },
+ },
+ {
+ fieldName: 'receivableTarget',
+ label: '鍥炴閲戦鐩爣锛堝厓锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ洖娆鹃噾棰濈洰鏍�',
+ },
+ },
+ {
+ fieldName: 'businessTarget',
+ label: '鍟嗘満閲戦鐩爣锛堝厓锛�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ placeholder: '璇疯緭鍏ュ晢鏈洪噾棰濈洰鏍�',
+ },
+ },
+ {
+ fieldName: 'customerTarget',
+ label: '鏂板瀹㈡埛鏁扮洰鏍�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 0,
+ placeholder: '璇疯緭鍏ユ柊澧炲鎴锋暟鐩爣',
+ },
+ },
+ {
+ fieldName: 'contractCountTarget',
+ label: '鏂板鍚堝悓鏁扮洰鏍�',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 0,
+ placeholder: '璇疯緭鍏ユ柊澧炲悎鍚屾暟鐩爣',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '鐩爣鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ洰鏍囧悕绉�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'periodType',
+ label: '鍛ㄦ湡绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍛ㄦ湡绫诲瀷',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'periodValue',
+ label: '鍛ㄦ湡鍊�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '濡� 2026 / 2026-Q1 / 2026-07',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'targetDimension',
+ label: '鐩爣缁村害',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_DIMENSION, 'number'),
+ placeholder: '璇烽�夋嫨鐩爣缁村害',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.CRM_SALES_TARGET_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'name',
+ title: '鐩爣鍚嶇О',
+ minWidth: 200,
+ fixed: 'left',
+ },
+ {
+ field: 'periodType',
+ title: '鍛ㄦ湡绫诲瀷',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_SALES_TARGET_PERIOD_TYPE },
+ },
+ },
+ {
+ field: 'periodValue',
+ title: '鍛ㄦ湡鍊�',
+ minWidth: 120,
+ },
+ {
+ field: 'targetUserName',
+ title: '鐩爣瀵硅薄',
+ minWidth: 160,
+ slots: { default: 'target' },
+ },
+ {
+ field: 'contractTarget',
+ title: '鍚堝悓閲戦',
+ minWidth: 200,
+ slots: { default: 'contract' },
+ },
+ {
+ field: 'receivableTarget',
+ title: '鍥炴閲戦',
+ minWidth: 200,
+ slots: { default: 'receivable' },
+ },
+ {
+ field: 'businessTarget',
+ title: '鍟嗘満閲戦',
+ minWidth: 200,
+ slots: { default: 'business' },
+ },
+ {
+ field: 'customerTarget',
+ title: '鏂板瀹㈡埛',
+ minWidth: 170,
+ slots: { default: 'customer' },
+ },
+ {
+ field: 'contractCountTarget',
+ title: '鏂板鍚堝悓',
+ minWidth: 170,
+ slots: { default: 'contractCount' },
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.CRM_SALES_TARGET_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'creatorName',
+ title: '鍒涘缓浜�',
+ minWidth: 120,
+ },
+ {
+ title: '鎿嶄綔',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/crm/salesTarget/index.vue b/src/views/crm/salesTarget/index.vue
new file mode 100644
index 0000000..101f658
--- /dev/null
+++ b/src/views/crm/salesTarget/index.vue
@@ -0,0 +1,221 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { CrmSalesTargetApi } from '#/api/crm/salesTarget';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ changeSalesTargetStatus,
+ deleteSalesTarget,
+ exportSalesTarget,
+ getSalesTargetPage,
+} from '#/api/crm/salesTarget';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportSalesTarget(formValues);
+ downloadFileFromBlobPart({ fileName: '閿�鍞洰鏍�.xls', source: data });
+}
+
+/** 鏂板閿�鍞洰鏍� */
+function handleCreate() {
+ formModalApi.setData(null).open();
+}
+
+/** 缂栬緫閿�鍞洰鏍� */
+function handleEdit(row: CrmSalesTargetApi.SalesTarget) {
+ formModalApi.setData({ salesTarget: row }).open();
+}
+
+/** 浣滃簾/鎭㈠閿�鍞洰鏍� */
+async function handleChangeStatus(row: CrmSalesTargetApi.SalesTarget) {
+ const nextStatus = row.status === 3 ? 0 : 3; // 浣滃簾 <-> 鎭㈠
+ const actionText = row.status === 3 ? '鎭㈠' : '浣滃簾';
+ const hideLoading = message.loading({
+ content: `${actionText}涓�...`,
+ duration: 0,
+ });
+ try {
+ await changeSalesTargetStatus({ id: row.id!, status: nextStatus });
+ message.success(`${actionText}鎴愬姛`);
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 鍒犻櫎閿�鍞洰鏍� */
+async function handleDelete(row: CrmSalesTargetApi.SalesTarget) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.name!]),
+ duration: 0,
+ });
+ try {
+ await deleteSalesTarget(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.name!]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getSalesTargetPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ height: 64, // 鏀惧ぇ琛岄珮锛屽绾崇洰鏍�/瀹為檯/杈炬垚鐜囦笁琛屽唴瀹�
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<CrmSalesTargetApi.SalesTarget>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Grid>
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鏂板閿�鍞洰鏍�',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['crm:sales-target:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['crm:sales-target:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #target="{ row }">
+ <span>
+ {{
+ row.targetUserName ||
+ row.targetDeptName ||
+ row.targetItemName ||
+ '--'
+ }}
+ </span>
+ </template>
+ <template #contract="{ row }">
+ <div class="text-xs leading-5">
+ <div>鐩爣锛歿{ row.contractTarget ?? '-' }}</div>
+ <div>瀹為檯锛歿{ row.actualContractPrice ?? '-' }}</div>
+ <div>杈炬垚鐜囷細{{ row.contractRate ?? '-' }}%</div>
+ </div>
+ </template>
+ <template #receivable="{ row }">
+ <div class="text-xs leading-5">
+ <div>鐩爣锛歿{ row.receivableTarget ?? '-' }}</div>
+ <div>瀹為檯锛歿{ row.actualReceivablePrice ?? '-' }}</div>
+ <div>杈炬垚鐜囷細{{ row.receivableRate ?? '-' }}%</div>
+ </div>
+ </template>
+ <template #business="{ row }">
+ <div class="text-xs leading-5">
+ <div>鐩爣锛歿{ row.businessTarget ?? '-' }}</div>
+ <div>瀹為檯锛歿{ row.actualBusinessPrice ?? '-' }}</div>
+ <div>杈炬垚鐜囷細{{ row.businessRate ?? '-' }}%</div>
+ </div>
+ </template>
+ <template #customer="{ row }">
+ <div class="text-xs leading-5">
+ <div>鐩爣锛歿{ row.customerTarget ?? '-' }}</div>
+ <div>瀹為檯锛歿{ row.actualCustomerCount ?? '-' }}</div>
+ <div>杈炬垚鐜囷細{{ row.customerRate ?? '-' }}%</div>
+ </div>
+ </template>
+ <template #contractCount="{ row }">
+ <div class="text-xs leading-5">
+ <div>鐩爣锛歿{ row.contractCountTarget ?? '-' }}</div>
+ <div>瀹為檯锛歿{ row.actualContractCount ?? '-' }}</div>
+ <div>杈炬垚鐜囷細{{ row.contractCountRate ?? '-' }}%</div>
+ </div>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['crm:sales-target:update'],
+ onClick: handleEdit.bind(null, row),
+ ifShow: row.status !== 3,
+ },
+ {
+ label: row.status === 3 ? '鎭㈠' : '浣滃簾',
+ type: 'link',
+ auth: ['crm:sales-target:update'],
+ popConfirm: {
+ title:
+ row.status === 3 ? '纭鎭㈠璇ョ洰鏍囷紵' : '纭浣滃簾璇ョ洰鏍囷紵',
+ confirm: handleChangeStatus.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['crm:sales-target:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.name!]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/crm/salesTarget/modules/form.vue b/src/views/crm/salesTarget/modules/form.vue
new file mode 100644
index 0000000..5e09e17
--- /dev/null
+++ b/src/views/crm/salesTarget/modules/form.vue
@@ -0,0 +1,94 @@
+<script lang="ts" setup>
+import type { CrmSalesTargetApi } from '#/api/crm/salesTarget';
+
+import { computed, ref } from 'vue';
+
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import {
+ createSalesTarget,
+ getSalesTarget,
+ updateSalesTarget,
+} from '#/api/crm/salesTarget';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+type SalesTargetFormModalData = {
+ salesTarget?: Pick<CrmSalesTargetApi.SalesTarget, 'id'>;
+};
+
+const emit = defineEmits(['success']);
+const formData = ref<Partial<CrmSalesTargetApi.SalesTarget>>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['閿�鍞洰鏍�'])
+ : $t('ui.actionTitle.create', ['閿�鍞洰鏍�']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 150,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as CrmSalesTargetApi.SalesTarget;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updateSalesTarget(data)
+ : createSalesTarget(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formData.value = undefined;
+ await formApi.resetForm();
+ const data = modalApi.getData() as null | SalesTargetFormModalData;
+ if (!data?.salesTarget?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getSalesTarget(data.salesTarget.id);
+ if (formData.value) {
+ await formApi.setValues(formData.value as any);
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/crm/statistics/performance/chartOptions.ts b/src/views/crm/statistics/performance/chartOptions.ts
index 073c625..cef5b8e 100644
--- a/src/views/crm/statistics/performance/chartOptions.ts
+++ b/src/views/crm/statistics/performance/chartOptions.ts
@@ -47,7 +47,7 @@
yAxisIndex: 1,
data: res.map((s: any) =>
s.lastMonthCount === 0
- ? 'NULL'
+ ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((s.currentMonthCount - s.lastMonthCount) /
s.lastMonthCount) *
@@ -61,7 +61,7 @@
yAxisIndex: 1,
data: res.map((s: any) =>
s.lastYearCount === 0
- ? 'NULL'
+ ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((s.currentMonthCount - s.lastYearCount) /
s.lastYearCount) *
@@ -166,7 +166,7 @@
yAxisIndex: 1,
data: res.map((s: any) =>
s.lastMonthCount === 0
- ? 'NULL'
+ ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((s.currentMonthCount - s.lastMonthCount) /
s.lastMonthCount) *
@@ -180,7 +180,7 @@
yAxisIndex: 1,
data: res.map((s: any) =>
s.lastYearCount === 0
- ? 'NULL'
+ ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((s.currentMonthCount - s.lastYearCount) /
s.lastYearCount) *
@@ -290,7 +290,7 @@
yAxisIndex: 1,
data: res.map((s: any) =>
s.lastMonthCount === 0
- ? 'NULL'
+ ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((s.currentMonthCount - s.lastMonthCount) /
s.lastMonthCount) *
@@ -304,7 +304,7 @@
yAxisIndex: 1,
data: res.map((s: any) =>
s.lastYearCount === 0
- ? 'NULL'
+ ? (s.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((s.currentMonthCount - s.lastYearCount) /
s.lastYearCount) *
diff --git a/src/views/crm/statistics/performance/index.vue b/src/views/crm/statistics/performance/index.vue
index 0e029ae..2c66fc3 100644
--- a/src/views/crm/statistics/performance/index.vue
+++ b/src/views/crm/statistics/performance/index.vue
@@ -131,7 +131,7 @@
tableData[2][`field${index}`] = item.lastYearCount;
tableData[3][`field${index}`] =
item.lastMonthCount === 0
- ? 'NULL'
+ ? (item.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((item.currentMonthCount - item.lastMonthCount) /
item.lastMonthCount) *
@@ -139,7 +139,7 @@
).toFixed(2);
tableData[4][`field${index}`] =
item.lastYearCount === 0
- ? 'NULL'
+ ? (item.currentMonthCount > 0 ? 100 : 0).toFixed(2)
: (
((item.currentMonthCount - item.lastYearCount) /
item.lastYearCount) *
diff --git a/src/views/dashboard/analytics/index.vue b/src/views/dashboard/analytics/index.vue
index 3714991..13794de 100644
--- a/src/views/dashboard/analytics/index.vue
+++ b/src/views/dashboard/analytics/index.vue
@@ -95,7 +95,6 @@
</Col>
<Col :lg="10" :md="24" :sm="24" :xl="10" :xs="24" class="mb-4">
<AlertPanel
- :andon-active-count="summary.andonActiveCount"
:repair-active-count="summary.repairActiveCount"
@navigate="handleNavigate"
/>
diff --git a/src/views/dashboard/analytics/modules/alert-panel.vue b/src/views/dashboard/analytics/modules/alert-panel.vue
index 4be1544..8ac16f2 100644
--- a/src/views/dashboard/analytics/modules/alert-panel.vue
+++ b/src/views/dashboard/analytics/modules/alert-panel.vue
@@ -15,7 +15,6 @@
defineOptions({ name: 'DashboardAlertPanel' });
const props = defineProps<{
- andonActiveCount: number;
repairActiveCount: number;
}>();
@@ -67,24 +66,7 @@
<template>
<div class="space-y-4">
<!-- 棰勮鍗$墖琛� -->
- <div class="grid grid-cols-2 gap-3">
- <div
- class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4"
- @click="emit('navigate', 'MesProAndon')"
- >
- <div class="relative z-10 flex items-center gap-3">
- <div class="flex size-10 items-center justify-center rounded-lg bg-red-50 text-red-600 shadow-sm dark:bg-red-500/15 dark:text-red-400">
- <IconifyIcon class="size-5" icon="lucide:alert-triangle" />
- </div>
- <div>
- <div class="text-xs text-gray-500 dark:text-gray-400">瀹夌伅鍛煎彨</div>
- <div class="flex items-baseline gap-1">
- <CountTo class="text-xl font-bold text-red-600 dark:text-red-400" :end-val="andonActiveCount" :duration="1000" />
- <span class="text-xs text-gray-400">鏈缃�</span>
- </div>
- </div>
- </div>
- </div>
+ <div class="grid grid-cols-1 gap-3">
<div
class="glass-tilt group cursor-pointer overflow-hidden rounded-2xl border-0 p-4"
@click="emit('navigate', 'MesProRepair')"
diff --git a/src/views/dashboard/analytics/modules/quick-actions.vue b/src/views/dashboard/analytics/modules/quick-actions.vue
index f84affb..9a825b5 100644
--- a/src/views/dashboard/analytics/modules/quick-actions.vue
+++ b/src/views/dashboard/analytics/modules/quick-actions.vue
@@ -21,7 +21,6 @@
{ title: '璐ㄦ绠$悊', icon: 'lucide:clipboard-check', route: 'MesQcRecord', gradient: 'from-orange-500 to-amber-500', shadow: 'shadow-orange-500/15' },
{ title: '璁惧鐐规', icon: 'lucide:settings', route: 'MesDvMachinery', gradient: 'from-violet-500 to-purple-500', shadow: 'shadow-violet-500/15' },
{ title: '鐢熶骇鎺掔▼', icon: 'lucide:calendar-range', route: 'MesProScheduling', gradient: 'from-cyan-500 to-teal-500', shadow: 'shadow-cyan-500/15' },
- { title: '瀹夌伅鍛煎彨', icon: 'lucide:bell-ring', route: 'MesProAndon', gradient: 'from-red-500 to-rose-500', shadow: 'shadow-red-500/15' },
{ title: '鐗╂枡搴撳瓨', icon: 'lucide:package-search', route: 'WlsMaterialStock', gradient: 'from-indigo-500 to-blue-500', shadow: 'shadow-indigo-500/15' },
{ title: '宸ヨ壓璺嚎', icon: 'lucide:route', route: 'MesPdRouting', gradient: 'from-teal-500 to-emerald-500', shadow: 'shadow-teal-500/15' },
{ title: '鏁版嵁鎶ヨ〃', icon: 'lucide:bar-chart-big', route: 'MesHomeIndex', gradient: 'from-pink-500 to-rose-500', shadow: 'shadow-pink-500/15' },
@@ -31,7 +30,7 @@
<template>
<div class="glass-card rounded-2xl p-5">
<h3 class="mb-4 text-sm font-semibold text-gray-800 dark:text-gray-100">蹇嵎鎿嶄綔</h3>
- <div class="grid grid-cols-3 gap-3 sm:grid-cols-9">
+ <div class="grid grid-cols-4 gap-3 sm:grid-cols-8">
<div
v-for="action in actions"
:key="action.route"
diff --git a/src/views/erp/finance/payment/data.ts b/src/views/erp/finance/payment/data.ts
index 22aa220..2d40c12 100644
--- a/src/views/erp/finance/payment/data.ts
+++ b/src/views/erp/finance/payment/data.ts
@@ -6,6 +6,7 @@
import { erpPriceInputFormatter } from '@vben/utils';
import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getPurchaseInvoiceSimpleList } from '#/api/erp/purchase/invoice';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -50,7 +51,7 @@
fieldName: 'supplierId',
label: '渚涘簲鍟�',
component: 'ApiSelect',
- componentProps: {
+ componentProps: (_values, form) => ({
disabled: formType === 'detail',
placeholder: '璇烽�夋嫨渚涘簲鍟�',
allowClear: true,
@@ -58,8 +59,54 @@
api: getSupplierSimpleList,
labelField: 'name',
valueField: 'id',
- },
+ onChange: () => {
+ // 渚涘簲鍟嗗彉鍖栧悗锛屾竻绌哄凡閫夌殑鍏宠仈鏉ョエ
+ form.setFieldValue('invoiceId', undefined);
+ },
+ }),
rules: 'required',
+ },
+ {
+ fieldName: 'invoiceId',
+ label: '鍏宠仈鏉ョエ',
+ component: formType === 'detail' ? 'Input' : 'Select',
+ componentProps:
+ formType === 'detail'
+ ? {
+ disabled: true,
+ placeholder: '--',
+ }
+ : undefined,
+ rules: 'required',
+ dependencies: {
+ // 渚濊禆 invoiceId 鑷韩锛岀‘淇濈紪杈戝洖鏄炬椂閲嶆柊鍔犺浇閫夐」浠ュ尮閰嶅凡閫夋潵绁�
+ triggerFields: ['supplierId', 'invoiceId'],
+ disabled: (values) => formType === 'detail' || !values.supplierId,
+ async componentProps(values) {
+ if (formType === 'detail') {
+ return {};
+ }
+ if (!values.supplierId) {
+ return {
+ options: [],
+ placeholder: '璇峰厛閫夋嫨渚涘簲鍟�',
+ };
+ }
+ const invoices = await getPurchaseInvoiceSimpleList(
+ values.supplierId,
+ );
+ return {
+ options: invoices.map((item) => {
+ const remain = item.remainingInvoicePrice ?? item.price ?? 0;
+ return {
+ label: `${item.no} / ${item.invoiceNo ?? '-'} / 楼${item.price ?? 0} / 鍓╀綑鍙粯 楼${remain.toFixed(2)}`,
+ value: item.id,
+ };
+ }),
+ placeholder: '璇烽�夋嫨鍏宠仈鏉ョエ',
+ } as any;
+ },
+ },
},
{
fieldName: 'financeUserId',
@@ -105,16 +152,13 @@
formItemClass: 'col-span-3',
},
{
- fieldName: 'accountId',
+ fieldName: 'accountName',
label: '浠樻璐︽埛',
- component: 'ApiSelect',
+ component: 'Input',
+ rules: 'required',
componentProps: {
- placeholder: '璇烽�夋嫨浠樻璐︽埛',
+ placeholder: '璇疯緭鍏ヤ粯娆捐处鎴�',
allowClear: true,
- showSearch: true,
- api: getAccountSimpleList,
- labelField: 'name',
- valueField: 'id',
},
},
{
@@ -133,6 +177,7 @@
fieldName: 'discountPrice',
label: '浼樻儬閲戦',
component: 'InputNumber',
+ defaultValue: 0,
componentProps: {
class: '!w-full',
disabled: formType === 'detail',
@@ -165,15 +210,13 @@
];
}
-/** 琛ㄥ崟鐨勬槑缁嗚〃鏍煎垪 */
-export function useFormItemColumns(
- disabled: boolean,
-): VxeTableGridOptions['columns'] {
+/** 琛ㄥ崟鐨勬槑缁嗚〃鏍煎垪锛堜粯娆惧熀浜庢潵绁紝鏄庣粏鐢辨潵绁ㄨ嚜鍔ㄧ敓鎴愶紝涓嶅彲鍒犻櫎锛� */
+export function useFormItemColumns(): VxeTableGridOptions['columns'] {
return [
{ type: 'seq', title: '搴忓彿', minWidth: 50, fixed: 'left' },
{
field: 'bizNo',
- title: '閲囪喘鍗曟嵁缂栧彿',
+ title: '鏉ョエ缂栧彿',
minWidth: 200,
},
{
@@ -200,13 +243,6 @@
title: '澶囨敞',
minWidth: 150,
slots: { default: 'remark' },
- },
- {
- title: '鎿嶄綔',
- width: 50,
- fixed: 'right',
- slots: { default: 'actions' },
- visible: !disabled,
},
];
}
@@ -335,6 +371,12 @@
minWidth: 120,
},
{
+ field: 'invoice',
+ title: '鍏宠仈鏉ョエ',
+ minWidth: 200,
+ slots: { default: 'invoice' },
+ },
+ {
field: 'paymentTime',
title: '浠樻鏃堕棿',
width: 160,
diff --git a/src/views/erp/finance/payment/detail/approve/index.vue b/src/views/erp/finance/payment/detail/approve/index.vue
new file mode 100644
index 0000000..c098816
--- /dev/null
+++ b/src/views/erp/finance/payment/detail/approve/index.vue
@@ -0,0 +1,172 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, nextTick, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getFinancePayment } from '#/api/erp/finance/payment';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'ErpFinancePaymentApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<ErpFinancePaymentApi.FinancePayment>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+/** 浠樻鏄庣粏锛堝鎵硅鎯呭彧璇诲垪锛� */
+function usePaymentItemColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ { field: 'bizNo', title: '鏉ョエ缂栧彿', minWidth: 200 },
+ {
+ field: 'totalPrice',
+ title: '搴斾粯閲戦',
+ minWidth: 120,
+ formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'paidPrice',
+ title: '宸蹭粯閲戦',
+ minWidth: 120,
+ formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'paymentPrice',
+ title: '鏈浠樻',
+ minWidth: 120,
+ formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 150,
+ formatter: ({ cellValue }) => textRender(cellValue),
+ },
+ ];
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: usePaymentItemColumns(),
+ data: [],
+ minHeight: 200,
+ autoResize: true,
+ border: true,
+ rowConfig: {
+ keyField: 'bizId',
+ isHover: true,
+ },
+ pagerConfig: {
+ enabled: false,
+ },
+ toolbarConfig: {
+ enabled: false,
+ },
+ },
+});
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '浠樻鍗曞彿', field: 'no', render: textRender },
+ { label: '渚涘簲鍟嗗悕绉�', field: 'supplierName', render: textRender },
+ {
+ label: '鍏宠仈鏉ョエ',
+ field: 'invoice',
+ render: (_val, data) => (data?.invoice?.no ? data.invoice.no : '-'),
+ },
+ {
+ label: '浠樻鏃堕棿',
+ field: 'paymentTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '鍚堣閲戦锛堝厓锛�',
+ field: 'totalPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '浼樻儬閲戦锛堝厓锛�',
+ field: 'discountPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '瀹為檯浠樻閲戦锛堝厓锛�',
+ field: 'paymentPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ { label: '浠樻璐︽埛', field: 'accountName', render: textRender },
+ { label: '璐㈠姟浜哄憳', field: 'financeUserName', render: textRender },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
+ },
+ { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getFinancePayment(Number(props.id));
+ if (formData.value?.items?.length) {
+ await nextTick();
+ await gridApi.grid.reloadData(formData.value.items);
+ }
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 浠樻鍗曞熀鏈俊鎭� -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">浠樻鍗曞熀鏈俊鎭�</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ <!-- 浠樻鏄庣粏 -->
+ <div
+ v-if="formData?.items?.length"
+ class="rounded-lg border border-border bg-background p-4"
+ >
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">浠樻鏄庣粏</span>
+ </div>
+ <Grid />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/erp/finance/payment/index.vue b/src/views/erp/finance/payment/index.vue
index 3c5637d..8a9658e 100644
--- a/src/views/erp/finance/payment/index.vue
+++ b/src/views/erp/finance/payment/index.vue
@@ -3,6 +3,7 @@
import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment';
import { ref } from 'vue';
+import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
@@ -13,19 +14,27 @@
import {
deleteFinancePayment,
exportFinancePayment,
+ getFinancePaymentApproveProcessList,
getFinancePaymentPage,
- updateFinancePaymentStatus,
} from '#/api/erp/finance/payment';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
/** ERP 浠樻鍗曞垪琛� */
defineOptions({ name: 'ErpFinancePayment' });
+const router = useRouter();
+
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+ connectedComponent: ProcessSelectModal,
destroyOnClose: true,
});
@@ -65,22 +74,36 @@
}
}
-/** 瀹℃壒/鍙嶅鎵规搷浣� */
-async function handleUpdateStatus(
- row: ErpFinancePaymentApi.FinancePayment,
- status: number,
-) {
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit(row: ErpFinancePaymentApi.FinancePayment) {
const hideLoading = message.loading({
- content: `纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヤ粯娆惧崟鍚楋紵`,
+ content: '鑾峰彇瀹℃壒娴佺▼...',
duration: 0,
});
try {
- await updateFinancePaymentStatus(row.id!, status);
- message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`);
- handleRefresh();
- } finally {
+ const processList = await getFinancePaymentApproveProcessList();
+ hideLoading();
+ if (!processList || processList.length === 0) {
+ message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+ return;
+ }
+ // 鎵撳紑娴佺▼閫夋嫨寮圭獥
+ processModalApi.setData({ id: row.id, processList }).open();
+ } catch {
hideLoading();
}
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: ErpFinancePaymentApi.FinancePayment) {
+ if (!row.processInstanceId) {
+ message.warning('璇ヤ粯娆惧崟灏氭湭鎻愪氦瀹℃壒');
+ return;
+ }
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: row.processInstanceId },
+ });
}
const checkedIds = ref<number[]>([]);
@@ -134,6 +157,7 @@
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
+ <ProcessModal @success="handleRefresh" />
<Grid table-title="浠樻鍗曞垪琛�">
<template #toolbar-tools>
<TableAction
@@ -167,6 +191,13 @@
]"
/>
</template>
+ <template #invoice="{ row }">
+ <span v-if="row.invoice">
+ {{ row.invoice.no
+ }}{{ row.invoice.invoiceNo ? ` / ${row.invoice.invoiceNo}` : '' }}
+ </span>
+ <span v-else>--</span>
+ </template>
<template #actions="{ row }">
<TableAction
:actions="[
@@ -182,22 +213,24 @@
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['erp:finance-payment:update'],
- ifShow: () => row.status !== 20,
+ ifShow: () => row.status !== 20 && row.status !== 15,
onClick: handleEdit.bind(null, row),
},
{
- label: row.status === 10 ? '瀹℃壒' : '鍙嶅鎵�',
+ label: '鎻愪氦瀹℃牳',
type: 'link',
icon: ACTION_ICON.AUDIT,
- auth: ['erp:finance-payment:update-status'],
- popConfirm: {
- title: `纭${row.status === 10 ? '瀹℃壒' : '鍙嶅鎵�'}${row.no}鍚楋紵`,
- confirm: handleUpdateStatus.bind(
- null,
- row,
- row.status === 10 ? 20 : 10,
- ),
- },
+ auth: ['erp:finance-payment:update'],
+ ifShow: () => row.status === 10,
+ onClick: handleSubmit.bind(null, row),
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['erp:finance-payment:query'],
+ ifShow: () => row.status !== 10 && !!row.processInstanceId,
+ onClick: handleProcessDetail.bind(null, row),
},
{
label: $t('common.delete'),
@@ -205,6 +238,7 @@
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['erp:finance-payment:delete'],
+ ifShow: () => row.status !== 15,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
confirm: handleDelete.bind(null, [row.id!]),
diff --git a/src/views/erp/finance/payment/modules/form.vue b/src/views/erp/finance/payment/modules/form.vue
index 9d92bb2..3a29e85 100644
--- a/src/views/erp/finance/payment/modules/form.vue
+++ b/src/views/erp/finance/payment/modules/form.vue
@@ -17,6 +17,7 @@
getFinancePayment,
updateFinancePayment,
} from '#/api/erp/finance/payment';
+import { getPurchaseInvoiceSimpleList } from '#/api/erp/purchase/invoice';
import { useFormSchema } from '../data';
import ItemForm from './item-form.vue';
@@ -28,7 +29,7 @@
id: undefined,
no: '',
supplierId: undefined,
- accountId: undefined,
+ accountName: undefined,
financeUserId: undefined,
paymentTime: undefined,
remark: '',
@@ -68,6 +69,11 @@
if (changedFields.includes('supplierId')) {
formData.value.supplierId = values.supplierId;
}
+ // 鍏宠仈鏉ョエ鍙樺寲鏃讹紝鍔犺浇鏉ョエ淇℃伅渚涙槑缁嗚嚜鍔ㄧ敓鎴�
+ if (changedFields.includes('invoiceId')) {
+ formData.value.invoiceId = values.invoiceId;
+ handleInvoiceChange(values.invoiceId);
+ }
// 鐩殑锛氬悓姝ュ埌 item-form 缁勪欢锛岃Е鍙戞暣浣撶殑浠锋牸璁$畻
if (changedFields.includes('discountPrice')) {
formData.value.discountPrice = values.discountPrice;
@@ -80,6 +86,27 @@
}
},
});
+
+/** 鍏宠仈鏉ョエ鍙樺寲鏃讹紝鍔犺浇鏉ョエ淇℃伅锛屼緵浠樻鏄庣粏鑷姩鐢熸垚 */
+async function handleInvoiceChange(invoiceId: number | undefined) {
+ if (formType.value === 'detail' || !invoiceId) {
+ if (!invoiceId) {
+ formData.value.invoice = undefined;
+ }
+ return;
+ }
+ const supplierId = formData.value.supplierId;
+ if (!supplierId) {
+ return;
+ }
+ const invoices = await getPurchaseInvoiceSimpleList(supplierId);
+ // 璇锋眰鏈熼棿鏉ョエ宸插啀娆″垏鎹㈡椂锛屼涪寮冭繃鏈熺粨鏋滐紝閬垮厤鍥炴樉閿欎贡
+ if (formData.value.invoiceId !== invoiceId) {
+ return;
+ }
+ const invoice = invoices.find((item) => item.id === invoiceId);
+ formData.value.invoice = invoice;
+}
/** 鏇存柊浠樻椤� */
function handleUpdateItems(items: ErpFinancePaymentApi.FinancePaymentItem[]) {
@@ -149,11 +176,11 @@
formApi.setDisabled(formType.value === 'detail');
formApi.updateSchema(useFormSchema(formType.value));
if (!data || !data.id) {
- // 鏂板鏃讹紝榛樿閫変腑璐︽埛
+ // 鏂板鏃讹紝榛樿濉叆榛樿缁撶畻璐︽埛鍚嶇О
const accountList = await getAccountSimpleList();
const defaultAccount = accountList.find((item) => item.defaultStatus);
if (defaultAccount) {
- await formApi.setValues({ accountId: defaultAccount.id });
+ await formApi.setValues({ accountName: defaultAccount.name });
}
return;
}
@@ -162,6 +189,10 @@
formData.value = await getFinancePayment(data.id);
// 璁剧疆鍒� values
await formApi.setValues(formData.value, false);
+ // 璇︽儏妯″紡涓嬶紝鍏宠仈鏉ョエ瀛楁鐢� Input 灞曠ず鏉ョエ缂栧彿
+ if (formType.value === 'detail' && formData.value?.invoice) {
+ await formApi.setFieldValue('invoiceId', formData.value.invoice.no);
+ }
if (formData.value?.attachmentList?.length) {
const blobIds = formData.value.attachmentList.map((item) => ({
uid: String(item.id),
@@ -194,6 +225,7 @@
:supplier-id="formData?.supplierId"
:disabled="formType === 'detail'"
:discount-price="formData?.discountPrice ?? 0"
+ :invoice="formData?.invoice"
@update:items="handleUpdateItems"
@update:total-price="handleUpdateTotalPrice"
@update:payment-price="handleUpdatePaymentPrice"
diff --git a/src/views/erp/finance/payment/modules/item-form.vue b/src/views/erp/finance/payment/modules/item-form.vue
index 3aadbe5..f41cbc4 100644
--- a/src/views/erp/finance/payment/modules/item-form.vue
+++ b/src/views/erp/finance/payment/modules/item-form.vue
@@ -1,26 +1,30 @@
<script lang="ts" setup>
import type { ErpFinancePaymentApi } from '#/api/erp/finance/payment';
-import type { ErpPurchaseInApi } from '#/api/erp/purchase/in';
-import type { ErpPurchaseReturnApi } from '#/api/erp/purchase/return';
import { computed, nextTick, ref, watch } from 'vue';
import { ErpBizType } from '@vben/constants';
import { erpPriceInputFormatter } from '@vben/utils';
-import { Input, InputNumber, message } from 'ant-design-vue';
+import { Input, InputNumber } from 'ant-design-vue';
-import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { useFormItemColumns } from '../data';
-import PurchaseInSelect from './purchase-in-select.vue';
-import SaleReturnSelect from './sale-return-select.vue';
+
+interface InvoiceForItem {
+ id?: number;
+ no?: string;
+ price?: number;
+ remainingInvoicePrice?: number;
+}
interface Props {
items?: ErpFinancePaymentApi.FinancePaymentItem[];
supplierId?: number;
disabled?: boolean;
discountPrice?: number;
+ invoice?: InvoiceForItem;
}
const props = withDefaults(defineProps<Props>(), {
@@ -28,6 +32,7 @@
supplierId: undefined,
disabled: false,
discountPrice: 0,
+ invoice: undefined,
});
const emit = defineEmits([
@@ -36,7 +41,18 @@
'update:paymentPrice',
]);
+/** 杈撳叆鏃朵笉琛ラ浂锛屽け鐒﹀悗浠嶆寜閲戦鏍煎紡淇濈暀涓や綅灏忔暟 */
+const paymentPriceFormatter = (
+ value: number | string | undefined,
+ info: { input: string; userTyping: boolean },
+) => {
+ return info.userTyping ? info.input : erpPriceInputFormatter(value);
+};
+
const tableData = ref<ErpFinancePaymentApi.FinancePaymentItem[]>([]); // 琛ㄦ牸鏁版嵁
+
+/** 宸叉牴鎹鏉ョエ鐢熸垚鐨勬槑缁嗭紝閬垮厤閲嶅鐢熸垚 */
+const generatedInvoiceId = ref<number | undefined>();
/** 鑾峰彇琛ㄦ牸鍚堣鏁版嵁 */
const summaries = computed(() => {
@@ -59,7 +75,7 @@
/** 琛ㄦ牸閰嶇疆 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
- columns: useFormItemColumns(props.disabled),
+ columns: useFormItemColumns(),
data: tableData.value,
minHeight: 250,
autoResize: true,
@@ -85,11 +101,44 @@
return;
}
tableData.value = [...items];
+ // 缂栬緫鍥炴樉鏃讹紝鏍囪宸叉牴鎹綋鍓嶆潵绁ㄧ敓鎴愯繃锛岄伩鍏嶉噸澶嶇敓鎴�
+ generatedInvoiceId.value = props.invoice?.id;
await nextTick(); // 鐗规畩锛氫繚璇� gridApi 宸茬粡鍒濆鍖�
await gridApi.grid.reloadData(tableData.value);
},
{
immediate: true,
+ },
+);
+
+/** 鍏宠仈鏉ョエ鍙樺寲鏃讹紝鑷姩鐢熸垚鍞竴鐨勪粯娆炬槑缁嗭紙浠樻鍩轰簬鏉ョエ锛� */
+watch(
+ () => props.invoice?.id,
+ async (newId) => {
+ if (!newId || !props.invoice) {
+ return;
+ }
+ if (generatedInvoiceId.value === newId) {
+ return;
+ }
+ generatedInvoiceId.value = newId;
+ const invoice = props.invoice;
+ const price = invoice.price ?? 0;
+ const remaining = invoice.remainingInvoicePrice ?? price;
+ tableData.value = [
+ {
+ bizId: invoice.id ?? 0,
+ bizType: ErpBizType.PURCHASE_INVOICE,
+ bizNo: invoice.no ?? '',
+ totalPrice: price,
+ paidPrice: price - remaining,
+ paymentPrice: remaining,
+ remark: '',
+ },
+ ];
+ await nextTick();
+ await gridApi.grid.reloadData(tableData.value);
+ emit('update:items', [...tableData.value]);
},
);
@@ -116,74 +165,6 @@
{ deep: true },
);
-/** 娣诲姞閲囪喘鍏ュ簱鍗� */
-const purchaseInSelectRef = ref();
-const handleOpenPurchaseIn = () => {
- if (!props.supplierId) {
- message.error('璇烽�夋嫨渚涘簲鍟�');
- return;
- }
- purchaseInSelectRef.value?.open(props.supplierId);
-};
-
-const handleAddPurchaseIn = (rows: ErpPurchaseInApi.PurchaseIn[]) => {
- rows.forEach((row) => {
- const totalPrice = row.totalPrice ?? 0;
- const paidPrice = row.paymentPrice ?? 0;
- const newItem: ErpFinancePaymentApi.FinancePaymentItem = {
- bizId: row.id ?? 0,
- bizType: ErpBizType.PURCHASE_IN,
- bizNo: row.no ?? '',
- totalPrice,
- paidPrice,
- paymentPrice: totalPrice - paidPrice,
- remark: undefined,
- };
- tableData.value.push(newItem);
- });
- emit('update:items', [...tableData.value]);
-};
-
-/** 娣诲姞閲囪喘閫�璐у崟 */
-const saleReturnSelectRef = ref();
-const handleOpenSaleReturn = () => {
- if (!props.supplierId) {
- message.error('璇烽�夋嫨渚涘簲鍟�');
- return;
- }
- saleReturnSelectRef.value?.open(props.supplierId);
-};
-
-const handleAddSaleReturn = (rows: ErpPurchaseReturnApi.PurchaseReturn[]) => {
- rows.forEach((row) => {
- const totalPrice = row.totalPrice ?? 0;
- const refundPrice = row.refundPrice ?? 0;
- const newItem: ErpFinancePaymentApi.FinancePaymentItem = {
- bizId: row.id ?? 0,
- bizType: ErpBizType.PURCHASE_RETURN,
- bizNo: row.no ?? '',
- totalPrice: -totalPrice,
- paidPrice: -refundPrice,
- paymentPrice: -totalPrice + refundPrice,
- remark: undefined,
- };
- tableData.value.push(newItem);
- });
- emit('update:items', [...tableData.value]);
-};
-
-/** 鍒犻櫎琛� */
-const handleDelete = async (row: any) => {
- const index = tableData.value.findIndex(
- (item) => item.bizId === row.bizId && item.bizType === row.bizType,
- );
- if (index !== -1) {
- tableData.value.splice(index, 1);
- }
- // 閫氱煡鐖剁粍浠舵洿鏂�
- emit('update:items', [...tableData.value]);
-};
-
/** 澶勭悊琛屾暟鎹彉鏇� */
const handleRowChange = (row: any) => {
const index = tableData.value.findIndex(
@@ -201,7 +182,7 @@
const validate = () => {
// 妫�鏌ユ槸鍚︽湁鏄庣粏
if (tableData.value.length === 0) {
- throw new Error('璇锋坊鍔犱粯娆炬槑缁�');
+ throw new Error('璇峰厛閫夋嫨鍏宠仈鏉ョエ');
}
// 妫�鏌ユ瘡琛岀殑浠樻閲戦
for (let i = 0; i < tableData.value.length; i++) {
@@ -223,7 +204,7 @@
v-model:value="row.paymentPrice"
:precision="2"
:disabled="disabled"
- :formatter="erpPriceInputFormatter"
+ :formatter="paymentPriceFormatter"
placeholder="璇疯緭鍏ユ湰娆′粯娆�"
@change="handleRowChange(row)"
/>
@@ -236,21 +217,6 @@
@change="handleRowChange(row)"
/>
</template>
- <template #actions="{ row }">
- <TableAction
- :actions="[
- {
- label: '鍒犻櫎',
- type: 'link',
- danger: true,
- popConfirm: {
- title: '纭鍒犻櫎璇ヤ粯娆炬槑缁嗗悧锛�',
- confirm: handleDelete.bind(null, row),
- },
- },
- ]"
- />
- </template>
<template #bottom>
<div class="mt-2 rounded border border-border bg-muted p-2">
@@ -258,7 +224,7 @@
<span class="font-medium text-foreground">鍚堣锛�</span>
<div class="flex space-x-4">
<span>
- 鍚堣浠樻锛歿{ erpPriceInputFormatter(summaries.totalPrice) }}
+ 搴斾粯閲戦锛歿{ erpPriceInputFormatter(summaries.totalPrice) }}
</span>
<span>
宸蹭粯閲戦锛歿{ erpPriceInputFormatter(summaries.paidPrice) }}
@@ -270,34 +236,13 @@
</div>
</div>
</div>
- <TableAction
- v-if="!disabled"
- class="mt-2 flex justify-center"
- :actions="[
- {
- label: '娣诲姞閲囪喘鍏ュ簱鍗�',
- type: 'default',
- onClick: handleOpenPurchaseIn,
- },
- {
- label: '娣诲姞閲囪喘閫�璐у崟',
- type: 'default',
- onClick: handleOpenSaleReturn,
- },
- ]"
- />
+ <div
+ v-if="!invoice && !disabled"
+ class="mt-2 flex justify-center text-sm text-muted-foreground"
+ >
+ 璇峰厛鍦ㄤ笂鏂硅〃鍗曢�夋嫨鍏宠仈鏉ョエ锛岀郴缁熷皢鑷姩鐢熸垚浠樻鏄庣粏
+ </div>
</template>
</Grid>
-
- <!-- 閲囪喘鍏ュ簱鍗曢�夋嫨缁勪欢 -->
- <PurchaseInSelect
- ref="purchaseInSelectRef"
- @success="handleAddPurchaseIn"
- />
- <!-- 閲囪喘閫�璐у崟閫夋嫨缁勪欢 -->
- <SaleReturnSelect
- ref="saleReturnSelectRef"
- @success="handleAddSaleReturn"
- />
</div>
</template>
diff --git a/src/views/erp/finance/payment/modules/process-select-modal.vue b/src/views/erp/finance/payment/modules/process-select-modal.vue
new file mode 100644
index 0000000..78ce174
--- /dev/null
+++ b/src/views/erp/finance/payment/modules/process-select-modal.vue
@@ -0,0 +1,76 @@
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitFinancePayment } from '#/api/erp/finance/payment';
+
+interface ProcessDefinition {
+ id: string;
+ name: string;
+ version: number;
+ key: string;
+}
+
+const emit = defineEmits(['success']);
+
+const paymentId = ref<number>();
+const processList = ref<ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+const processOptions = computed(() => {
+ return processList.value.map((p) => ({
+ label: `${p.name} (V${p.version})`,
+ value: p.key,
+ }));
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!selectedProcessKey.value) {
+ message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+ return;
+ }
+ modalApi.lock();
+ try {
+ await submitFinancePayment(paymentId.value!, selectedProcessKey.value);
+ await modalApi.close();
+ emit('success');
+ message.success('鎻愪氦鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ paymentId.value = undefined;
+ selectedProcessKey.value = '';
+ processList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
+ paymentId.value = data.id;
+ processList.value = data.processList || [];
+ // 榛樿閫夋嫨绗竴涓祦绋�
+ if (processList.value.length > 0) {
+ selectedProcessKey.value = processList.value[0].key;
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+ <div class="mb-4">
+ <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+ <Select
+ v-model:value="selectedProcessKey"
+ :options="processOptions"
+ style="width: 200px"
+ placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+ />
+ </div>
+ </Modal>
+</template>
diff --git a/src/views/erp/finance/payment/modules/purchase-in-select.vue b/src/views/erp/finance/payment/modules/purchase-in-select.vue
deleted file mode 100644
index deefa82..0000000
--- a/src/views/erp/finance/payment/modules/purchase-in-select.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { ErpPurchaseInApi } from '#/api/erp/purchase/in';
-
-import { ref } from 'vue';
-
-import { message, Modal } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getPurchaseInPage } from '#/api/erp/purchase/in';
-
-import { usePurchaseInGridColumns, usePurchaseInGridFormSchema } from '../data';
-
-const emit = defineEmits<{
- success: [rows: ErpPurchaseInApi.PurchaseIn[]];
-}>();
-
-const supplierId = ref<number>(); // 渚涘簲鍟� ID
-const open = ref<boolean>(false); // 寮圭獥鏄惁鎵撳紑
-const selectedRows = ref<ErpPurchaseInApi.PurchaseIn[]>([]); // 閫変腑鐨勮
-
-/** 琛ㄦ牸閰嶇疆 */
-const [Grid, gridApi] = useVbenVxeGrid({
- formOptions: {
- schema: usePurchaseInGridFormSchema(),
- },
- gridOptions: {
- columns: usePurchaseInGridColumns(),
- height: 520,
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }, formValues) => {
- return await getPurchaseInPage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- supplierId: supplierId.value,
- paymentEnable: true, // 鍙煡璇㈠彲浠樻鐨�
- ...formValues,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- checkboxConfig: {
- highlight: true,
- range: true,
- },
- toolbarConfig: {
- refresh: true,
- search: true,
- },
- } as VxeTableGridOptions<ErpPurchaseInApi.PurchaseIn>,
- gridEvents: {
- checkboxChange: ({
- records,
- }: {
- records: ErpPurchaseInApi.PurchaseIn[];
- }) => {
- selectedRows.value = records;
- },
- checkboxAll: ({ records }: { records: ErpPurchaseInApi.PurchaseIn[] }) => {
- selectedRows.value = records;
- },
- },
-});
-
-/** 鎵撳紑寮圭獥 */
-function openModal(id: number) {
- // 閲嶇疆鏁版嵁
- supplierId.value = id;
- open.value = true;
- selectedRows.value = [];
- // 鏌ヨ鍒楄〃
- gridApi.formApi?.resetForm();
- gridApi.formApi?.setValues({ supplierId: id });
- gridApi.query();
-}
-
-/** 纭閫夋嫨閲囪喘鍏ュ簱鍗� */
-function handleOk() {
- if (selectedRows.value.length === 0) {
- message.warning('璇烽�夋嫨瑕佹坊鍔犵殑閲囪喘鍏ュ簱鍗�');
- return;
- }
- emit('success', selectedRows.value);
- open.value = false;
-}
-
-defineExpose({ open: openModal });
-</script>
-
-<template>
- <Modal
- v-model:open="open"
- title="閫夋嫨閲囪喘鍏ュ簱鍗�"
- width="80%"
- @cancel.stop="open = false"
- @ok.stop="handleOk"
- >
- <Grid table-title="閲囪喘鍏ュ簱鍗曞垪琛�(浠呭睍绀哄彲浠樻鐨勫崟鎹�)" />
- </Modal>
-</template>
diff --git a/src/views/erp/finance/payment/modules/sale-return-select.vue b/src/views/erp/finance/payment/modules/sale-return-select.vue
deleted file mode 100644
index 71534b3..0000000
--- a/src/views/erp/finance/payment/modules/sale-return-select.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { ErpPurchaseReturnApi } from '#/api/erp/purchase/return';
-
-import { ref } from 'vue';
-
-import { message, Modal } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getPurchaseReturnPage } from '#/api/erp/purchase/return';
-
-import { useSaleReturnGridColumns, useSaleReturnGridFormSchema } from '../data';
-
-const emit = defineEmits<{
- success: [rows: ErpPurchaseReturnApi.PurchaseReturn[]];
-}>();
-
-const supplierId = ref<number>(); // 渚涘簲鍟� ID
-const open = ref<boolean>(false); // 寮圭獥鏄惁鎵撳紑
-const selectedRows = ref<ErpPurchaseReturnApi.PurchaseReturn[]>([]); // 閫変腑鐨勮
-
-/** 琛ㄦ牸閰嶇疆 */
-const [Grid, gridApi] = useVbenVxeGrid({
- formOptions: {
- schema: useSaleReturnGridFormSchema(),
- },
- gridOptions: {
- columns: useSaleReturnGridColumns(),
- height: 520,
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }, formValues) => {
- return await getPurchaseReturnPage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- supplierId: supplierId.value,
- refundEnable: true, // 鍙煡璇㈠彲閫�娆剧殑
- ...formValues,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- checkboxConfig: {
- highlight: true,
- range: true,
- },
- toolbarConfig: {
- refresh: true,
- search: true,
- },
- } as VxeTableGridOptions<ErpPurchaseReturnApi.PurchaseReturn>,
- gridEvents: {
- checkboxChange: ({
- records,
- }: {
- records: ErpPurchaseReturnApi.PurchaseReturn[];
- }) => {
- selectedRows.value = records;
- },
- checkboxAll: ({
- records,
- }: {
- records: ErpPurchaseReturnApi.PurchaseReturn[];
- }) => {
- selectedRows.value = records;
- },
- },
-});
-
-/** 鎵撳紑寮圭獥 */
-function openModal(id: number) {
- // 閲嶇疆鏁版嵁
- supplierId.value = id;
- open.value = true;
- selectedRows.value = [];
- // 鏌ヨ鍒楄〃
- gridApi.formApi?.resetForm();
- gridApi.formApi?.setValues({ supplierId: id });
- gridApi.query();
-}
-
-/** 纭閫夋嫨 */
-function handleOk() {
- if (selectedRows.value.length === 0) {
- message.warning('璇烽�夋嫨瑕佹坊鍔犵殑閲囪喘閫�璐у崟');
- return;
- }
- emit('success', selectedRows.value);
- open.value = false;
-}
-
-defineExpose({ open: openModal });
-</script>
-
-<template>
- <Modal
- v-model:open="open"
- title="閫夋嫨閲囪喘閫�璐у崟"
- width="80%"
- @cancel.stop="open = false"
- @ok.stop="handleOk"
- >
- <Grid table-title="閲囪喘閫�璐у崟鍒楄〃(浠呭睍绀洪渶閫�娆剧殑鍗曟嵁)" />
- </Modal>
-</template>
diff --git a/src/views/erp/purchase/invoice/data.ts b/src/views/erp/purchase/invoice/data.ts
new file mode 100644
index 0000000..73ff3e0
--- /dev/null
+++ b/src/views/erp/purchase/invoice/data.ts
@@ -0,0 +1,250 @@
+import type { Ref } from 'vue';
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { getSupplierSimpleList } from '#/api/srm/supplier';
+
+import { z } from '#/adapter/form';
+
+import { markRaw } from 'vue';
+
+import PurchaseOrderSelect from './modules/purchase-order-select.vue';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'edit';
+
+/** 鏂板/淇敼鐨勮〃鍗� */
+export function useFormSchema(
+ orderRemainInvoicePrice?: Ref<number | undefined>,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'no',
+ label: '鏉ョエ缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '淇濆瓨鏃惰嚜鍔ㄧ敓鎴�',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'purchaseOrderId',
+ label: '閲囪喘璁㈠崟',
+ component: markRaw(PurchaseOrderSelect),
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['id'],
+ disabled: (values) => !!values.id,
+ },
+ componentProps: (_values, form) => ({
+ placeholder: '璇烽�夋嫨閲囪喘璁㈠崟',
+ onChange: (
+ order:
+ | { supplierName?: string; remainInvoicePrice?: number }
+ | undefined,
+ ) => {
+ form.setFieldValue('supplierName', order?.supplierName);
+ if (orderRemainInvoicePrice) {
+ orderRemainInvoicePrice.value = order?.remainInvoicePrice;
+ }
+ },
+ }),
+ },
+ {
+ fieldName: 'supplierName',
+ label: '渚涘簲鍟�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '閫夋嫨閲囪喘璁㈠崟鍚庤嚜鍔ㄥ甫鍑�',
+ disabled: true,
+ },
+ },
+ {
+ fieldName: 'invoiceNo',
+ label: '鍙戠エ鍙风爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ彂绁ㄥ彿鐮�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'invoiceTitle',
+ label: '鍙戠エ鎶ご',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ彂绁ㄦ姮澶�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'price',
+ label: '鏉ョエ閲戦锛堝厓锛�',
+ component: 'InputNumber',
+ rules: 'required',
+ description: () =>
+ orderRemainInvoicePrice?.value != null
+ ? `閲囪喘璁㈠崟鏈紑绁ㄩ噾棰濓細${Number(orderRemainInvoicePrice.value).toFixed(2)} 鍏僠
+ : '',
+ componentProps: {
+ class: '!w-full',
+ placeholder: '璇疯緭鍏ユ潵绁ㄩ噾棰�',
+ min: 0,
+ precision: 2,
+ },
+ dependencies: {
+ triggerFields: ['purchaseOrderId'],
+ componentProps: () => ({
+ class: '!w-full',
+ placeholder: '璇疯緭鍏ユ潵绁ㄩ噾棰�',
+ min: 0,
+ max: orderRemainInvoicePrice?.value ?? undefined,
+ precision: 2,
+ }),
+ rules: () => {
+ const max = orderRemainInvoicePrice?.value;
+ if (max != null) {
+ return z
+ .number({ message: '璇疯緭鍏ユ潵绁ㄩ噾棰�' })
+ .min(0, '鏉ョエ閲戦涓嶈兘灏忎簬 0')
+ .max(max, `鏉ョエ閲戦涓嶈兘澶т簬鏈紑绁ㄩ噾棰� ${Number(max).toFixed(2)}`);
+ }
+ return z
+ .number({ message: '璇疯緭鍏ユ潵绁ㄩ噾棰�' })
+ .min(0, '鏉ョエ閲戦涓嶈兘灏忎簬 0');
+ },
+ },
+ },
+ {
+ fieldName: 'invoiceTime',
+ label: '鏉ョエ鏃ユ湡',
+ component: 'DatePicker',
+ rules: 'required',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鏉ョエ鏃ユ湡',
+ showTime: false,
+ valueFormat: 'YYYY-MM-DD',
+ format: 'YYYY-MM-DD',
+ },
+ },
+ {
+ fieldName: 'blobIds',
+ label: '鍙戠エ闄勪欢',
+ component: 'FileUpload',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ accept: ['jpg', 'jpeg', 'png', 'pdf'],
+ helpText: '鏀寔jpg銆乯peg銆乸ng銆乸df鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃50MB',
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 4,
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'no',
+ label: '鏉ョエ缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ潵绁ㄧ紪鍙�',
+ allowClear: true,
+ },
+ },
+ {
+ fieldName: 'supplierId',
+ label: '渚涘簲鍟�',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '璇烽�夋嫨渚涘簲鍟�',
+ allowClear: true,
+ showSearch: true,
+ api: getSupplierSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ title: '鏉ョエ缂栧彿',
+ field: 'no',
+ minWidth: 160,
+ fixed: 'left',
+ slots: { default: 'no' },
+ },
+ {
+ title: '渚涘簲鍟�',
+ field: 'supplierName',
+ minWidth: 150,
+ },
+ {
+ title: '閲囪喘璁㈠崟缂栧彿',
+ field: 'order',
+ minWidth: 160,
+ slots: { default: 'orderNo' },
+ },
+ {
+ title: '鍙戠エ鍙风爜',
+ field: 'invoiceNo',
+ minWidth: 150,
+ },
+ {
+ title: '鏉ョエ閲戦锛堝厓锛�',
+ field: 'price',
+ minWidth: 150,
+ formatter: 'formatAmount2',
+ },
+ {
+ title: '鏉ョエ鏃ユ湡',
+ field: 'invoiceTime',
+ minWidth: 150,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '澶囨敞',
+ field: 'remark',
+ minWidth: 150,
+ },
+ {
+ title: '闄勪欢',
+ field: 'attachmentList',
+ minWidth: 100,
+ fixed: 'right',
+ slots: { default: 'attachment' },
+ },
+ {
+ title: '鎿嶄綔',
+ field: 'actions',
+ minWidth: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/erp/purchase/invoice/detail/approve/index.vue b/src/views/erp/purchase/invoice/detail/approve/index.vue
new file mode 100644
index 0000000..993efbc
--- /dev/null
+++ b/src/views/erp/purchase/invoice/detail/approve/index.vue
@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { erpPriceInputFormatter, formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { getPurchaseInvoice } from '#/api/erp/purchase/invoice';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'ErpPurchaseInvoiceApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鏉ョエ缂栧彿', field: 'no', render: textRender },
+ { label: '渚涘簲鍟嗗悕绉�', field: 'supplierName', render: textRender },
+ {
+ label: '閲囪喘璁㈠崟鍙�',
+ field: 'order',
+ render: (_val, data) => (data?.order?.no ? data.order.no : '-'),
+ },
+ { label: '鍙戠エ鍙风爜', field: 'invoiceNo', render: textRender },
+ { label: '鍙戠エ鎶ご', field: 'invoiceTitle', render: textRender },
+ {
+ label: '鏉ョエ閲戦锛堝厓锛�',
+ field: 'price',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '鍓╀綑鍙粯閲戦锛堝厓锛�',
+ field: 'remainingInvoicePrice',
+ render: (val) => erpPriceInputFormatter(val ?? 0) || '-',
+ },
+ {
+ label: '寮�绁ㄦ棩鏈�',
+ field: 'invoiceTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getPurchaseInvoice(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 鏉ョエ鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鏉ョエ鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/erp/purchase/invoice/index.vue b/src/views/erp/purchase/invoice/index.vue
new file mode 100644
index 0000000..2878f23
--- /dev/null
+++ b/src/views/erp/purchase/invoice/index.vue
@@ -0,0 +1,194 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
+import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Tag } from 'ant-design-vue';
+import { nextTick, ref } from 'vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deletePurchaseInvoice,
+ exportPurchaseInvoice,
+ getPurchaseInvoicePage,
+} from '#/api/erp/purchase/invoice';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+import OcrUpload from './modules/ocr-upload.vue';
+
+defineOptions({ name: 'ErpPurchaseInvoice' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>();
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎嚭琛ㄦ牸 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportPurchaseInvoice(formValues);
+ downloadFileFromBlobPart({ fileName: '鏉ョエ.xls', source: data });
+}
+
+/** 鍒涘缓鏉ョエ */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 璇嗗埆褰曞叆 */
+function handleOcrCreate() {
+ ocrUploadRef.value?.open();
+}
+
+/** OCR 璇嗗埆缁撴灉纭 */
+function handleOcrSuccess(data: ErpPurchaseInvoiceAiApi.OcrResultVO) {
+ nextTick(() => {
+ formModalApi.setData({ formType: 'create', ocrData: data } as any).open();
+ });
+}
+
+/** 缂栬緫鏉ョエ */
+function handleEdit(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅鏉ョエ璇︽儏 */
+function handleDetail(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 鍒犻櫎鏉ョエ */
+async function handleDelete(row: ErpPurchaseInvoiceApi.PurchaseInvoice) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.no]),
+ duration: 0,
+ });
+ try {
+ await deletePurchaseInvoice(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPurchaseInvoicePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<ErpPurchaseInvoiceApi.PurchaseInvoice>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" />
+ <Grid table-title="鏉ョエ鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鏉ョエ']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['erp:purchase-invoice:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '璇嗗埆褰曞叆',
+ type: 'primary',
+ auth: ['erp:purchase-invoice:create'],
+ onClick: handleOcrCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['erp:purchase-invoice:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #no="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.no }}
+ </Button>
+ </template>
+ <template #orderNo="{ row }">
+ <span>{{ row.order?.no ?? '--' }}</span>
+ </template>
+ <template #attachment="{ row }">
+ <Tag v-if="!row.hasAttachment" color="warning">鏈笂浼�</Tag>
+ <Tag v-else color="success">宸蹭笂浼�</Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['erp:purchase-invoice:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['erp:purchase-invoice:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['erp:purchase-invoice:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.no]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/erp/purchase/invoice/modules/form.vue b/src/views/erp/purchase/invoice/modules/form.vue
new file mode 100644
index 0000000..fd27f98
--- /dev/null
+++ b/src/views/erp/purchase/invoice/modules/form.vue
@@ -0,0 +1,139 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice';
+import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createPurchaseInvoice,
+ getPurchaseInvoice,
+ updatePurchaseInvoice,
+} from '#/api/erp/purchase/invoice';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>();
+const formType = ref<FormType>('create');
+const ocrSupplierName = ref('');
+const orderRemainInvoicePrice = ref<number>(); // 鎵�閫夐噰璐鍗曞墿浣欏彲寮�绁ㄩ噾棰濓紙鏈紑绁ㄩ噾棰濓紝鐢ㄤ簬鎻愮ず涓庢牎楠岋級
+
+const getTitle = computed(() => {
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['鏉ョエ']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['鏉ョエ']);
+ } else {
+ return '鏉ョエ璇︽儏';
+ }
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(orderRemainInvoicePrice),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as ErpPurchaseInvoiceApi.PurchaseInvoice;
+ if (formData.value?.id) {
+ data.id = formData.value.id;
+ }
+ try {
+ await (formData.value?.id
+ ? updatePurchaseInvoice(data)
+ : createPurchaseInvoice(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ ocrSupplierName.value = '';
+ orderRemainInvoicePrice.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{
+ formType: FormType;
+ id?: number;
+ ocrData?: ErpPurchaseInvoiceAiApi.OcrResultVO;
+ }>();
+ formType.value = data.formType;
+ formApi.setDisabled(formType.value === 'detail');
+ formApi.updateSchema(useFormSchema(orderRemainInvoicePrice));
+ await formApi.resetForm();
+ // OCR 璇嗗埆鏁版嵁棰勫~锛堟柊寤哄満鏅紝缂栬緫/璇︽儏涓嶅鐞嗭級
+ const ocr = data?.ocrData;
+ ocrSupplierName.value = ocr?.supplierName || '';
+ if (ocr) {
+ await formApi.setValues({
+ invoiceNo: ocr.invoiceNo,
+ invoiceTitle: ocr.invoiceTitle,
+ price: ocr.price,
+ invoiceTime: ocr.invoiceTime,
+ remark: ocr.remark,
+ });
+ }
+ if (!data || !data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getPurchaseInvoice(data.id);
+ if (formData.value?.attachmentList?.length) {
+ // 杞负涓婁紶缁勪欢鐨勬枃浠跺璞★紱鎻愪氦鏃� formApi 浼氭寜 valueKey 鎻愬彇闄勪欢 ID
+ (formData.value as Record<string, unknown>).blobIds =
+ formData.value.attachmentList.map((item) => ({
+ uid: String(item.id),
+ name: item.name || '',
+ url: item.url || '',
+ status: 'done',
+ id: item.id,
+ }));
+ }
+ await formApi.setValues(formData.value as any);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Alert
+ v-if="ocrSupplierName"
+ type="info"
+ show-icon
+ class="mx-4 mb-4"
+ message="璇嗗埆鍒伴攢鍞柟鍚嶇О锛岃閫夋嫨閲囪喘璁㈠崟骞舵牳瀵硅鍗曚緵搴斿晢鏄惁涓�鑷�"
+ :description="`璇嗗埆缁撴灉锛�${ocrSupplierName}`"
+ />
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/erp/purchase/invoice/modules/ocr-upload.vue b/src/views/erp/purchase/invoice/modules/ocr-upload.vue
new file mode 100644
index 0000000..214c7e9
--- /dev/null
+++ b/src/views/erp/purchase/invoice/modules/ocr-upload.vue
@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai';
+
+import { ref } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import {
+ Alert,
+ Button,
+ Descriptions,
+ Modal,
+ Tag,
+ Upload,
+ message,
+} from 'ant-design-vue';
+
+import { ocrPurchaseInvoice } from '#/api/erp/purchase/invoice/ai';
+import { uploadFile } from '#/api/system/storage';
+
+defineOptions({ name: 'ErpPurchaseInvoiceOcrUpload' });
+
+const emit = defineEmits<{
+ success: [data: ErpPurchaseInvoiceAiApi.OcrResultVO];
+}>();
+
+const open = ref(false);
+const step = ref(0); // 0=upload, 1=processing, 2=preview
+const fileList = ref<any[]>([]);
+const ocrResult = ref<ErpPurchaseInvoiceAiApi.OcrResultVO>();
+const processingTip = ref('');
+
+function handleOpen() {
+ open.value = true;
+ step.value = 0;
+ fileList.value = [];
+ ocrResult.value = undefined;
+}
+
+/** 鑷畾涔変笂浼� */
+async function handleUpload(options: any) {
+ const { file, onSuccess, onError } = options;
+ try {
+ const result = await uploadFile([file as File]);
+ const blobId = result?.[0]?.id;
+ if (!blobId) {
+ onError(new Error('涓婁紶澶辫触'));
+ return;
+ }
+ onSuccess({ blobId }, file);
+ // 寮�濮� OCR 璇嗗埆
+ step.value = 1;
+ processingTip.value = '姝e湪杩涜鍙戠エ璇嗗埆锛岃绋嶅��...';
+ const ocrData = await ocrPurchaseInvoice(blobId);
+ // 妫�鏌ラ敊璇細rawText 涓洪敊璇彁绀猴紝涓斾笟鍔″瓧娈靛叏绌�
+ const hasData = !!(
+ ocrData.invoiceNo ||
+ ocrData.invoiceTitle ||
+ ocrData.supplierName ||
+ ocrData.price != null ||
+ ocrData.invoiceTime ||
+ ocrData.invoiceType ||
+ ocrData.taxRate != null ||
+ ocrData.remark
+ );
+ if (ocrData.rawText && !hasData) {
+ processingTip.value = '';
+ step.value = 0;
+ message.error(ocrData.rawText);
+ return;
+ }
+ if (!hasData) {
+ processingTip.value = '';
+ step.value = 0;
+ message.warning('鏈瘑鍒埌鍙戠エ鏁版嵁锛岃鎵嬪姩褰曞叆鎴栭噸鏂颁笂浼�');
+ return;
+ }
+ ocrResult.value = ocrData;
+ step.value = 2;
+ } catch {
+ processingTip.value = '';
+ step.value = 0;
+ onError(new Error('涓婁紶澶辫触'));
+ }
+}
+
+function handleConfirm() {
+ if (ocrResult.value) {
+ emit('success', ocrResult.value);
+ }
+ handleClose();
+}
+
+function handleReset() {
+ step.value = 0;
+ fileList.value = [];
+ ocrResult.value = undefined;
+ processingTip.value = '';
+}
+
+function handleClose() {
+ open.value = false;
+ handleReset();
+}
+
+defineExpose({ open: handleOpen });
+</script>
+
+<template>
+ <Modal
+ v-model:open="open"
+ title="璇嗗埆褰曞叆鍙戠エ"
+ width="680px"
+ :footer="null"
+ @cancel="handleClose"
+ >
+ <!-- Step 0: 涓婁紶 -->
+ <template v-if="step === 0">
+ <Upload.Dragger
+ v-model:file-list="fileList"
+ :max-count="1"
+ :custom-request="handleUpload"
+ accept=".pdf,.png,.jpg,.jpeg"
+ @remove="handleReset"
+ >
+ <p class="text-4xl text-gray-400">
+ <IconifyIcon icon="ant-design:inbox-outlined" />
+ </p>
+ <p class="text-base text-gray-500">鐐瑰嚮鎴栨嫋鎷藉彂绁ㄦ枃浠跺埌姝ゅ尯鍩熶笂浼�</p>
+ <p class="text-sm text-gray-400">鏀寔 PDF銆丣PG銆丳NG 鏍煎紡锛屼笌鏉ョエ闄勪欢涓婁紶鏍煎紡涓�鑷�</p>
+ </Upload.Dragger>
+ </template>
+
+ <!-- Step 1: 璇嗗埆涓� -->
+ <template v-else-if="step === 1">
+ <div class="flex flex-col items-center gap-4 py-12">
+ <a-spin size="large" />
+ <span class="text-base text-gray-500">{{ processingTip }}</span>
+ </div>
+ </template>
+
+ <!-- Step 2: 棰勮缁撴灉 -->
+ <template v-else-if="step === 2 && ocrResult">
+ <div class="mb-4">
+ <Descriptions :column="2" bordered size="small">
+ <Descriptions.Item v-if="ocrResult.invoiceNo" label="鍙戠エ鍙风爜">
+ {{ ocrResult.invoiceNo }}
+ </Descriptions.Item>
+ <Descriptions.Item v-if="ocrResult.invoiceType" label="鍙戠エ绫诲瀷">
+ {{ ocrResult.invoiceType }}
+ </Descriptions.Item>
+ <Descriptions.Item v-if="ocrResult.invoiceTitle" label="鍙戠エ鎶ご">
+ {{ ocrResult.invoiceTitle }}
+ </Descriptions.Item>
+ <Descriptions.Item v-if="ocrResult.supplierName" label="閿�鍞柟锛堥渶鏍稿锛�">
+ <Tag color="orange">{{ ocrResult.supplierName }}</Tag>
+ </Descriptions.Item>
+ <Descriptions.Item v-if="ocrResult.price != null" label="鏉ョエ閲戦锛堝厓锛�">
+ {{ ocrResult.price }}
+ </Descriptions.Item>
+ <Descriptions.Item v-if="ocrResult.invoiceTime" label="寮�绁ㄦ棩鏈�">
+ {{ ocrResult.invoiceTime }}
+ </Descriptions.Item>
+ <Descriptions.Item v-if="ocrResult.taxRate != null" label="绋庣巼(%)">
+ {{ ocrResult.taxRate }}
+ </Descriptions.Item>
+ <Descriptions.Item v-if="ocrResult.remark" label="澶囨敞" :span="2">
+ {{ ocrResult.remark }}
+ </Descriptions.Item>
+ </Descriptions>
+ </div>
+
+ <Alert
+ message="閿�鍞柟鍚嶇О浠呬负鍙戠エ鏂囨湰璇嗗埆缁撴灉锛岃鍦ㄨ〃鍗曚腑閫夋嫨閲囪喘璁㈠崟鍚庢牳瀵硅鍗曚緵搴斿晢涓庨攢鍞柟鏄惁涓�鑷达紱璇嗗埆鏁版嵁鍙墜鍔ㄤ慨鏀广��"
+ type="info"
+ show-icon
+ class="mb-4"
+ />
+
+ <div class="flex justify-end gap-2">
+ <Button @click="handleReset">閲嶆柊涓婁紶</Button>
+ <Button type="primary" @click="handleConfirm">纭骞跺~鍏ヨ〃鍗�</Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/erp/purchase/invoice/modules/purchase-order-select.vue b/src/views/erp/purchase/invoice/modules/purchase-order-select.vue
new file mode 100644
index 0000000..59e969b
--- /dev/null
+++ b/src/views/erp/purchase/invoice/modules/purchase-order-select.vue
@@ -0,0 +1,194 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
+
+import { computed, nextTick, ref, watch } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { IconifyIcon } from '@vben/icons';
+
+import { Input, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ getPurchaseOrder,
+ getPurchaseOrderPage,
+} from '#/api/erp/purchase/order';
+
+defineOptions({ name: 'ErpPurchaseInvoiceOrderSelect' });
+
+const props = withDefaults(
+ defineProps<{
+ disabled?: boolean;
+ modelValue?: number;
+ placeholder?: string;
+ }>(),
+ {
+ disabled: false,
+ modelValue: undefined,
+ placeholder: '璇烽�夋嫨閲囪喘璁㈠崟',
+ },
+);
+const emit = defineEmits<{
+ change: [order: ErpPurchaseOrderApi.PurchaseOrder | undefined];
+ 'update:modelValue': [value: number | undefined];
+}>();
+
+const open = ref(false); // 閫夋嫨寮圭獥鏄惁鎵撳紑
+const order = ref<ErpPurchaseOrderApi.PurchaseOrder>(); // 褰撳墠閫変腑閲囪喘璁㈠崟
+
+const displayNo = computed(() => order.value?.no ?? '');
+
+/** 鏍规嵁璁㈠崟缂栧彿鍥炴樉閫夋嫨鍣� */
+async function resolveOrderById(id: number | undefined) {
+ if (!id) {
+ order.value = undefined;
+ return;
+ }
+ if (order.value?.id === id) {
+ return;
+ }
+ order.value = await getPurchaseOrder(id);
+}
+
+watch(
+ () => props.modelValue,
+ (value) => {
+ resolveOrderById(value);
+ },
+ { immediate: true },
+);
+
+/** 琛ㄦ牸閰嶇疆 */
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: [
+ {
+ fieldName: 'no',
+ label: '璁㈠崟鍗曞彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鍗曞崟鍙�',
+ allowClear: true,
+ },
+ },
+ ],
+ },
+ gridOptions: {
+ columns: [
+ { type: 'radio', width: 50, fixed: 'left' },
+ { field: 'no', title: '璁㈠崟鍗曞彿', width: 200, fixed: 'left' },
+ { field: 'supplierName', title: '渚涘簲鍟�', minWidth: 120 },
+ {
+ field: 'orderTime',
+ title: '璁㈠崟鏃堕棿',
+ width: 160,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'totalPrice',
+ title: '鍚◣閲戦',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'remainInvoicePrice',
+ title: '鏈紑绁ㄩ噾棰�',
+ formatter: 'formatAmount2',
+ minWidth: 120,
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ERP_AUDIT_STATUS },
+ },
+ },
+ ],
+ height: 520,
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPurchaseOrderPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ status: 20, // 浠呭睍绀哄凡瀹℃牳閫氳繃鐨勯噰璐鍗�
+ remainPriceEnable: true, // 浠呭睍绀哄墿浣欏彲寮�绁ㄩ噾棰濆ぇ浜� 0 鐨勮鍗曪紙閲戦宸插紑瀹岀殑涓嶅睍绀猴級
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ radioConfig: {
+ trigger: 'row',
+ highlight: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<ErpPurchaseOrderApi.PurchaseOrder>,
+ gridEvents: {
+ radioChange: ({ row }: { row: ErpPurchaseOrderApi.PurchaseOrder }) => {
+ order.value = row;
+ },
+ },
+});
+
+/** 鎵撳紑閫夋嫨寮圭獥 */
+async function handleOpen() {
+ if (props.disabled) {
+ return;
+ }
+ open.value = true;
+ await nextTick();
+ gridApi.query();
+}
+
+/** 纭閫夋嫨閲囪喘璁㈠崟 */
+function handleOk() {
+ if (!order.value?.id) {
+ message.warning('璇烽�夋嫨閲囪喘璁㈠崟');
+ return;
+ }
+ emit('update:modelValue', order.value.id);
+ emit('change', order.value);
+ open.value = false;
+}
+</script>
+
+<template>
+ <div>
+ <Input
+ readonly
+ :disabled="disabled"
+ :placeholder="placeholder"
+ :value="displayNo"
+ @click="handleOpen"
+ >
+ <template #suffix>
+ <IconifyIcon
+ class="size-4 cursor-pointer"
+ icon="lucide:search"
+ :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
+ />
+ </template>
+ </Input>
+ <Modal
+ v-model:open="open"
+ title="閫夋嫨閲囪喘璁㈠崟锛堜粎宸插鏍搁�氳繃锛�"
+ width="80%"
+ @cancel.stop="open = false"
+ @ok.stop="handleOk"
+ >
+ <Grid table-title="閲囪喘璁㈠崟鍒楄〃" />
+ </Modal>
+ </div>
+</template>
diff --git a/src/views/erp/purchase/order/data.ts b/src/views/erp/purchase/order/data.ts
index 19ccc26..0676202 100644
--- a/src/views/erp/purchase/order/data.ts
+++ b/src/views/erp/purchase/order/data.ts
@@ -12,7 +12,7 @@
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
-import { getRangePickerDefaultProps } from '#/utils';
+import { getRangePickerDefaultProps, withoutFixedColumns } from '#/utils';
/** 琛ㄥ崟绫诲瀷 */
export type FormType = 'create' | 'detail' | 'edit';
@@ -65,14 +65,27 @@
},
{
component: 'InputNumber',
- componentProps: {
+ componentProps: (values) => ({
class: '!w-full',
placeholder: '璇疯緭鍏ユ敮浠樿閲�',
precision: 2,
min: 0,
- },
+ max: values.totalPrice ?? undefined,
+ }),
fieldName: 'depositPrice',
label: '鏀粯璁㈤噾',
+ dependencies: {
+ triggerFields: ['totalPrice'],
+ rules(values) {
+ if (values.totalPrice != null) {
+ return z
+ .number()
+ .min(0, '璁㈤噾涓嶈兘灏忎簬0')
+ .max(values.totalPrice, '璁㈤噾涓嶈兘澶т簬浼樻儬鍚庨噾棰�');
+ }
+ return z.number().min(0, '璁㈤噾涓嶈兘灏忎簬0');
+ },
+ },
rules: 'required',
},
{
@@ -162,7 +175,7 @@
export function useFormItemColumns(
disabled: boolean,
): VxeTableGridOptions['columns'] {
- return [
+ const columns: VxeTableGridOptions['columns'] = [
{ type: 'seq', title: '搴忓彿', minWidth: 50, fixed: 'left' },
{
field: 'productId',
@@ -238,6 +251,8 @@
visible: !disabled,
},
];
+ // 鏌ョ湅瀹℃壒鏄庣粏锛堝彧璇伙級鏃跺彇娑堝浐瀹氬垪锛岄伩鍏嶇獎寮圭獥閲屾尋鎺変骇鍝佸悕绉般�佸崟浣嶃�佸娉ㄧ瓑鍒�
+ return disabled ? withoutFixedColumns(columns) : columns;
}
/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
diff --git a/src/views/erp/purchase/order/detail/index.vue b/src/views/erp/purchase/order/detail/index.vue
new file mode 100644
index 0000000..fadc835
--- /dev/null
+++ b/src/views/erp/purchase/order/detail/index.vue
@@ -0,0 +1,134 @@
+<script lang="ts" setup>
+import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ formatDateTime,
+} from '@vben/utils';
+
+import { Spin, Tag } from 'ant-design-vue';
+
+import { getPurchaseOrder } from '#/api/erp/purchase/order';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+import ItemForm from '../modules/item-form.vue';
+
+defineOptions({ name: 'ErpPurchaseOrderDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+/** 鍏ュ簱鐘舵�佹覆鏌� */
+function renderInStatus(val?: number) {
+ const map: Record<number, { text: string; color: string }> = {
+ 0: { text: '鏈叆搴�', color: 'default' },
+ 1: { text: '閮ㄥ垎鍏ュ簱', color: 'warning' },
+ 2: { text: '鍏ㄩ儴鍏ュ簱', color: 'success' },
+ };
+ const item = map[val ?? 0]!;
+ return h(Tag, { color: item.color }, () => item.text);
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '閲囪喘璁㈠崟鍙�', field: 'no' },
+ { label: '渚涘簲鍟嗗悕绉�', field: 'supplierName', render: textRender },
+ {
+ label: '璁㈠崟鏃堕棿',
+ field: 'orderTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '鍚堣鏁伴噺',
+ field: 'totalCount',
+ render: (val) => erpCountInputFormatter(val) || '-',
+ },
+ {
+ label: '鍚堣閲戦',
+ field: 'totalPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '浼樻儬閲戦',
+ field: 'discountPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '瀹氶噾閲戦',
+ field: 'depositPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
+ },
+ {
+ label: '鍏ュ簱鐘舵��',
+ field: 'inStatus',
+ render: (val) => renderInStatus(val as number),
+ },
+ { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getPurchaseOrder(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ <!-- 閲囪喘鏄庣粏 -->
+ <div
+ v-if="formData?.items?.length"
+ class="rounded-lg border border-border bg-background p-4"
+ >
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">閲囪喘鏄庣粏</span>
+ </div>
+ <ItemForm :items="formData.items" :disabled="true" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/erp/purchase/order/index.vue b/src/views/erp/purchase/order/index.vue
index c56c84d..6189ea1 100644
--- a/src/views/erp/purchase/order/index.vue
+++ b/src/views/erp/purchase/order/index.vue
@@ -16,12 +16,12 @@
deletePurchaseOrder,
exportPurchaseOrder,
getPurchaseOrderPage,
- updatePurchaseOrderStatus,
} from '#/api/erp/purchase/order';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
/** ERP 閲囪喘璁㈠崟鍒楄〃 */
defineOptions({ name: 'ErpPurchaseOrder' });
@@ -30,6 +30,11 @@
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -42,6 +47,11 @@
async function handleExport() {
const data = await exportPurchaseOrder(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '閲囪喘璁㈠崟.xls', source: data });
+}
+
+/** 瀵煎叆閲囪喘璁㈠崟 */
+function handleImport() {
+ importModalApi.open();
}
/** 鏂板閲囪喘璁㈠崟 */
@@ -63,24 +73,6 @@
try {
await deletePurchaseOrder(ids);
message.success($t('ui.actionMessage.deleteSuccess'));
- handleRefresh();
- } finally {
- hideLoading();
- }
-}
-
-/** 瀹℃壒/鍙嶅鎵规搷浣� */
-async function handleUpdateStatus(
- row: ErpPurchaseOrderApi.PurchaseOrder,
- status: number,
-) {
- const hideLoading = message.loading({
- content: `纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヨ鍗曞悧锛焋,
- duration: 0,
- });
- try {
- await updatePurchaseOrderStatus(row.id!, status);
- message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`);
handleRefresh();
} finally {
hideLoading();
@@ -159,7 +151,9 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<Grid table-title="閲囪喘璁㈠崟鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -177,6 +171,13 @@
icon: ACTION_ICON.DOWNLOAD,
auth: ['erp:purchase-order:export'],
onClick: handleExport,
+ },
+ {
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['erp:purchase-order:import'],
+ onClick: handleImport,
},
{
label: '鎵归噺鍒犻櫎',
@@ -215,20 +216,6 @@
auth: ['erp:purchase-order:update'],
ifShow: () => row.status !== 20,
onClick: handleEdit.bind(null, row),
- },
- {
- label: row.status === 10 ? '瀹℃壒' : '鍙嶅鎵�',
- type: 'link',
- icon: ACTION_ICON.AUDIT,
- auth: ['erp:purchase-order:update-status'],
- popConfirm: {
- title: `纭${row.status === 10 ? '瀹℃壒' : '鍙嶅鎵�'}${row.no}鍚楋紵`,
- confirm: handleUpdateStatus.bind(
- null,
- row,
- row.status === 10 ? 20 : 10,
- ),
- },
},
{
label: '纭鏀惰揣',
diff --git a/src/views/erp/purchase/order/modules/import-form.vue b/src/views/erp/purchase/order/modules/import-form.vue
new file mode 100644
index 0000000..027d268
--- /dev/null
+++ b/src/views/erp/purchase/order/modules/import-form.vue
@@ -0,0 +1,167 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/erp/purchase/order';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Switch, Upload } from 'ant-design-vue';
+
+import {
+ importPurchaseOrder,
+ importPurchaseOrderTemplate,
+} from '#/api/erp/purchase/order';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+/** 鏄惁灏嗗鍏ョ殑璁㈠崟鐩存帴缃负鈥滃凡瀹℃牳鈥� */
+const directApprove = ref(false);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importPurchaseOrder(
+ fileList.value[0] as File,
+ directApprove.value,
+ );
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importPurchaseOrderTemplate();
+ downloadFileFromBlobPart({ fileName: '閲囪喘璁㈠崟瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureList ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆閲囪喘璁㈠崟" class="w-[720px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div class="mt-4">
+ <span class="mr-2">鏄惁瀹℃牳</span>
+ <Switch v-model:checked="directApprove" />
+ </div>
+ <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+ <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑鈥滀緵搴斿晢鍚嶇О鈥濇寜鍚嶇О锛堟垨缂栫爜锛夊~鍐欙紝闇�涓庣郴缁熷唴渚涘簲鍟嗕竴鑷达紱鈥滀骇鍝佺紪鐮佲�濇寜鐗╂枡缂栫爜濉啓锛堚�滀骇鍝佸悕绉扳�濆垪浠呬緵瀵圭収锛屽尮閰嶄互浜у搧缂栫爜涓哄噯锛夛紱鈥滈噰璐椂闂粹�濇牸寮忓 2026-01-01 10:00:00锛岀暀绌哄彇褰撳墠鏃堕棿銆�</p>
+ <p>涓�寮犻噰璐崟鍙惈澶氭潯鏄庣粏锛氱浉鍚屸�滈噰璐崟缂栧彿鈥濈殑澶氳浼氬悎骞朵负涓�寮犺鍗曪紙涓昏〃瀛楁鍙栬缁勯琛岋級锛涒�滈噰璐崟缂栧彿鈥濈暀绌烘椂姣忎竴琛屽悇鐢熸垚涓�寮犳柊璁㈠崟銆傞噰璐崟缂栧彿宸插瓨鍦ㄦ椂浼氬鍏ュけ璐ャ��</p>
+ <p>璁㈠崟鐘舵�侊細鍕鹃�変笂鏂瑰紑鍏� = 鐩存帴缃负鈥滃凡瀹℃牳鈥濓紱涓嶅嬀閫� = 鈥滄湭瀹℃牳鈥濄�傚鍏ヤ笉浼氳嚜鍔ㄦ彁浜ゅ鎵规祦绋嬨��</p>
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆閲囪喘璁㈠崟鏁版嵁锛岃绋嶅��...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex gap-4">
+ <span class="text-green-600">鏂板锛歿{ createCount }} 寮�</span>
+ <span class="text-blue-600">瑕嗙洊鏇存柊锛歿{ updateCount }} 寮�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 寮�</span>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureList"
+ :key="name"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/erp/purchase/request/data.ts b/src/views/erp/purchase/request/data.ts
index cc4745b..ac87d16 100644
--- a/src/views/erp/purchase/request/data.ts
+++ b/src/views/erp/purchase/request/data.ts
@@ -271,8 +271,8 @@
options: [
{ label: '鏈彁浜�', value: 0 },
{ label: '瀹℃壒涓�', value: 10 },
- { label: '瀹℃牳閫氳繃', value: 20 },
- { label: '瀹℃牳涓嶉�氳繃', value: 30 },
+ { label: '瀹℃壒閫氳繃', value: 20 },
+ { label: '瀹℃壒涓嶉�氳繃', value: 30 },
{ label: '宸插彇娑�', value: 40 },
],
placeholder: '璇烽�夋嫨鐘舵��',
diff --git a/src/views/erp/purchase/request/detail/index.vue b/src/views/erp/purchase/request/detail/index.vue
new file mode 100644
index 0000000..62c383e
--- /dev/null
+++ b/src/views/erp/purchase/request/detail/index.vue
@@ -0,0 +1,181 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { ErpPurchaseRequestApi } from '#/api/erp/purchase/request';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, nextTick, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ formatDateTime,
+} from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getPurchaseRequest } from '#/api/erp/purchase/request';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'ErpPurchaseRequestDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<ErpPurchaseRequestApi.PurchaseRequest>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+/** 閲囪喘鐢宠鏄庣粏锛堝鎵硅鎯呭彧璇诲垪锛� */
+function useDetailItemColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { type: 'seq', title: '搴忓彿', width: 60 },
+ { field: 'productName', title: '浜у搧鍚嶇О', minWidth: 180 },
+ { field: 'productUnitName', title: '鍗曚綅', minWidth: 80 },
+ {
+ field: 'count',
+ title: '鏁伴噺',
+ minWidth: 100,
+ formatter: ({ cellValue }) => erpCountInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'productPrice',
+ title: '鍗曚环锛堝弬鑰冧环锛�',
+ minWidth: 120,
+ formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'totalPrice',
+ title: '棰勪及閲戦',
+ minWidth: 120,
+ formatter: ({ cellValue }) => erpPriceInputFormatter(cellValue) || '-',
+ },
+ {
+ field: 'taxPercent',
+ title: '绋庣巼(%)',
+ minWidth: 90,
+ formatter: ({ cellValue }) => (cellValue ? `${cellValue}%` : '-'),
+ },
+ { field: 'demandTime', title: '闇�姹傛棩鏈�', minWidth: 110 },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 160,
+ showOverflow: 'tooltip',
+ },
+ {
+ field: 'qcCheckFlag',
+ title: '鏄惁璐ㄦ',
+ minWidth: 80,
+ formatter: ({ cellValue }) => (cellValue ? '鏄�' : '鍚�'),
+ },
+ ];
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useDetailItemColumns(),
+ data: [],
+ minHeight: 200,
+ autoResize: true,
+ border: true,
+ rowConfig: {
+ keyField: 'seq',
+ isHover: true,
+ },
+ pagerConfig: {
+ enabled: false,
+ },
+ toolbarConfig: {
+ enabled: false,
+ },
+ },
+});
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鐢宠缂栧彿', field: 'no' },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
+ },
+ { label: '鐢宠浜�', field: 'requestUserName', render: textRender },
+ { label: '鐢宠閮ㄩ棬', field: 'requestDeptName', render: textRender },
+ {
+ label: '鐢宠鏃堕棿',
+ field: 'requestTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '渚涘簲鍟嗗悕绉�', field: 'supplierName', render: textRender },
+ {
+ label: '鍚堣鏁伴噺',
+ field: 'totalCount',
+ render: (val) => erpCountInputFormatter(val) || '-',
+ },
+ {
+ label: '鍚堣閲戦',
+ field: 'totalPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ { label: '鐢宠鐞嗙敱', field: 'requestReason', render: textRender },
+ { label: '鐢熸垚閲囪喘璁㈠崟', field: 'orderNo', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getPurchaseRequest(Number(props.id));
+ if (formData.value?.items?.length) {
+ await nextTick();
+ await gridApi.grid.reloadData(formData.value.items);
+ }
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ <!-- 鐢宠鏄庣粏 -->
+ <div
+ v-if="formData?.items?.length"
+ class="rounded-lg border border-border bg-background p-4"
+ >
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鐢宠鏄庣粏</span>
+ </div>
+ <Grid />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/erp/purchase/request/index.vue b/src/views/erp/purchase/request/index.vue
index cd97ba8..9957565 100644
--- a/src/views/erp/purchase/request/index.vue
+++ b/src/views/erp/purchase/request/index.vue
@@ -17,11 +17,13 @@
getApproveProcessList,
getPurchaseRequestPage,
submitPurchaseRequest,
+ syncPurchaseRequestStatus,
} from '#/api/erp/purchase/request';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
import OrderForm from './modules/order-form.vue';
import ProcessSelectModal from './modules/process-select-modal.vue';
@@ -32,6 +34,11 @@
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -54,6 +61,11 @@
async function handleExport() {
const data = await exportPurchaseRequest(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '閲囪喘鐢宠.xls', source: data });
+}
+
+/** 瀵煎叆閲囪喘鐢宠 */
+function handleImport() {
+ importModalApi.open();
}
/** 鏂板閲囪喘鐢宠 */
@@ -145,11 +157,18 @@
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
- return await getPurchaseRequestPage({
+ const result = await getPurchaseRequestPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
+ const processingItems = (result.list || []).filter(
+ (item) => item.status === 10 && item.processInstanceId,
+ );
+ if (processingItems.length > 0) {
+ await syncPurchaseRequestStatus(processingItems);
+ }
+ return result;
},
},
},
@@ -172,6 +191,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<OrderFormModal @success="handleRefresh" />
<ProcessModal @success="handleRefresh" />
<Grid table-title="閲囪喘鐢宠鍒楄〃">
@@ -193,6 +213,13 @@
onClick: handleExport,
},
{
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['erp:purchase-request:import'],
+ onClick: handleImport,
+ },
+ {
label: '鎵归噺鍒犻櫎',
type: 'primary',
danger: true,
@@ -210,8 +237,8 @@
<template #status="{ row }">
<Tag v-if="row.status === 0" color="default">鏈彁浜�</Tag>
<Tag v-else-if="row.status === 10" color="processing">瀹℃壒涓�</Tag>
- <Tag v-else-if="row.status === 20" color="success">瀹℃牳閫氳繃</Tag>
- <Tag v-else-if="row.status === 30" color="error">瀹℃牳涓嶉�氳繃</Tag>
+ <Tag v-else-if="row.status === 20" color="success">瀹℃壒閫氳繃</Tag>
+ <Tag v-else-if="row.status === 30" color="error">瀹℃壒涓嶉�氳繃</Tag>
<Tag v-else-if="row.status === 40" color="warning">宸插彇娑�</Tag>
</template>
<template #inStatus="{ row }">
@@ -234,7 +261,7 @@
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['erp:purchase-request:update'],
- ifShow: () => row.status === 0,
+ ifShow: () => row.status === 0 || Number(row.status) === 30,
onClick: handleEdit.bind(null, row),
},
{
diff --git a/src/views/erp/purchase/request/modules/form.vue b/src/views/erp/purchase/request/modules/form.vue
index 7444f4e..ff87d9a 100644
--- a/src/views/erp/purchase/request/modules/form.vue
+++ b/src/views/erp/purchase/request/modules/form.vue
@@ -13,6 +13,8 @@
import {
createPurchaseRequest,
getPurchaseRequest,
+ resetPurchaseRequestStatus,
+ syncPurchaseRequestStatus,
updatePurchaseRequest,
} from '#/api/erp/purchase/request';
import { $t } from '#/locales';
@@ -88,6 +90,13 @@
remark: item.remark,
}));
try {
+ console.log('[DEBUG] formType:', formType.value, 'status:', formData.value?.status, 'type:', typeof formData.value?.status);
+ if (formType.value === 'edit' && Number(formData.value?.status) !== 0) {
+ if (Number(formData.value?.status) !== 30) {
+ await syncPurchaseRequestStatus([formData.value!]);
+ }
+ await resetPurchaseRequestStatus(formData.value!.id!);
+ }
await (formType.value === 'create'
? createPurchaseRequest(data)
: updatePurchaseRequest(data));
diff --git a/src/views/erp/purchase/request/modules/import-form.vue b/src/views/erp/purchase/request/modules/import-form.vue
new file mode 100644
index 0000000..4d3a9be
--- /dev/null
+++ b/src/views/erp/purchase/request/modules/import-form.vue
@@ -0,0 +1,158 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/erp/purchase/request';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import {
+ importPurchaseRequest,
+ importPurchaseRequestTemplate,
+} from '#/api/erp/purchase/request';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importPurchaseRequest(fileList.value[0] as File);
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importPurchaseRequestTemplate();
+ downloadFileFromBlobPart({ fileName: '閲囪喘鐢宠瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureList ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆閲囪喘鐢宠" class="w-[720px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+ <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涘繀濉垪锛氫骇鍝佺紪鐮併�佹暟閲忋�傗�滀骇鍝佺紪鐮佲�濇寜鐗╂枡缂栫爜濉啓锛堚�滀骇鍝佸悕绉扳�濆垪浠呬緵瀵圭収锛夛紝渚涘簲鍟嗗彲绌猴紙濉簡闇�涓庣郴缁熶緵搴斿晢涓�鑷达級锛屸�滈渶姹傛棩鏈熲�濇牸寮忓 2026-01-01锛屸�滄槸鍚︽潵鏂欐楠屸�濆~鈥滄槸/鍚︹�濄��</p>
+ <p>涓�寮犻噰璐敵璇峰彲鍚鏉℃槑缁嗭細鐩稿悓鈥滈噰璐敵璇峰崟鍙封�濈殑澶氳浼氬悎骞朵负涓�寮犵敵璇凤紙涓昏〃瀛楁鍙栬缁勯琛岋級锛涒�滈噰璐敵璇峰崟鍙封�濈暀绌烘椂姣忎竴琛屽悇鐢熸垚涓�寮犳柊鐢宠銆傞噰璐敵璇峰崟鍙峰凡瀛樺湪鏃朵細瀵煎叆澶辫触銆�</p>
+ <p>瀵煎叆鐨勭敵璇凤紝鍏躲�岀敵璇蜂汉 / 鐢宠閮ㄩ棬銆嶆寜褰撳墠鐧诲綍鐢ㄦ埛鑷姩甯﹀嚭锛涚姸鎬佷负鈥滄湭鎻愪氦鈥濓紝鍙湪鍒楄〃涓户缁紪杈戞垨鎻愪氦瀹℃壒銆�</p>
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆閲囪喘鐢宠鏁版嵁锛岃绋嶅��...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex gap-4">
+ <span class="text-green-600">鏂板锛歿{ createCount }} 鏉�</span>
+ <span class="text-blue-600">瑕嗙洊鏇存柊锛歿{ updateCount }} 鏉�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 鏉�</span>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureList"
+ :key="name"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/erp/sale/order/data.ts b/src/views/erp/sale/order/data.ts
index 9597b94..fc572c7 100644
--- a/src/views/erp/sale/order/data.ts
+++ b/src/views/erp/sale/order/data.ts
@@ -7,7 +7,7 @@
import { getAccountSimpleList } from '#/api/erp/finance/account';
import { getItemPage } from '#/api/mdm/item';
import { getSimpleUserList } from '#/api/system/user';
-import { getRangePickerDefaultProps } from '#/utils';
+import { getRangePickerDefaultProps, withoutFixedColumns } from '#/utils';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { markRaw } from 'vue';
@@ -181,14 +181,27 @@
},
{
component: 'InputNumber',
- componentProps: {
+ componentProps: (values) => ({
class: '!w-full',
placeholder: '璇疯緭鍏ユ敹鍙栬閲�',
precision: 2,
min: 0,
- },
+ max: values.totalPrice ?? undefined,
+ }),
fieldName: 'depositPrice',
label: '鏀跺彇璁㈤噾',
+ dependencies: {
+ triggerFields: ['totalPrice'],
+ rules(values) {
+ if (values.totalPrice != null) {
+ return z
+ .number()
+ .min(0, '璁㈤噾涓嶈兘灏忎簬0')
+ .max(values.totalPrice, '鏀跺彇璁㈤噾涓嶈兘澶т簬浼樻儬鍚庨噾棰�');
+ }
+ return z.number().min(0, '璁㈤噾涓嶈兘灏忎簬0').optional();
+ },
+ },
rules: z.number().min(0).optional(),
},
];
@@ -198,7 +211,7 @@
export function useFormItemColumns(
disabled: boolean,
): VxeTableGridOptions['columns'] {
- return [
+ const columns: VxeTableGridOptions['columns'] = [
{ type: 'seq', title: '搴忓彿', minWidth: 50, fixed: 'left' },
{
field: 'productId',
@@ -298,6 +311,8 @@
visible: !disabled,
},
];
+ // 鏌ョ湅瀹℃壒鏄庣粏锛堝彧璇伙級鏃跺彇娑堝浐瀹氬垪锛岄伩鍏嶇獎寮圭獥閲屾尋鎺変骇鍝佸悕绉般�佸崟浣嶃�佸娉ㄧ瓑鍒�
+ return disabled ? withoutFixedColumns(columns) : columns;
}
/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
@@ -365,8 +380,10 @@
component: 'Select',
componentProps: {
options: [
- { label: '鏈鏍�', value: 10 },
+ { label: '鏈彁浜�', value: 0 },
+ { label: '瀹℃壒涓�', value: 15 },
{ label: '宸插鏍�', value: 20 },
+ { label: '宸查┏鍥�', value: 35 },
{ label: '宸蹭綔搴�', value: 30 },
],
placeholder: '璇烽�夋嫨鐘舵��',
diff --git a/src/views/erp/sale/order/detail/index.vue b/src/views/erp/sale/order/detail/index.vue
new file mode 100644
index 0000000..673f4a4
--- /dev/null
+++ b/src/views/erp/sale/order/detail/index.vue
@@ -0,0 +1,154 @@
+<script lang="ts" setup>
+import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ formatDateTime,
+} from '@vben/utils';
+
+import { Spin, Tag } from 'ant-design-vue';
+
+import { getSaleOrder } from '#/api/erp/sale/order';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+import ItemForm from '../modules/item-form.vue';
+
+defineOptions({ name: 'ErpSaleOrderDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<ErpSaleOrderApi.SaleOrder>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+/** 鍑哄簱鐘舵�佹覆鏌� */
+function renderOutStatus(val?: number) {
+ const map: Record<number, { text: string; color: string }> = {
+ 0: { text: '鏈嚭搴�', color: 'default' },
+ 1: { text: '宸查鐣�', color: 'processing' },
+ 2: { text: '閮ㄥ垎鍑哄簱', color: 'warning' },
+ 3: { text: '鍏ㄩ儴鍑哄簱', color: 'success' },
+ };
+ const item = map[val ?? 0]!;
+ return h(Tag, { color: item.color }, () => item.text);
+}
+
+/** 鐢熶骇鐘舵�佹覆鏌� */
+function renderProductionStatus(val?: number) {
+ if (formData.value?.needProduction !== 1) {
+ return h(Tag, { color: 'default' }, () => '鏃犻渶鐢熶骇');
+ }
+ const map: Record<number, { text: string; color: string }> = {
+ 0: { text: '鏈畬鎴�', color: 'warning' },
+ 1: { text: '宸插畬鎴�', color: 'success' },
+ };
+ const item = map[val ?? 0]!;
+ return h(Tag, { color: item.color }, () => item.text);
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '璁㈠崟鍗曞彿', field: 'no', render: textRender },
+ { label: '瀹㈡埛鍚嶇О', field: 'customerName', render: textRender },
+ { label: '鍏宠仈鍚堝悓', field: 'contractNo', render: textRender },
+ {
+ label: '璁㈠崟鏃堕棿',
+ field: 'orderTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '鍚堣鏁伴噺',
+ field: 'totalCount',
+ render: (val) => erpCountInputFormatter(val) || '-',
+ },
+ {
+ label: '鍚堣閲戦',
+ field: 'totalPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '浼樻儬閲戦',
+ field: 'discountPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '瀹氶噾閲戦',
+ field: 'depositPrice',
+ render: (val) => erpPriceInputFormatter(val) || '-',
+ },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.ERP_AUDIT_STATUS, value: val }),
+ },
+ {
+ label: '鍑哄簱鐘舵��',
+ field: 'outStatus',
+ render: (val) => renderOutStatus(val as number),
+ },
+ {
+ label: '鐢熶骇鐘舵��',
+ field: 'productionStatus',
+ render: (val) => renderProductionStatus(val as number),
+ },
+ { label: '鍒涘缓浜�', field: 'creatorName', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getSaleOrder(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ <!-- 閿�鍞槑缁� -->
+ <div
+ v-if="formData?.items?.length"
+ class="rounded-lg border border-border bg-background p-4"
+ >
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">閿�鍞槑缁�</span>
+ </div>
+ <ItemForm :items="formData.items" :disabled="true" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/erp/sale/order/index.vue b/src/views/erp/sale/order/index.vue
index 7071c8e..5d32a61 100644
--- a/src/views/erp/sale/order/index.vue
+++ b/src/views/erp/sale/order/index.vue
@@ -16,14 +16,17 @@
deleteSaleOrder,
enableSaleOrder,
exportSaleOrder,
+ getSaleOrderApproveProcessList,
getSaleOrderPage,
- updateSaleOrderStatus,
+ submitSaleOrder,
} from '#/api/erp/sale/order';
import { generateFromSaleOrder } from '#/api/mes/wm/salesnotice';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
+import SaleOrderPrint from './modules/print.vue';
import SalesNoticeForm from '#/views/wls/salesnotice/modules/form.vue';
/** ERP 閿�鍞鍗曞垪琛� */
@@ -34,6 +37,11 @@
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -51,6 +59,11 @@
async function handleExport() {
const data = await exportSaleOrder(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '閿�鍞鍗�.xls', source: data });
+}
+
+/** 瀵煎叆閿�鍞鍗� */
+function handleImport() {
+ importModalApi.open();
}
/** 鏂板閿�鍞鍗� */
@@ -78,21 +91,34 @@
}
}
-/** 瀹℃壒/鍙嶅鎵规搷浣� */
-async function handleUpdateStatus(
- row: ErpSaleOrderApi.SaleOrder,
- status: number,
-) {
+/** 鎻愪氦閿�鍞鍗曞鎵癸紙鍗忓悓鍔炲叕锛� */
+async function handleSubmit(row: ErpSaleOrderApi.SaleOrder) {
+ // 鑾峰彇鍙敤鐨勫鎵规祦绋嬪畾涔�
+ const processList = await getSaleOrderApproveProcessList();
+ if (isEmpty(processList)) {
+ message.error('閿�鍞鍗曞鎵规祦绋嬫湭閰嶇疆锛岃鍏堝湪銆屽崗鍚屽姙鍏啋娴佺▼绠$悊銆嶄腑鍒涘缓骞跺彂甯冮攢鍞鍗曞鎵规祦绋�');
+ return;
+ }
const hideLoading = message.loading({
- content: `纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヨ鍗曞悧锛焋,
+ content: '姝e湪鎻愪氦瀹℃壒...',
duration: 0,
});
try {
- await updateSaleOrderStatus(row.id!, status);
- message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`);
+ await submitSaleOrder(row.id!, processList[0].key);
+ message.success('鎻愪氦瀹℃壒鎴愬姛锛岃鍒板崗鍚屽姙鍏腑澶勭悊');
handleRefresh();
} finally {
hideLoading();
+ }
+}
+
+/** 鏌ョ湅瀹℃壒杩涘害锛堣烦杞崗鍚屽姙鍏祦绋嬭鎯咃紝鍙妯″紡锛屼笉鏄剧ず瀹℃壒鎿嶄綔锛� */
+function handleViewApproval(row: ErpSaleOrderApi.SaleOrder) {
+ if (row.processInstanceId) {
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: row.processInstanceId, viewOnly: '1' },
+ });
}
}
@@ -138,6 +164,12 @@
/** 鏌ョ湅璇︽儏 */
function handleDetail(row: ErpSaleOrderApi.SaleOrder) {
formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+/** 鎵撳嵃/瀵煎嚭閿�鍞鍗曡鎯咃紝渚涗紒涓氱洊绔犵暀瀛� */
+const printRef = ref<InstanceType<typeof SaleOrderPrint>>();
+function handlePrint(row: ErpSaleOrderApi.SaleOrder) {
+ printRef.value?.print(row.id!);
}
/** 鏌ョ湅鍏宠仈鍚堝悓 */
@@ -219,7 +251,9 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<SalesNoticeFormModal />
+ <SaleOrderPrint ref="printRef" />
<Grid table-title="閿�鍞鍗曞垪琛�">
<template #toolbar-tools>
<TableAction
@@ -237,6 +271,13 @@
icon: ACTION_ICON.DOWNLOAD,
auth: ['erp:sale-order:export'],
onClick: handleExport,
+ },
+ {
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['erp:sale-order:import'],
+ onClick: handleImport,
},
{
label: '鎵归噺鍒犻櫎',
@@ -270,8 +311,10 @@
<span v-else>-</span>
</template>
<template #status="{ row }">
- <Tag v-if="row.status === 10" color="warning">鏈鏍�</Tag>
+ <Tag v-if="row.status === 0" color="default">鏈彁浜�</Tag>
+ <Tag v-if="row.status === 15" color="processing">瀹℃壒涓�</Tag>
<Tag v-if="row.status === 20" color="success">宸插鏍�</Tag>
+ <Tag v-if="row.status === 35" color="warning">宸查┏鍥�</Tag>
<Tag v-if="row.status === 30" color="error">宸蹭綔搴�</Tag>
</template>
<template #needProduction="{ row }">
@@ -304,34 +347,38 @@
onClick: handleDetail.bind(null, row),
},
{
+ label: '鎵撳嵃',
+ type: 'link',
+ icon: 'lucide:printer',
+ auth: ['erp:sale-order:query'],
+ onClick: handlePrint.bind(null, row),
+ },
+ {
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
auth: ['erp:sale-order:update'],
- ifShow: () => row.status === 10,
+ ifShow: () => row.status === 0 || row.status === 35,
onClick: handleEdit.bind(null, row),
},
{
- label: '瀹℃壒',
+ label: '鎻愪氦瀹℃壒',
type: 'link',
icon: ACTION_ICON.AUDIT,
auth: ['erp:sale-order:update-status'],
- ifShow: () => row.status === 10,
+ ifShow: () => row.status === 0 || row.status === 35,
popConfirm: {
- title: `纭瀹℃壒${row.no}鍚楋紵`,
- confirm: handleUpdateStatus.bind(null, row, 20),
+ title: `纭鎻愪氦${row.no}鐨勫鎵瑰悧锛熸彁浜ゅ悗瀹℃壒灏嗗湪鍗忓悓鍔炲叕涓畬鎴恅,
+ confirm: handleSubmit.bind(null, row),
},
},
{
- label: '鍙嶅鎵�',
+ label: '瀹℃壒杩涘害',
type: 'link',
- icon: ACTION_ICON.AUDIT,
- auth: ['erp:sale-order:update-status'],
- ifShow: () => row.status === 20,
- popConfirm: {
- title: `纭鍙嶅鎵�${row.no}鍚楋紵`,
- confirm: handleUpdateStatus.bind(null, row, 10),
- },
+ icon: 'lucide:file-clock',
+ auth: ['erp:sale-order:query'],
+ ifShow: () => row.status === 15 && !!row.processInstanceId,
+ onClick: handleViewApproval.bind(null, row),
},
{
label: '浣滃簾',
@@ -360,7 +407,7 @@
type: 'link',
icon: 'ant-design:file-protect-outlined',
auth: ['mes:wm-sales-notice:create'],
- ifShow: () => row.status === 20 && row.canCreateSalesNotice,
+ ifShow: () => row.status === 20 && !!row.canCreateSalesNotice,
onClick: handleGenerateNotice.bind(null, row),
},
{
@@ -369,7 +416,7 @@
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['erp:sale-order:delete'],
- ifShow: () => row.status === 10,
+ ifShow: () => row.status === 0 || row.status === 35,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.no]),
confirm: handleDelete.bind(null, [row.id!]),
diff --git a/src/views/erp/sale/order/modules/import-form.vue b/src/views/erp/sale/order/modules/import-form.vue
new file mode 100644
index 0000000..09e4167
--- /dev/null
+++ b/src/views/erp/sale/order/modules/import-form.vue
@@ -0,0 +1,155 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/erp/sale/order';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import { importSaleOrder, importSaleOrderTemplate } from '#/api/erp/sale/order';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importSaleOrder(fileList.value[0] as File);
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importSaleOrderTemplate();
+ downloadFileFromBlobPart({ fileName: '閿�鍞鍗曞鍏ユā鏉�.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureList ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆閿�鍞鍗�" class="w-[720px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+ <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑鈥滃鎴峰悕绉扳�濇寜鍚嶇О濉啓锛岄渶涓� CRM 瀹㈡埛涓�鑷达紱鈥滀骇鍝佺紪鐮佲�濇寜鐗╂枡缂栫爜濉啓锛堚�滀骇鍝佸悕绉扳�濆垪浠呬緵瀵圭収锛屽尮閰嶄互浜у搧缂栫爜涓哄噯锛夛紱鈥滀笅鍗曟椂闂粹�濇牸寮忓 2026-01-01 10:00:00锛岀暀绌哄彇褰撳墠鏃堕棿锛涒�滃崟浠封�濈暀绌哄彇鐗╂枡閿�鍞环銆�</p>
+ <p>涓�寮犻攢鍞崟鍙惈澶氭潯鏄庣粏锛氱浉鍚屸�滈攢鍞崟缂栧彿鈥濈殑澶氳浼氬悎骞朵负涓�寮犺鍗曪紙涓昏〃瀛楁鍙栬缁勯琛岋級锛涒�滈攢鍞崟缂栧彿鈥濈暀绌烘椂姣忎竴琛屽悇鐢熸垚涓�寮犳柊璁㈠崟銆傞攢鍞崟缂栧彿宸插瓨鍦ㄦ椂浼氬鍏ュけ璐ャ��</p>
+ <p>瀵煎叆鍚庣殑璁㈠崟涓衡�滄湭鎻愪氦鈥濈姸鎬侊紝鍙湪鍒楄〃涓户缁紪杈戞垨鎻愪氦瀹℃壒锛涘鍏ヤ笉浼氳嚜鍔ㄦ彁浜ゅ鎵规祦绋嬨��</p>
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆閿�鍞鍗曟暟鎹紝璇风◢鍊�...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex gap-4">
+ <span class="text-green-600">鏂板锛歿{ createCount }} 寮�</span>
+ <span class="text-blue-600">瑕嗙洊鏇存柊锛歿{ updateCount }} 寮�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 寮�</span>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureList"
+ :key="name"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/erp/sale/order/modules/item-form.vue b/src/views/erp/sale/order/modules/item-form.vue
index ffd3207..eea4f3c 100644
--- a/src/views/erp/sale/order/modules/item-form.vue
+++ b/src/views/erp/sale/order/modules/item-form.vue
@@ -63,6 +63,10 @@
minHeight: 250,
autoResize: true,
border: true,
+ // 鏄庣粏鍒楄緝澶氾紝鍚敤妯悜婊氬姩锛岄伩鍏嶅垪琚帇缂╁鑷存樉绀轰笉瀹屾暣锛堝崗鍚屽姙鍏鎵硅鎯呯瓑绐勫鍣ㄥ満鏅級
+ scroll: {
+ x: 'auto',
+ },
rowConfig: {
keyField: 'seq',
isHover: true,
diff --git a/src/views/erp/sale/order/modules/print.vue b/src/views/erp/sale/order/modules/print.vue
new file mode 100644
index 0000000..12e3cc5
--- /dev/null
+++ b/src/views/erp/sale/order/modules/print.vue
@@ -0,0 +1,459 @@
+<script lang="ts" setup>
+import type { ErpSaleOrderApi } from '#/api/erp/sale/order';
+
+import { computed, nextTick, ref } from 'vue';
+
+import { Barcode, BarcodeFormatEnum } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+import { useUserStore } from '@vben/stores';
+import { erpPriceMultiply, formatDateTime } from '@vben/utils';
+
+import { getCustomerSimpleList } from '#/api/crm/customer';
+import { getAccountSimpleList } from '#/api/erp/finance/account';
+import { getSaleOrder } from '#/api/erp/sale/order';
+import { getSimpleUserList } from '#/api/system/user';
+
+/**
+ * 閿�鍞鍗曟墦鍗扮粍浠�
+ *
+ * 鐢ㄤ簬瀵煎嚭/鎵撳嵃閿�鍞鍗曡缁嗕俊鎭紝渚涗紒涓氱洊绔犵暀瀛樸��
+ * 鎵撳嵃鏍峰紡鍙傝�� WMS 鍑哄簱鍗曟墦鍗帮紙wms/order/shipment/modules/print.vue锛夈��
+ */
+defineOptions({ name: 'ErpSaleOrderPrint' });
+
+const userStore = useUserStore();
+
+const printData = ref<Partial<ErpSaleOrderApi.SaleOrder>>({});
+/** 閿�鍞汉鍛樻樀绉版槧灏� */
+const saleUserMap = ref<Map<number, string>>(new Map());
+/** 缁撶畻璐︽埛鍚嶇О鏄犲皠 */
+const accountMap = ref<Map<number, string>>(new Map());
+/** 瀹㈡埛鍚嶇О鏄犲皠锛堣鎯呮帴鍙d笉杩斿洖瀹㈡埛鍚嶇О锛岄渶鎸夊鎴风紪鍙疯В鏋愶級 */
+const customerMap = ref<Map<number, string>>(new Map());
+/** 鎵撳嵃鏃堕棿 */
+const printTime = ref('');
+
+/** 璁㈠崟鐘舵�佹枃妗堬紙status 涓虹┖鏃朵笉璋冪敤 getDictLabel锛岄伩鍏嶅唴閮ㄥ undefined 鎵ц toString 鎶ラ敊锛� */
+const statusText = computed(() => {
+ if (printData.value.status == null) {
+ return '-';
+ }
+ const label = getDictLabel(DICT_TYPE.ERP_AUDIT_STATUS, printData.value.status);
+ return label || '-';
+});
+
+/** 鍑哄簱鐘舵�佹枃妗� */
+const outStatusText = computed(() => {
+ switch (printData.value.outStatus) {
+ case 0:
+ return '鏈嚭搴�';
+ case 1:
+ return '宸查鐣�';
+ case 2:
+ return '閮ㄥ垎鍑哄簱';
+ case 3:
+ return '鍏ㄩ儴鍑哄簱';
+ default:
+ return '-';
+ }
+});
+
+/** 鐢熶骇鐘舵�佹枃妗� */
+const productionStatusText = computed(() => {
+ if (printData.value.needProduction !== 1) {
+ return '鏃犻渶鐢熶骇';
+ }
+ return printData.value.productionStatus === 1 ? '宸插畬鎴�' : '鏈畬鎴�';
+});
+
+/** 閿�鍞汉鍛樺悕绉� */
+const saleUserName = computed(
+ () => saleUserMap.value.get(printData.value.saleUserId!) || '-',
+);
+
+/** 缁撶畻璐︽埛鍚嶇О */
+const accountName = computed(
+ () => accountMap.value.get(printData.value.accountId!) || '-',
+);
+
+/** 瀹㈡埛鍚嶇О锛氫紭鍏堝彇璇︽儏杩斿洖鐨勫鎴峰悕绉帮紝鍚﹀垯鎸夊鎴风紪鍙疯В鏋� */
+const customerName = computed(
+ () =>
+ printData.value.customerName ||
+ customerMap.value.get(printData.value.customerId!) ||
+ '-',
+);
+
+/** 鍒涘缓浜哄悕绉帮細璇︽儏鎺ュ彛涓嶈繑鍥炲垱寤轰汉鍚嶇О锛屾寜鍒涘缓浜虹紪鍙疯В鏋� */
+const creatorName = computed(
+ () =>
+ printData.value.creatorName ||
+ saleUserMap.value.get(Number(printData.value.creator)) ||
+ '-',
+);
+
+/** 鍗曚环 脳 鏁伴噺锛堜换涓�涓虹┖杩斿洖 undefined锛� */
+function multiplyPrice(price?: number, count?: number) {
+ return price == null || count == null ? undefined : erpPriceMultiply(price, count);
+}
+
+/**
+ * 鏄庣粏琛岋紙鍚庣涓嶈繑鍥炴槑缁嗚鐨� totalProductPrice/totalPrice锛岄渶鎸夊崟浠访楁暟閲忚绠楋細
+ * 閲戦 = 鍗曚环 脳 鏁伴噺锛涚◣棰� = 閲戦 脳 绋庣巼锛涘惈绋庨噾棰� = 閲戦 + 绋庨锛�
+ */
+const printRows = computed(() =>
+ (printData.value.items || []).map((item) => {
+ const totalProductPrice =
+ item.totalProductPrice ?? multiplyPrice(item.productPrice, item.count) ?? 0;
+ const taxPrice =
+ item.taxPrice ?? multiplyPrice(totalProductPrice, (item.taxPercent ?? 0) / 100) ?? 0;
+ const totalPrice = item.totalPrice ?? totalProductPrice + taxPrice;
+ return { ...item, totalProductPrice, taxPrice, totalPrice };
+ }),
+);
+
+/** 鏄庣粏鍚堣锛氭暟閲�/閲戦浼樺厛鍙栬鍗曞ご锛堝悗绔潈濞佸�硷級锛岀◣棰濅笌鍚◣閲戦鎸夋槑缁嗙疮鍔狅紙璁㈠崟澶� totalPrice 涓轰紭鎯犲悗閲戦锛屽彟鍦ㄩ噾棰濅俊鎭尯灞曠ず锛� */
+const summaries = computed(() => {
+ const rows = printRows.value;
+ const count = rows.reduce((sum, item) => sum + (item.count || 0), 0);
+ const totalProductPrice = rows.reduce(
+ (sum, item) => sum + (item.totalProductPrice || 0),
+ 0,
+ );
+ const taxPrice = rows.reduce((sum, item) => sum + (item.taxPrice || 0), 0);
+ const totalPrice = rows.reduce((sum, item) => sum + (item.totalPrice || 0), 0);
+ return {
+ count: printData.value.totalCount ?? count,
+ totalProductPrice: printData.value.totalProductPrice ?? totalProductPrice,
+ taxPrice,
+ totalPrice,
+ };
+});
+
+/** 淇濈暀涓や綅灏忔暟锛堥噾棰濆睍绀猴級 */
+function formatPrice(value?: number | null) {
+ return value === undefined || value === null || Number.isNaN(Number(value))
+ ? '-'
+ : Number(value).toFixed(2);
+}
+
+/** 淇濈暀涓変綅灏忔暟锛堟暟閲忓睍绀猴級 */
+function formatCount(value?: number | null) {
+ return value === undefined || value === null || Number.isNaN(Number(value))
+ ? '-'
+ : Number(value).toFixed(3);
+}
+
+/** 绛夊緟鏉$爜鍜屾墦鍗� DOM 瀹屾垚缁樺埗锛岄伩鍏嶆祻瑙堝櫒鎵撳嵃鍒版棫鍐呭 */
+function waitForPaint() {
+ return new Promise<void>((resolve) => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => resolve());
+ });
+ });
+}
+
+/** 閫�鍑烘墦鍗版ā寮忥紝鎭㈠褰撳墠椤甸潰鏄剧ず */
+function removePrintMode() {
+ document.body.classList.remove('erp-sale-order-printing');
+}
+
+/** 鎵撳嵃閿�鍞鍗曪細鍔犺浇鏁版嵁鍚庡彧灞曠ず鎵撳嵃鍖哄煙锛屽啀璋冪敤娴忚鍣ㄦ墦鍗� */
+async function print(id: number) {
+ const [order, userList, accountList, customerList] = await Promise.all([
+ getSaleOrder(id),
+ getSimpleUserList(),
+ getAccountSimpleList(),
+ getCustomerSimpleList(),
+ ]);
+ printData.value = order;
+ saleUserMap.value = new Map(
+ userList.map((item) => [Number(item.id), item.nickname || item.username]),
+ );
+ accountMap.value = new Map(
+ accountList.map((item) => [Number(item.id), item.name]),
+ );
+ customerMap.value = new Map(
+ customerList.map((item) => [Number(item.id), item.name]),
+ );
+ printTime.value = formatDateTime(new Date());
+ await nextTick();
+ await waitForPaint();
+ document.body.classList.add('erp-sale-order-printing');
+ window.addEventListener('afterprint', removePrintMode, { once: true });
+ window.print();
+}
+
+defineExpose({ print });
+</script>
+
+<template>
+ <Teleport to="body">
+ <div
+ id="erpSaleOrderPrint"
+ class="erp-sale-order-print pointer-events-none fixed left-0 top-0 z-[-1] w-full bg-white text-[#303133] opacity-0"
+ >
+ <div class="relative mb-2">
+ <h2 class="m-0 text-center text-[1.6em] font-bold leading-[1.2]">
+ 閿�鍞鍗�
+ </h2>
+ <div v-if="printData.no" class="absolute right-0 top-0">
+ <Barcode
+ :content="printData.no"
+ :display-value="false"
+ :format="BarcodeFormatEnum.CODE39"
+ :height="40"
+ :width="180"
+ />
+ </div>
+ </div>
+
+ <!-- 鍩烘湰淇℃伅 -->
+ <div
+ class="mb-3 grid grid-cols-3 gap-x-6 gap-y-2 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]"
+ >
+ <div>璁㈠崟鍗曞彿锛歿{ printData.no || '-' }}</div>
+ <div>
+ 璁㈠崟鏃堕棿锛歿{
+ formatDateTime(printData.orderTime) || '-'
+ }}
+ </div>
+ <div>瀹㈡埛锛歿{ customerName }}</div>
+ <div>閿�鍞汉鍛橈細{{ saleUserName }}</div>
+ <div>鍏宠仈鍚堝悓锛歿{ printData.contractNo || '-' }}</div>
+ <div>缁撶畻璐︽埛锛歿{ accountName }}</div>
+ <div>璁㈠崟鐘舵�侊細{{ statusText }}</div>
+ <div>鍑哄簱鐘舵�侊細{{ outStatusText }}</div>
+ <div>鐢熶骇鐘舵�侊細{{ productionStatusText }}</div>
+ <div>鍒涘缓浜猴細{{ creatorName }}</div>
+ <div>
+ 鍒涘缓鏃堕棿锛歿{
+ formatDateTime(printData.createTime) || '-'
+ }}
+ </div>
+ <div class="col-span-3">澶囨敞锛歿{ printData.remark || '-' }}</div>
+ </div>
+
+ <!-- 浜у搧鏄庣粏 -->
+ <table class="w-full border-collapse text-[13px] leading-[1.5]">
+ <thead>
+ <tr>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center font-bold"
+ >
+ 搴忓彿
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
+ >
+ 浜у搧鍚嶇О鍙婅鏍�
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center font-bold"
+ >
+ 鍗曚綅
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
+ >
+ 鏁伴噺
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
+ >
+ 浜у搧鍗曚环(鍏�)
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
+ >
+ 閲戦(鍏�)
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
+ >
+ 绋庣巼(%)
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
+ >
+ 绋庨(鍏�)
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right font-bold"
+ >
+ 鍚◣閲戦(鍏�)
+ </th>
+ <th
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-left font-bold"
+ >
+ 澶囨敞
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(item, index) in printRows" :key="item.id || index">
+ <td class="border border-solid border-[#dcdfe6] p-2 text-center">
+ {{ index + 1 }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2">
+ <div>{{ item.productName || '-' }}</div>
+ <div v-if="item.productSpecification" class="text-xs">
+ {{ item.productSpecification }}
+ </div>
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2 text-center">
+ {{ item.productUnitName || '-' }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2 text-right">
+ {{ formatCount(item.count) }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2 text-right">
+ {{ formatPrice(item.productPrice) }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2 text-right">
+ {{ formatPrice(item.totalProductPrice) }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2 text-right">
+ {{ formatPrice(item.taxPercent) }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2 text-right">
+ {{ formatPrice(item.taxPrice) }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2 text-right">
+ {{ formatPrice(item.totalPrice) }}
+ </td>
+ <td class="border border-solid border-[#dcdfe6] p-2">
+ {{ item.remark || '-' }}
+ </td>
+ </tr>
+ <tr v-if="printData.items && printData.items.length > 0">
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-center"
+ >
+ 鍚堣
+ </td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
+ ></td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
+ ></td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
+ >
+ {{ formatCount(summaries.count) }}
+ </td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
+ ></td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
+ >
+ {{ formatPrice(summaries.totalProductPrice) }}
+ </td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
+ ></td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
+ >
+ {{ formatPrice(summaries.taxPrice) }}
+ </td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2 text-right"
+ >
+ {{ formatPrice(summaries.totalPrice) }}
+ </td>
+ <td
+ class="border border-solid border-[#dcdfe6] bg-[#f5f7fa] p-2"
+ ></td>
+ </tr>
+ <tr v-if="printRows.length === 0">
+ <td
+ class="border border-solid border-[#dcdfe6] p-2 text-center"
+ colspan="10"
+ >
+ 鏆傛棤鏄庣粏
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!-- 閲戦淇℃伅 -->
+ <div
+ class="mt-3 grid grid-cols-4 gap-x-6 gap-y-2 border border-solid border-[#dcdfe6] p-3 text-sm leading-[1.6]"
+ >
+ <div>浼樻儬鐜�(%)锛歿{ formatPrice(printData.discountPercent) }}</div>
+ <div>浠樻浼樻儬(鍏�)锛歿{ formatPrice(printData.discountPrice) }}</div>
+ <div>浼樻儬鍚庨噾棰�(鍏�)锛歿{ formatPrice(printData.totalPrice) }}</div>
+ <div>鏀跺彇璁㈤噾(鍏�)锛歿{ formatPrice(printData.depositPrice) }}</div>
+ </div>
+
+ <!-- 鐩栫珷鏍� -->
+ <div
+ class="mt-6 grid grid-cols-2 gap-x-10 gap-y-4 border border-dashed border-[#dcdfe6] p-4 text-sm leading-[1.8]"
+ >
+ <div>
+ 瀹㈡埛鐩栫珷/绛惧瓧锛�<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
+ </div>
+ <div>
+ 鍏徃鐩栫珷/绛惧瓧锛�<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
+ </div>
+ <div>
+ 鏃ユ湡锛�<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
+ </div>
+ <div>
+ 鏃ユ湡锛�<span class="inline-block w-36 border-b border-solid border-[#303133]"></span>
+ </div>
+ </div>
+
+ <!-- 鎵撳嵃淇℃伅 -->
+ <div
+ class="mt-4 flex justify-between text-xs text-[#909399]"
+ >
+ <div>
+ 鎵撳嵃浜哄憳锛歿{ userStore.userInfo?.nickname || '-' }}
+ </div>
+ <div>鎵撳嵃鏃堕棿锛歿{ printTime || '-' }}</div>
+ </div>
+ </div>
+ </Teleport>
+</template>
+
+<style scoped>
+@page {
+ margin: 8mm 10mm;
+}
+
+@media print {
+ :global(body.erp-sale-order-printing) {
+ padding: 0 !important;
+ margin: 0 !important;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ }
+
+ :global(body.erp-sale-order-printing *) {
+ visibility: hidden !important;
+ }
+
+ :global(body.erp-sale-order-printing .erp-sale-order-print),
+ :global(body.erp-sale-order-printing .erp-sale-order-print *) {
+ visibility: visible !important;
+ }
+
+ :global(body.erp-sale-order-printing .erp-sale-order-print) {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: auto;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0 !important;
+ margin: 0 !important;
+ pointer-events: auto;
+ opacity: 1;
+ }
+}
+</style>
diff --git a/src/views/hrm/attendance/exception/detail/approve/index.vue b/src/views/hrm/attendance/exception/detail/approve/index.vue
new file mode 100644
index 0000000..e0fef3a
--- /dev/null
+++ b/src/views/hrm/attendance/exception/detail/approve/index.vue
@@ -0,0 +1,82 @@
+<script lang="ts" setup>
+import type { HrmAttendanceExceptionApi } from '#/api/hrm/attendance/exception';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { getAttendanceException } from '#/api/hrm/attendance/exception';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'HrmAttendanceExceptionApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<HrmAttendanceExceptionApi.AttendanceException>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鍛樺伐濮撳悕', field: 'userName', render: textRender },
+ { label: '鎵�灞為儴闂�', field: 'deptName', render: textRender },
+ {
+ label: '寮傚父鏃ユ湡',
+ field: 'date',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '寮傚父绫诲瀷', field: 'exceptionTypeName', render: textRender },
+ { label: '寮傚父璇存槑', field: 'reason', render: textRender },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getAttendanceException(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 鑰冨嫟寮傚父鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鑰冨嫟寮傚父鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/hrm/attendance/record/data.ts b/src/views/hrm/attendance/record/data.ts
index 933798f..98581f2 100644
--- a/src/views/hrm/attendance/record/data.ts
+++ b/src/views/hrm/attendance/record/data.ts
@@ -75,6 +75,7 @@
return [
{ field: 'userName', title: '鍛樺伐濮撳悕', minWidth: 100 },
{ field: 'deptName', title: '閮ㄩ棬', minWidth: 120 },
+ { field: 'phone', title: '鎵嬫満鍙�', width: 130 },
{ field: 'date', title: '鑰冨嫟鏃ユ湡', width: 110 },
{ field: 'clockInTime', title: '涓婄彮鎵撳崱鏃堕棿', width: 160 },
{
diff --git a/src/views/hrm/attendance/record/modules/import-form.vue b/src/views/hrm/attendance/record/modules/import-form.vue
index 486e53c..ccc0e50 100644
--- a/src/views/hrm/attendance/record/modules/import-form.vue
+++ b/src/views/hrm/attendance/record/modules/import-form.vue
@@ -23,10 +23,18 @@
}
modalApi.lock();
try {
- await importAttendanceRecord(fileList.value[0] as File, updateSupport.value);
+ const res: string = await importAttendanceRecord(
+ fileList.value[0] as File,
+ updateSupport.value,
+ );
await modalApi.close();
emit('success');
- message.success('瀵煎叆鎴愬姛');
+ // 鍚庣杩斿洖瀵煎叆缁撴灉锛堝惈澶辫触鍘熷洜锛屼緥濡傗�滀互涓嬪憳宸ヤ笉瀛樺湪...锛涢渶琛ュ厖鎵嬫満鍙�...鈥濓級
+ if (res && res.length > 0) {
+ message[res.includes('鏈�') || res.includes('涓嶅畬鏁�') ? 'warning' : 'success'](res);
+ } else {
+ message.success('瀵煎叆鎴愬姛');
+ }
} finally {
modalApi.unlock();
}
diff --git a/src/views/hrm/employee/components/select.vue b/src/views/hrm/employee/components/select.vue
index bde15c3..64b8f9f 100644
--- a/src/views/hrm/employee/components/select.vue
+++ b/src/views/hrm/employee/components/select.vue
@@ -118,7 +118,7 @@
<div>鍛樺伐缂栧彿锛歿{ selectedItem.employeeNo || '-' }}</div>
<div>鍛樺伐濮撳悕锛歿{ selectedItem.name || '-' }}</div>
<div v-if="selectedItem.deptName">閮ㄩ棬锛歿{ selectedItem.deptName }}</div>
- <div v-if="selectedItem.postName">宀椾綅锛歿{ selectedItem.postName }}</div>
+ <div v-if="selectedItem.postNames">宀椾綅锛歿{ selectedItem.postNames }}</div>
</div>
</template>
<Input
diff --git a/src/views/hrm/employee/contract/data.ts b/src/views/hrm/employee/contract/data.ts
new file mode 100644
index 0000000..a978d4c
--- /dev/null
+++ b/src/views/hrm/employee/contract/data.ts
@@ -0,0 +1,297 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+import { getEmployeeSimpleList } from '#/api/hrm/employee';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'update';
+
+/** 鏂板/淇敼鍛樺伐鍚堝悓鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'employeeId',
+ label: '鍛樺伐',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getEmployeeSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ showSearch: true,
+ filterOption: (input: string, option: any) =>
+ option.label?.toLowerCase().includes(input.toLowerCase()),
+ placeholder: '璇烽�夋嫨鍛樺伐',
+ },
+ rules: 'selectRequired',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'contractType',
+ label: '鍚堝悓绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍚堝悓绫诲瀷',
+ options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TYPE, 'number'),
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'contractTermType',
+ label: '鏈熼檺绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鏈熼檺绫诲瀷',
+ options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, 'number'),
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'signCompany',
+ label: '绛剧害涓讳綋',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ绾︿富浣擄紙鍏徃鍏ㄧО锛�',
+ },
+ },
+ {
+ fieldName: 'signDate',
+ label: '绛捐鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨绛捐鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'startDate',
+ label: '寮�濮嬫棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨寮�濮嬫棩鏈�',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endDate',
+ label: '缁撴潫鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '鏃犲浐瀹氭湡闄愬彲涓嶅~',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'probationStartDate',
+ label: '璇曠敤鏈熷紑濮�',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨璇曠敤鏈熷紑濮嬫棩鏈�',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'probationEndDate',
+ label: '璇曠敤鏈熺粨鏉�',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨璇曠敤鏈熺粨鏉熸棩鏈�',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'probationSalary',
+ label: '璇曠敤鏈熷伐璧�',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘯鐢ㄦ湡宸ヨ祫',
+ min: 0,
+ precision: 2,
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'regularSalary',
+ label: '杞宸ヨ祫',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ浆姝e伐璧�',
+ min: 0,
+ precision: 2,
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'blobIds',
+ label: '闄勪欢',
+ component: 'FileUpload',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'contractNo',
+ label: '鍚堝悓缂栧彿',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ悎鍚岀紪鍙�',
+ },
+ },
+ {
+ fieldName: 'employeeId',
+ label: '鍛樺伐',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getEmployeeSimpleList,
+ labelField: 'name',
+ valueField: 'id',
+ showSearch: true,
+ filterOption: (input: string, option: any) =>
+ option.label?.toLowerCase().includes(input.toLowerCase()),
+ allowClear: true,
+ placeholder: '璇烽�夋嫨鍛樺伐',
+ },
+ },
+ {
+ fieldName: 'contractType',
+ label: '鍚堝悓绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍚堝悓绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'contractTermType',
+ label: '鏈熼檺绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鏈熼檺绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'terminateStatus',
+ label: '瑙i櫎缁堟鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_CONTRACT_TERMINATE_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨瑙i櫎缁堟鐘舵��',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鍚堝悓鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_CONTRACT_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鍚堝悓鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<HrmEmployeeContractApi.EmployeeContract>['columns'] {
+ return [
+ { field: 'contractNo', title: '鍚堝悓缂栧彿', minWidth: 160 },
+ {
+ field: 'parentNo',
+ title: '缁鑷�',
+ minWidth: 140,
+ slots: { default: 'parentNo' },
+ },
+ { field: 'employeeName', title: '鍛樺伐濮撳悕', minWidth: 100 },
+ { field: 'employeeNo', title: '鍛樺伐宸ュ彿', minWidth: 130 },
+ { field: 'deptName', title: '閮ㄩ棬', minWidth: 120 },
+ {
+ field: 'contractType',
+ title: '鍚堝悓绫诲瀷',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_CONTRACT_TYPE },
+ },
+ },
+ {
+ field: 'contractTermType',
+ title: '鏈熼檺绫诲瀷',
+ width: 130,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_CONTRACT_TERM_TYPE },
+ },
+ },
+ { field: 'signDate', title: '绛捐鏃ユ湡', width: 110 },
+ { field: 'startDate', title: '寮�濮嬫棩鏈�', width: 110 },
+ { field: 'endDate', title: '缁撴潫鏃ユ湡', width: 110 },
+ {
+ field: 'status',
+ title: '鍚堝悓鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_CONTRACT_STATUS },
+ },
+ },
+ {
+ field: 'isCurrent',
+ title: '褰撳墠鍚堝悓',
+ width: 90,
+ slots: { default: 'isCurrent' },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 260,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/hrm/employee/contract/index.vue b/src/views/hrm/employee/contract/index.vue
new file mode 100644
index 0000000..02aa33c
--- /dev/null
+++ b/src/views/hrm/employee/contract/index.vue
@@ -0,0 +1,183 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteEmployeeContract,
+ getEmployeeContractPage,
+} from '#/api/hrm/employee/contract';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
+import Terminate from './modules/terminate.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
+ destroyOnClose: true,
+});
+
+const [TerminateModal, terminateModalApi] = useVbenModal({
+ connectedComponent: Terminate,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鏂板鍚堝悓 */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 瀵煎叆鍚堝悓 */
+function handleImport() {
+ importModalApi.open();
+}
+
+/** 缂栬緫鍚堝悓 */
+function handleEdit(row: HrmEmployeeContractApi.EmployeeContract) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 缁鍚堝悓锛氬甫鍏ヤ笂浠藉悎鍚屽熀纭�淇℃伅锛屽垱寤烘柊鍚堝悓锛堝悗绔嚜鍔ㄥ叧鑱� parentId锛� */
+function handleRenew(row: HrmEmployeeContractApi.EmployeeContract) {
+ formModalApi.setData({ formType: 'create', renewFrom: row }).open();
+}
+
+/** 瑙i櫎/缁堟鍚堝悓 */
+function handleTerminate(row: HrmEmployeeContractApi.EmployeeContract) {
+ terminateModalApi.setData({ id: row.id }).open();
+}
+
+/** 鍒犻櫎鍚堝悓 */
+async function handleDelete(row: HrmEmployeeContractApi.EmployeeContract) {
+ await deleteEmployeeContract(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', ['鍛樺伐鍚堝悓']));
+ handleRefresh();
+}
+
+/** 鏄惁鍙В闄�/缁堟锛氫粎鏈В闄�/鏈粓姝㈢殑鍚堝悓鍙搷浣� */
+function isTerminable(row: HrmEmployeeContractApi.EmployeeContract) {
+ return row.terminateStatus === 0;
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getEmployeeContractPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<HrmEmployeeContractApi.EmployeeContract>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
+ <TerminateModal @success="handleRefresh" />
+ <Grid table-title="鍛樺伐鍚堝悓鍒楄〃">
+ <template #parentNo="{ row }">
+ <span>{{ row.parentNo || '-' }}</span>
+ </template>
+ <template #isCurrent="{ row }">
+ <Tag v-if="row.isCurrent" color="processing">褰撳墠</Tag>
+ <span v-else>-</span>
+ </template>
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鍛樺伐鍚堝悓']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:employee-contract:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['hrm:employee-contract:import'],
+ onClick: handleImport,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '缁',
+ type: 'link',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:employee-contract:create'],
+ ifShow: isTerminable(row),
+ onClick: handleRenew.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['hrm:employee-contract:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '瑙i櫎/缁堟',
+ type: 'link',
+ icon: ACTION_ICON.CLOSE,
+ auth: ['hrm:employee-contract:update'],
+ ifShow: isTerminable(row),
+ onClick: handleTerminate.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:employee-contract:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', ['鍛樺伐鍚堝悓']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/hrm/employee/contract/modules/form.vue b/src/views/hrm/employee/contract/modules/form.vue
new file mode 100644
index 0000000..d48678e
--- /dev/null
+++ b/src/views/hrm/employee/contract/modules/form.vue
@@ -0,0 +1,126 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createEmployeeContract,
+ getEmployeeContract,
+ updateEmployeeContract,
+} from '#/api/hrm/employee/contract';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const isRenew = ref(false);
+
+const getTitle = computed(() => {
+ if (formType.value === 'update') {
+ return '淇敼鍛樺伐鍚堝悓';
+ }
+ return isRenew.value ? '缁鍛樺伐鍚堝悓' : '鏂板鍛樺伐鍚堝悓';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data =
+ (await formApi.getValues()) as HrmEmployeeContractApi.EmployeeContract;
+ try {
+ // FileUpload 缁勪欢鐨勫�奸渶杞崲涓� blobId 鏁扮粍鎻愪氦
+ data.blobIds = (data.blobIds ?? []).map((item: any) => item.id ?? item);
+ if (data.id) {
+ await updateEmployeeContract(data);
+ } else {
+ await createEmployeeContract(data);
+ }
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{
+ formType: FormType;
+ id?: number;
+ renewFrom?: HrmEmployeeContractApi.EmployeeContract;
+ }>();
+ formType.value = data.formType;
+ isRenew.value = !data?.id && !!data?.renewFrom;
+ // 缁锛氬甫鍏ヤ笂浠藉悎鍚岀殑鍩虹淇℃伅锛堝憳宸ャ�佺被鍨嬨�佺绾︿富浣撱�佸伐璧勩�佺画绛炬潵婧愶級锛屾棩鏈熼渶閲嶆柊濉啓
+ if (data?.renewFrom) {
+ await formApi.resetForm();
+ await formApi.setValues({
+ parentId: data.renewFrom.id,
+ employeeId: data.renewFrom.employeeId,
+ contractType: data.renewFrom.contractType,
+ contractTermType: data.renewFrom.contractTermType,
+ signCompany: data.renewFrom.signCompany,
+ probationSalary: data.renewFrom.probationSalary,
+ regularSalary: data.renewFrom.regularSalary,
+ });
+ return;
+ }
+ if (!data?.id) {
+ await formApi.resetForm();
+ return;
+ }
+ modalApi.lock();
+ try {
+ const detail = await getEmployeeContract(data.id);
+ await formApi.setValues(detail);
+ if (detail.attachmentList?.length) {
+ await formApi.setFieldValue(
+ 'blobIds',
+ detail.attachmentList.map((item) => ({
+ uid: String(item.id),
+ name: item.name || '',
+ url: item.url || '',
+ status: 'done',
+ id: item.id,
+ })),
+ );
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-[720px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/hrm/employee/contract/modules/import-form.vue b/src/views/hrm/employee/contract/modules/import-form.vue
new file mode 100644
index 0000000..78987ea
--- /dev/null
+++ b/src/views/hrm/employee/contract/modules/import-form.vue
@@ -0,0 +1,165 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Switch, Upload } from 'ant-design-vue';
+
+import {
+ importEmployeeContract,
+ importEmployeeContractTemplate,
+} from '#/api/hrm/employee/contract';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const updateSupport = ref(false);
+const importResult = ref<HrmEmployeeContractApi.ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importEmployeeContract(
+ fileList.value[0] as File,
+ updateSupport.value,
+ );
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝瑕嗙洊 ${updateCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importEmployeeContractTemplate();
+ downloadFileFromBlobPart({ fileName: '鍛樺伐鍚堝悓瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createNames?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateNames?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureNames ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆鍛樺伐鍚堝悓" class="w-[720px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div class="mt-4">
+ <span class="mr-2">鍚屽憳宸ャ�佸悓鍚堝悓绫诲瀷銆佸悓寮�濮嬫棩鏈熺殑鍚堝悓宸插瓨鍦ㄦ椂瑕嗙洊鏇存柊璇ヨ锛�</span>
+ <Switch v-model:checked="updateSupport" />
+ </div>
+ <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+ <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑鈥滃憳宸ュ鍚�/鍚堝悓绫诲瀷/鍚堝悓鏈熼檺绫诲瀷鈥濇寜鍚嶇О鎴栦腑鏂囧~鍐欙紙鍚堝悓绫诲瀷濡傦細鍔冲姩鍚堝悓銆佸姵鍔″悎鍚岋級銆�</p>
+ <p>瀵煎叆鍒ゅ畾锛氭寜鈥滃憳宸ュ鍚� + 鍚堝悓绫诲瀷 + 鍚堝悓寮�濮嬫棩鏈熲�濊瘑鍒�傛湭鍕鹃�夆�滃凡瀛樺湪鏃舵洿鏂扳�濇椂锛屽懡涓凡瀛樺湪鍚堝悓鐨勮浼氬鍏ュけ璐ュ苟鎻愮ず锛涘嬀閫夊悗鍒欒鐩栨洿鏂拌琛岋紙淇濈暀鍘熷悎鍚岀紪鍙凤級锛屾湭鍛戒腑鐨勮涓�寰嬫柊澧炪��</p>
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆鍛樺伐鍚堝悓鏁版嵁锛岃绋嶅��...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex gap-4">
+ <span class="text-green-600">鏂板锛歿{ createCount }} 浠�</span>
+ <span class="text-blue-600">瑕嗙洊鏇存柊锛歿{ updateCount }} 浠�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 浠�</span>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureNames"
+ :key="name"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/hrm/employee/contract/modules/terminate.vue b/src/views/hrm/employee/contract/modules/terminate.vue
new file mode 100644
index 0000000..c554c0c
--- /dev/null
+++ b/src/views/hrm/employee/contract/modules/terminate.vue
@@ -0,0 +1,103 @@
+<script lang="ts" setup>
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { terminateEmployeeContract } from '#/api/hrm/employee/contract';
+import { $t } from '#/locales';
+
+const emit = defineEmits(['success']);
+const contractId = ref<number>();
+
+/** 瑙i櫎/缁堟琛ㄥ崟 */
+const schema: VbenFormSchema[] = [
+ {
+ fieldName: 'terminateStatus',
+ label: '鎿嶄綔绫诲瀷',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '瑙i櫎', value: 1 },
+ { label: '缁堟', value: 2 },
+ ],
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'terminateDate',
+ label: '瑙i櫎/缁堟鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'terminateReason',
+ label: '鍘熷洜',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨВ闄�/缁堟鍘熷洜',
+ rows: 3,
+ maxlength: 255,
+ showCount: true,
+ },
+ rules: 'required',
+ },
+];
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema,
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = await formApi.getValues();
+ try {
+ await terminateEmployeeContract({ id: contractId.value!, ...data });
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ contractId.value = data.id;
+ await formApi.resetForm();
+ },
+});
+</script>
+
+<template>
+ <Modal title="瑙i櫎/缁堟鍚堝悓" class="w-[520px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/hrm/employee/data.ts b/src/views/hrm/employee/data.ts
index b9e0233..7474ca7 100644
--- a/src/views/hrm/employee/data.ts
+++ b/src/views/hrm/employee/data.ts
@@ -10,6 +10,7 @@
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
+import { getSalaryStructureList } from '#/api/hrm/salary/structure';
import { getDeptList } from '#/api/system/dept';
import { getSimplePostList } from '#/api/system/post';
import { getSimpleRoleList } from '#/api/system/role';
@@ -94,14 +95,15 @@
rules: 'selectRequired',
},
{
- fieldName: 'postId',
+ fieldName: 'postIds',
label: '宀椾綅',
component: 'ApiSelect',
componentProps: {
api: getSimplePostList,
labelField: 'name',
valueField: 'id',
- placeholder: '璇烽�夋嫨宀椾綅',
+ mode: 'multiple',
+ placeholder: '璇烽�夋嫨宀椾綅锛堝彲澶氶�夛級',
},
rules: 'selectRequired',
},
@@ -177,6 +179,49 @@
},
},
{
+ fieldName: 'nation',
+ label: '姘戞棌',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ皯鏃�',
+ },
+ },
+ {
+ fieldName: 'marriageStatus',
+ label: '濠氬Щ鐘舵��',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ濮荤姸鎬�',
+ },
+ },
+ {
+ fieldName: 'age',
+ label: '骞撮緞',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ勾榫�',
+ min: 0,
+ max: 150,
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'education',
+ label: '瀛﹀巻',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ鍘�',
+ },
+ },
+ {
+ fieldName: 'politicalStatus',
+ label: '鏀挎不闈㈣矊',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ斂娌婚潰璨�',
+ },
+ },
+ {
fieldName: 'annualLeaveBalance',
label: '骞村亣浣欓',
component: 'InputNumber',
@@ -197,6 +242,19 @@
precision: 1,
style: { width: '100%' },
},
+ },
+ {
+ fieldName: 'salaryStructureId',
+ label: '钖叕缁撴瀯',
+ component: 'ApiSelect',
+ componentProps: {
+ api: () => getSalaryStructureList({ status: 0 }),
+ labelField: 'name',
+ valueField: 'id',
+ allowClear: true,
+ placeholder: '璇烽�夋嫨钖叕缁撴瀯',
+ },
+ help: '鍐冲畾宸ヤ綔鏃ュ姞鐝�嶇巼锛涙湭閫夋嫨鏃朵娇鐢ㄧ郴缁熼粯璁� 1.5 鍊�',
},
{
fieldName: 'baseSalary',
@@ -310,7 +368,10 @@
},
{ field: 'phone', title: '鎵嬫満鍙风爜', minWidth: 120 },
{ field: 'deptName', title: '閮ㄩ棬', minWidth: 120 },
- { field: 'postName', title: '宀椾綅', minWidth: 120 },
+ { field: 'postNames', title: '宀椾綅', minWidth: 120 },
+ { field: 'nation', title: '姘戞棌', width: 90 },
+ { field: 'age', title: '骞撮緞', width: 70 },
+ { field: 'education', title: '瀛﹀巻', width: 90 },
{
field: 'employeeStatus',
title: '鍛樺伐鐘舵��',
@@ -371,7 +432,7 @@
{ field: 'employeeNo', title: '鍛樺伐缂栧彿', minWidth: 120 },
{ field: 'name', title: '鍛樺伐濮撳悕', minWidth: 100 },
{ field: 'deptName', title: '閮ㄩ棬', minWidth: 120 },
- { field: 'postName', title: '宀椾綅', minWidth: 120 },
+ { field: 'postNames', title: '宀椾綅', minWidth: 120 },
{ field: 'phone', title: '鎵嬫満鍙风爜', minWidth: 120 },
];
}
diff --git a/src/views/hrm/employee/index.vue b/src/views/hrm/employee/index.vue
index 98a6237..9f5cb70 100644
--- a/src/views/hrm/employee/index.vue
+++ b/src/views/hrm/employee/index.vue
@@ -5,7 +5,9 @@
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
-import { Button, message, Tag } from 'ant-design-vue';
+import { Button, message, Progress, Tag } from 'ant-design-vue';
+
+import { ref } from 'vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
@@ -16,9 +18,15 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -57,10 +65,55 @@
}
}
+const exporting = ref(false);
+const exportPercent = ref(0);
+let exportTimer: ReturnType<typeof setInterval> | undefined;
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startExportProgress() {
+ exportPercent.value = 0;
+ exportTimer = setInterval(() => {
+ if (exportPercent.value < 90) {
+ exportPercent.value = Math.min(
+ 90,
+ exportPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopExportProgress() {
+ if (exportTimer) {
+ clearInterval(exportTimer);
+ exportTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
/** 瀵煎嚭鍛樺伐 */
async function handleExport() {
- const data = await exportEmployee(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '鍛樺伐鍒楄〃.xls', source: data });
+ if (exporting.value) return;
+ exporting.value = true;
+ startExportProgress();
+ try {
+ const data = await exportEmployee(await gridApi.formApi.getValues());
+ // 瀹屾垚锛氳繘搴︽潯璧版弧鍚庤Е鍙戜笅杞�
+ stopExportProgress();
+ exportPercent.value = 100;
+ await delay(500);
+ downloadFileFromBlobPart({ fileName: '鍛樺伐鍒楄〃.xls', source: data });
+ } finally {
+ stopExportProgress();
+ exporting.value = false;
+ }
+}
+
+/** 瀵煎叆鍛樺伐 */
+function handleImport() {
+ importModalApi.open();
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -95,6 +148,7 @@
<template>
<Page auto-content-height>
+ <ImportModal @success="handleRefresh" />
<FormModal @success="handleRefresh" />
<Grid table-title="鍛樺伐鍒楄〃">
<template #toolbar-tools>
@@ -106,6 +160,13 @@
icon: ACTION_ICON.ADD,
auth: ['hrm:employee:create'],
onClick: handleCreate,
+ },
+ {
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['hrm:employee:import'],
+ onClick: handleImport,
},
{
label: $t('ui.actionTitle.export'),
@@ -155,5 +216,18 @@
/>
</template>
</Grid>
+
+ <!-- 瀵煎嚭杩涘害鏉¢伄缃� -->
+ <div
+ v-if="exporting"
+ class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40"
+ >
+ <div class="w-80 rounded-lg bg-white p-6 shadow-lg">
+ <div class="mb-3 text-center text-sm text-gray-600">
+ 姝e湪瀵煎嚭鍛樺伐鍒楄〃...
+ </div>
+ <Progress :percent="exportPercent" status="active" />
+ </div>
+ </div>
</Page>
</template>
diff --git a/src/views/hrm/employee/modules/contract-list.vue b/src/views/hrm/employee/modules/contract-list.vue
new file mode 100644
index 0000000..8f95f77
--- /dev/null
+++ b/src/views/hrm/employee/modules/contract-list.vue
@@ -0,0 +1,107 @@
+<script lang="ts" setup>
+import type { HrmEmployeeContractApi } from '#/api/hrm/employee/contract';
+
+import { ref, watch } from 'vue';
+
+import { Table, Tag } from 'ant-design-vue';
+
+import { getEmployeeContractPage } from '#/api/hrm/employee/contract';
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictLabel } from '#/packages/effects/hooks/src';
+
+const props = defineProps<{
+ employeeId?: number;
+}>();
+
+const list = ref<HrmEmployeeContractApi.EmployeeContract[]>([]);
+const loading = ref(false);
+
+/** 鍚堝悓鐘舵�侀鑹诧細1-寰呯敓鏁� 2-鐢熸晥涓� 3-鍗冲皢鍒版湡 4-宸插埌鏈� 5-宸茶В闄� 6-宸茬粓姝� */
+const statusColorMap: Record<number, string> = {
+ 1: 'processing',
+ 2: 'success',
+ 3: 'warning',
+ 4: 'default',
+ 5: 'error',
+ 6: 'error',
+};
+
+/** 鍒锋柊鍒楄〃 */
+async function handleRefresh() {
+ if (!props.employeeId) return;
+ loading.value = true;
+ try {
+ const res = await getEmployeeContractPage({
+ pageNo: 1,
+ pageSize: 100,
+ employeeId: props.employeeId,
+ });
+ list.value = res.list;
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 鐩戝惉 employeeId 鍙樺寲 */
+watch(
+ () => props.employeeId,
+ (val) => {
+ if (val) {
+ handleRefresh();
+ }
+ },
+ { immediate: true },
+);
+
+const columns = [
+ { title: '鍚堝悓缂栧彿', dataIndex: 'contractNo', width: 150 },
+ { title: '缁鑷�', dataIndex: 'parentNo', width: 130, key: 'parentNo' },
+ { title: '鍚堝悓绫诲瀷', dataIndex: 'contractType', width: 100, key: 'contractType' },
+ { title: '鏈熼檺绫诲瀷', dataIndex: 'contractTermType', width: 110, key: 'contractTermType' },
+ { title: '绛捐鏃ユ湡', dataIndex: 'signDate', width: 110 },
+ { title: '寮�濮嬫棩鏈�', dataIndex: 'startDate', width: 110 },
+ { title: '缁撴潫鏃ユ湡', dataIndex: 'endDate', width: 110 },
+ { title: '鍚堝悓鐘舵��', dataIndex: 'status', width: 100, key: 'status' },
+ { title: '褰撳墠鍚堝悓', dataIndex: 'isCurrent', width: 90, key: 'isCurrent' },
+];
+</script>
+
+<template>
+ <div class="contract-list">
+ <Table
+ :columns="columns"
+ :data-source="list"
+ :loading="loading"
+ :pagination="false"
+ size="small"
+ row-key="id"
+ >
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.key === 'parentNo'">
+ {{ record.parentNo || '-' }}
+ </template>
+ <template v-else-if="column.key === 'contractType'">
+ {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_TYPE, record.contractType) }}
+ </template>
+ <template v-else-if="column.key === 'contractTermType'">
+ {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_TERM_TYPE, record.contractTermType) }}
+ </template>
+ <template v-else-if="column.key === 'status'">
+ <Tag :color="statusColorMap[record.status!] || 'default'">
+ {{ getDictLabel(DICT_TYPE.HRM_CONTRACT_STATUS, record.status) }}
+ </Tag>
+ </template>
+ <template v-else-if="column.key === 'isCurrent'">
+ <Tag v-if="record.isCurrent" color="processing">褰撳墠</Tag>
+ <span v-else>-</span>
+ </template>
+ </template>
+ </Table>
+ </div>
+</template>
+
+<style scoped>
+.contract-list {
+ padding: 0;
+}
+</style>
diff --git a/src/views/hrm/employee/modules/detail.vue b/src/views/hrm/employee/modules/detail.vue
index ca20c47..ccd7122 100644
--- a/src/views/hrm/employee/modules/detail.vue
+++ b/src/views/hrm/employee/modules/detail.vue
@@ -12,6 +12,7 @@
import WorkHistoryList from './work-history-list.vue';
import EmergencyContactList from './emergency-contact-list.vue';
import SocialSecurityList from './social-security-list.vue';
+import ContractList from './contract-list.vue';
const props = defineProps<{
employee: HrmEmployeeApi.Employee;
@@ -48,12 +49,17 @@
<DescriptionsItem label="鍛樺伐鐘舵��">
<Tag :color="employeeStatusColor">{{ employeeStatusLabel }}</Tag>
</DescriptionsItem>
+ <DescriptionsItem label="姘戞棌">{{ employee.nation || '-' }}</DescriptionsItem>
+ <DescriptionsItem label="濠氬Щ鐘舵��">{{ employee.marriageStatus || '-' }}</DescriptionsItem>
+ <DescriptionsItem label="骞撮緞">{{ employee.age ?? '-' }}</DescriptionsItem>
+ <DescriptionsItem label="瀛﹀巻">{{ employee.education || '-' }}</DescriptionsItem>
+ <DescriptionsItem label="鏀挎不闈㈣矊">{{ employee.politicalStatus || '-' }}</DescriptionsItem>
</Descriptions>
<!-- 宸ヤ綔淇℃伅 -->
<Descriptions title="宸ヤ綔淇℃伅" bordered :column="2" class="mb-4">
<DescriptionsItem label="褰掑睘閮ㄩ棬">{{ employee.deptName || '-' }}</DescriptionsItem>
- <DescriptionsItem label="宀椾綅">{{ employee.postName || '-' }}</DescriptionsItem>
+ <DescriptionsItem label="宀椾綅">{{ employee.postNames || '-' }}</DescriptionsItem>
<DescriptionsItem label="鍏ヨ亴鏃ユ湡">{{ employee.hireDate }}</DescriptionsItem>
<DescriptionsItem label="杞鏃ユ湡">{{ employee.regularDate || '-' }}</DescriptionsItem>
<DescriptionsItem label="绂昏亴鏃ユ湡">{{ employee.leaveDate || '-' }}</DescriptionsItem>
@@ -91,6 +97,9 @@
<Tabs.TabPane key="socialSecurity" tab="绀句繚鍏Н閲戞。妗�">
<SocialSecurityList form-type="detail" :employee-id="employee.id" :user-id="employee.userId" />
</Tabs.TabPane>
+ <Tabs.TabPane key="contract" tab="鍛樺伐鍚堝悓">
+ <ContractList :employee-id="employee.id" />
+ </Tabs.TabPane>
</Tabs>
</div>
</template>
diff --git a/src/views/hrm/employee/modules/import-form.vue b/src/views/hrm/employee/modules/import-form.vue
new file mode 100644
index 0000000..b9f256e
--- /dev/null
+++ b/src/views/hrm/employee/modules/import-form.vue
@@ -0,0 +1,161 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { HrmEmployeeApi } from '#/api/hrm/employee';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Switch, Upload } from 'ant-design-vue';
+
+import { importEmployee, importEmployeeTemplate } from '#/api/hrm/employee';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const updateSupport = ref(false);
+const importResult = ref<HrmEmployeeApi.ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importEmployee(
+ fileList.value[0] as File,
+ updateSupport.value,
+ );
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭垚鍔� ${createCount.value} 鏉°�佽鐩� ${updateCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importEmployeeTemplate();
+ downloadFileFromBlobPart({ fileName: '鍛樺伐瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createNames?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateNames?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureNames ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆鍛樺伐" class="w-[640px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div class="mt-4">
+ <span class="mr-2">鎵嬫満鍙峰凡瀛樺湪鏃舵洿鏂板憳宸ヤ俊鎭細</span>
+ <Switch v-model:checked="updateSupport" />
+ </div>
+ <div class="mt-2 text-gray-500">
+ 浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢锛涙ā鏉夸腑"閮ㄩ棬鍚嶇О/宀椾綅鍚嶇О/鐢ㄦ埛瑙掕壊"濉啓鍚嶇О锛屽矖浣嶅拰瑙掕壊澶氫釜鐢ㄩ�楀彿鍒嗛殧
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆鍛樺伐鏁版嵁锛岃绋嶅��...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex gap-4">
+ <span class="text-green-600">鎴愬姛鍒涘缓锛歿{ createCount }} 浜�</span>
+ <span class="text-blue-600">鎴愬姛鏇存柊锛歿{ updateCount }} 浜�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 浜�</span>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureNames"
+ :key="name"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/hrm/leave/data.ts b/src/views/hrm/leave/data.ts
index c6d5154..6e8570e 100644
--- a/src/views/hrm/leave/data.ts
+++ b/src/views/hrm/leave/data.ts
@@ -2,8 +2,12 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmLeaveApi } from '#/api/hrm/leave';
+import { markRaw } from 'vue';
+
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
+
+import { UserSelect } from '#/views/system/user/components';
/** 琛ㄥ崟绫诲瀷 */
export type FormType = 'create' | 'detail' | 'update';
@@ -18,6 +22,15 @@
triggerFields: [''],
show: () => false,
},
+ },
+ {
+ fieldName: 'userId',
+ label: '鐢宠浜�',
+ component: markRaw(UserSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐢宠浜�',
+ },
+ rules: 'selectRequired',
},
{
fieldName: 'leaveType',
@@ -134,7 +147,7 @@
export function useGridColumns(): VxeTableGridOptions<HrmLeaveApi.Leave>['columns'] {
return [
{ field: 'id', title: '鐢宠缂栧彿', width: 80 },
- { field: 'employeeName', title: '鐢宠浜�', minWidth: 100 },
+ { field: 'userName', title: '鐢宠浜�', minWidth: 100 },
{ field: 'deptName', title: '閮ㄩ棬', minWidth: 120 },
{
field: 'leaveType',
diff --git a/src/views/hrm/leave/detail/approve/index.vue b/src/views/hrm/leave/detail/approve/index.vue
new file mode 100644
index 0000000..0573837
--- /dev/null
+++ b/src/views/hrm/leave/detail/approve/index.vue
@@ -0,0 +1,93 @@
+<script lang="ts" setup>
+import type { HrmLeaveApi } from '#/api/hrm/leave';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { getLeave } from '#/api/hrm/leave';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'HrmLeaveApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<HrmLeaveApi.Leave>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鐢宠浜�', field: 'userName', render: textRender },
+ { label: '鎵�灞為儴闂�', field: 'deptName', render: textRender },
+ {
+ label: '璇峰亣绫诲瀷',
+ field: 'leaveType',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_LEAVE_TYPE, value: val }),
+ },
+ {
+ label: '寮�濮嬫椂闂�',
+ field: 'startTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ {
+ label: '缁撴潫鏃堕棿',
+ field: 'endTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '璇峰亣鏃堕暱', field: 'duration', render: textRender },
+ { label: '璇峰亣浜嬬敱', field: 'reason', render: textRender },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getLeave(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 璇峰亣鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">璇峰亣鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/hrm/leave/index.vue b/src/views/hrm/leave/index.vue
index e7ae028..d088fc2 100644
--- a/src/views/hrm/leave/index.vue
+++ b/src/views/hrm/leave/index.vue
@@ -2,9 +2,11 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmLeaveApi } from '#/api/hrm/leave';
+import { ref } from 'vue';
+
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
-import { message, Tag } from 'ant-design-vue';
+import { message, Modal, Select, Tag } from 'ant-design-vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
@@ -13,6 +15,7 @@
import {
cancelLeave,
deleteLeave,
+ getLeaveApproveProcessDefinitionList,
getLeavePage,
submitLeave,
} from '#/api/hrm/leave';
@@ -61,11 +64,34 @@
}
}
-/** 鎻愪氦璇峰亣鐢宠 */
+const submitModalVisible = ref(false);
+const currentRow = ref<HrmLeaveApi.Leave>();
+const processList = ref<HrmLeaveApi.ApproveProcess[]>([]);
+const selectedProcessKey = ref<string>();
+const submitLoading = ref(false);
+
+/** 鎻愪氦璇峰亣鐢宠锛堝厛閫夋嫨瀹℃壒娴佺▼锛� */
async function handleSubmit(row: HrmLeaveApi.Leave) {
- await submitLeave(row.id!);
- message.success('鎻愪氦鎴愬姛');
- handleRefresh();
+ processList.value = await getLeaveApproveProcessDefinitionList();
+ currentRow.value = row;
+ selectedProcessKey.value = undefined;
+ submitModalVisible.value = true;
+}
+
+async function confirmSubmit() {
+ if (!selectedProcessKey.value) {
+ message.warning('璇烽�夋嫨璇峰亣瀹℃壒娴佺▼');
+ return;
+ }
+ submitLoading.value = true;
+ try {
+ await submitLeave(currentRow.value!.id!, selectedProcessKey.value);
+ message.success('鎻愪氦鎴愬姛');
+ submitModalVisible.value = false;
+ handleRefresh();
+ } finally {
+ submitLoading.value = false;
+ }
}
/** 鎾ら攢璇峰亣鐢宠 */
@@ -187,5 +213,19 @@
/>
</template>
</Grid>
+ <Modal
+ v-model:open="submitModalVisible"
+ title="鎻愪氦鑷冲崗鍚屽姙鍏�"
+ width="420px"
+ :confirm-loading="submitLoading"
+ @ok="confirmSubmit"
+ >
+ <Select
+ v-model:value="selectedProcessKey"
+ class="w-full"
+ placeholder="璇烽�夋嫨璇峰亣瀹℃壒娴佺▼"
+ :options="processList.map((item) => ({ label: item.name, value: item.key }))"
+ />
+ </Modal>
</Page>
</template>
\ No newline at end of file
diff --git a/src/views/hrm/leave/modules/form.vue b/src/views/hrm/leave/modules/form.vue
index 4aab581..f701b81 100644
--- a/src/views/hrm/leave/modules/form.vue
+++ b/src/views/hrm/leave/modules/form.vue
@@ -4,6 +4,8 @@
import { computed, ref } from 'vue';
+import { useUserStore } from '@vben/stores';
+
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
@@ -16,6 +18,8 @@
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
+
+const userStore = useUserStore();
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
@@ -74,6 +78,8 @@
formApi.setDisabled(data.formType === 'detail');
modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
if (!data?.id) {
+ // 鏂板鏃堕粯璁ょ敵璇蜂汉涓哄綋鍓嶇櫥褰曠敤鎴凤紝鍙慨鏀�
+ await formApi.setFieldValue('userId', userStore.userInfo?.id);
return;
}
modalApi.lock();
diff --git a/src/views/hrm/overtime/data.ts b/src/views/hrm/overtime/data.ts
new file mode 100644
index 0000000..a4985b5
--- /dev/null
+++ b/src/views/hrm/overtime/data.ts
@@ -0,0 +1,320 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+export const OVERTIME_TYPE_OPTIONS = [
+ { label: '宸ヤ綔鏃ュ姞鐝�', value: 1 },
+ { label: '浼戞伅鏃ュ姞鐝�', value: 2 },
+ { label: '娉曞畾鑺傚亣鏃ュ姞鐝�', value: 3 },
+];
+
+export const RELATED_TYPE_OPTIONS = [
+ { label: '椤圭洰', value: 1 },
+ { label: '宸ュ崟', value: 2 },
+];
+
+export function getOvertimeTypeLabel(type?: number) {
+ return OVERTIME_TYPE_OPTIONS.find((item) => item.value === type)?.label || '-';
+}
+
+/** 鏂板/淇敼鍔犵彮鐢宠鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'employeeId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'baseInfoDivider',
+ label: '鐢宠浜哄熀纭�淇℃伅',
+ component: 'Divider',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'employeeName',
+ label: '鐢宠浜�',
+ component: 'Input',
+ componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+ },
+ {
+ fieldName: 'employeeNo',
+ label: '宸ュ彿',
+ component: 'Input',
+ componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+ },
+ {
+ fieldName: 'deptName',
+ label: '鎵�灞為儴闂�',
+ component: 'Input',
+ componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+ },
+ {
+ fieldName: 'postName',
+ label: '宀椾綅/鑱岀骇',
+ component: 'Input',
+ componentProps: { disabled: true, placeholder: '绯荤粺鑷姩甯﹀嚭' },
+ },
+ {
+ fieldName: 'overtimeInfoDivider',
+ label: '鍔犵彮鏍稿績淇℃伅',
+ component: 'Divider',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'overtimeDate',
+ label: '鍔犵彮鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鍔犵彮鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'overtimeType',
+ label: '鍔犵彮绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: OVERTIME_TYPE_OPTIONS,
+ placeholder: '璇烽�夋嫨鍔犵彮绫诲瀷',
+ },
+ help: '涓嶅悓绫诲瀷瀵瑰簲涓嶅悓鐨勮柂璧勮绠楄鍒�',
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'startTime',
+ label: '寮�濮嬫椂闂�',
+ component: 'TimePicker',
+ componentProps: {
+ format: 'HH:mm',
+ minuteStep: 30,
+ placeholder: '璇烽�夋嫨寮�濮嬫椂闂�',
+ valueFormat: 'HH:mm:ss',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '缁撴潫鏃堕棿',
+ component: 'TimePicker',
+ componentProps: {
+ format: 'HH:mm',
+ minuteStep: 30,
+ placeholder: '璇烽�夋嫨缁撴潫鏃堕棿',
+ valueFormat: 'HH:mm:ss',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'crossDay',
+ label: '鏄惁璺ㄥぉ',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ disabled: true,
+ unCheckedChildren: '鍚�',
+ },
+ defaultValue: false,
+ help: '缁撴潫鏃堕棿鏃╀簬鎴栫瓑浜庡紑濮嬫椂闂存椂锛岀郴缁熻嚜鍔ㄦ爣璁颁负璺ㄥぉ',
+ },
+ {
+ fieldName: 'mealDeduction',
+ label: '鎵i櫎鐢ㄩ鏃堕棿',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鎵i櫎',
+ unCheckedChildren: '涓嶆墸闄�',
+ },
+ defaultValue: false,
+ },
+ {
+ fieldName: 'mealDeductionHours',
+ label: '鐢ㄩ鎵i櫎鏃堕暱',
+ component: 'InputNumber',
+ componentProps: {
+ min: 0.5,
+ max: 4,
+ precision: 1,
+ step: 0.5,
+ style: { width: '100%' },
+ },
+ defaultValue: 1,
+ suffix: '灏忔椂',
+ dependencies: {
+ triggerFields: ['mealDeduction'],
+ show: (values) => Boolean(values.mealDeduction),
+ },
+ },
+ {
+ fieldName: 'duration',
+ label: '鍔犵彮鏃堕暱',
+ component: 'InputNumber',
+ componentProps: {
+ disabled: true,
+ min: 0,
+ precision: 1,
+ step: 0.5,
+ style: { width: '100%' },
+ },
+ help: '鎸夊疄闄呮椂娈垫墸闄ょ敤椁愭椂闂村悗锛岃嚜鍔ㄥ彇鏁村埌 0.5 灏忔椂',
+ suffix: '灏忔椂',
+ rules: 'required',
+ },
+ {
+ fieldName: 'reasonInfoDivider',
+ label: '鍔犵彮浜嬬敱',
+ component: 'Divider',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'reason',
+ label: '鍔犵彮鍘熷洜',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxlength: 500,
+ placeholder: '璇疯鏄庡姞鐝殑蹇呰鎬у拰鍘熷洜',
+ rows: 3,
+ showCount: true,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'relatedType',
+ label: '鍏宠仈绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: RELATED_TYPE_OPTIONS,
+ placeholder: '鍙�夛細椤圭洰鎴栧伐鍗�',
+ },
+ },
+ {
+ fieldName: 'relatedCode',
+ label: '椤圭洰/宸ュ崟缂栧彿',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ maxlength: 100,
+ placeholder: '璇疯緭鍏ュ叧鑱旈」鐩垨宸ュ崟缂栧彿',
+ },
+ dependencies: {
+ triggerFields: ['relatedType'],
+ componentProps: (values) => ({
+ allowClear: true,
+ maxlength: 100,
+ placeholder: values.relatedType === 1
+ ? '璇疯緭鍏ラ」鐩紪鍙�'
+ : values.relatedType === 2
+ ? '璇疯緭鍏ュ伐鍗曠紪鍙�'
+ : '璇峰厛閫夋嫨鍏宠仈绫诲瀷',
+ disabled: !values.relatedType,
+ }),
+ },
+ },
+ {
+ fieldName: 'workContent',
+ label: '宸ヤ綔鍐呭绠�杩�',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxlength: 1000,
+ placeholder: '璇风畝瑕佹弿杩板姞鐝湡闂磋鍒掑畬鎴愮殑鍏蜂綋宸ヤ綔',
+ rows: 4,
+ showCount: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'overtimeDate',
+ label: '鍔犵彮鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇烽�夋嫨鍔犵彮鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ },
+ {
+ fieldName: 'overtimeType',
+ label: '鍔犵彮绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: OVERTIME_TYPE_OPTIONS,
+ placeholder: '璇烽�夋嫨鍔犵彮绫诲瀷',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '瀹℃壒鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨瀹℃壒鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃瀛楁 */
+export function useGridColumns(): VxeTableGridOptions<HrmOvertimeApi.Overtime>['columns'] {
+ return [
+ { field: 'id', title: '鐢宠缂栧彿', width: 90 },
+ { field: 'employeeName', title: '鐢宠浜�', minWidth: 100 },
+ { field: 'employeeNo', title: '宸ュ彿', minWidth: 110 },
+ { field: 'deptName', title: '鎵�灞為儴闂�', minWidth: 120 },
+ { field: 'overtimeDate', title: '鍔犵彮鏃ユ湡', width: 120 },
+ {
+ field: 'overtimeType',
+ title: '鍔犵彮绫诲瀷',
+ width: 130,
+ slots: { default: 'overtimeType' },
+ },
+ { field: 'startTime', title: '寮�濮嬫椂闂�', width: 100 },
+ { field: 'endTime', title: '缁撴潫鏃堕棿', width: 100 },
+ { field: 'duration', title: '鏃堕暱(灏忔椂)', width: 100 },
+ {
+ field: 'status',
+ title: '瀹℃壒鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.HRM_AUDIT_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/hrm/overtime/detail/approve/index.vue b/src/views/hrm/overtime/detail/approve/index.vue
new file mode 100644
index 0000000..bfab940
--- /dev/null
+++ b/src/views/hrm/overtime/detail/approve/index.vue
@@ -0,0 +1,83 @@
+<script lang="ts" setup>
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { Spin, Tag } from 'ant-design-vue';
+
+import { getOvertime } from '#/api/hrm/overtime';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+import { getOvertimeTypeLabel } from '#/views/hrm/overtime/data';
+
+defineOptions({ name: 'HrmOvertimeApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+const loading = ref(false);
+const formData = ref<HrmOvertimeApi.Overtime>();
+
+function textRender(value: unknown) {
+ return value === 0 || value ? String(value) : '-';
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鐢宠浜�', field: 'employeeName', render: textRender },
+ { label: '宸ュ彿', field: 'employeeNo', render: textRender },
+ { label: '鎵�灞為儴闂�', field: 'deptName', render: textRender },
+ { label: '宀椾綅/鑱岀骇', field: 'postName', render: textRender },
+ { label: '鍔犵彮鏃ユ湡', field: 'overtimeDate', render: textRender },
+ {
+ label: '鍔犵彮绫诲瀷',
+ field: 'overtimeType',
+ render: (value) => h(Tag, { color: value === 3 ? 'red' : value === 2 ? 'orange' : 'blue' }, () => getOvertimeTypeLabel(Number(value))),
+ },
+ { label: '寮�濮嬫椂闂�', field: 'startTime', render: textRender },
+ { label: '缁撴潫鏃堕棿', field: 'endTime', render: textRender },
+ { label: '鍔犵彮鏃堕暱', field: 'duration', render: (value) => `${textRender(value)} 灏忔椂` },
+ { label: '鏄惁璺ㄥぉ', field: 'crossDay', render: (value) => value ? '鏄�' : '鍚�' },
+ { label: '鐢ㄩ鎵i櫎', field: 'mealDeduction', render: (value) => value ? '鏄�' : '鍚�' },
+ { label: '鎵i櫎鏃堕暱', field: 'mealDeductionHours', render: (value) => value ? `${value} 灏忔椂` : '-' },
+ { label: '鍔犵彮鍘熷洜', field: 'reason', render: textRender },
+ { label: '鍏宠仈绫诲瀷', field: 'relatedType', render: (value) => value === 1 ? '椤圭洰' : value === 2 ? '宸ュ崟' : '-' },
+ { label: '椤圭洰/宸ュ崟缂栧彿', field: 'relatedCode', render: textRender },
+ { label: '宸ヤ綔鍐呭绠�杩�', field: 'workContent', render: textRender },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (value) => h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value }),
+ },
+ { label: '鍒涘缓鏃堕棿', field: 'createTime', render: (value) => formatDateTime(value) as string },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ loading.value = true;
+ try {
+ formData.value = await getOvertime(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">鍔犵彮鐢宠淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/hrm/overtime/index.vue b/src/views/hrm/overtime/index.vue
new file mode 100644
index 0000000..9b58111
--- /dev/null
+++ b/src/views/hrm/overtime/index.vue
@@ -0,0 +1,200 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+
+import { ref } from 'vue';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message, Modal, Select, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ cancelOvertime,
+ deleteOvertime,
+ getOvertimeApproveProcessDefinitionList,
+ getOvertimePage,
+ submitOvertime,
+} from '#/api/hrm/overtime';
+import { $t } from '#/locales';
+
+import {
+ getOvertimeTypeLabel,
+ useGridColumns,
+ useGridFormSchema,
+} from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleDetail(row: HrmOvertimeApi.Overtime) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+function handleEdit(row: HrmOvertimeApi.Overtime) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+async function handleDelete(row: HrmOvertimeApi.Overtime) {
+ const hideLoading = message.loading({ content: '姝e湪鍒犻櫎鍔犵彮鐢宠...', duration: 0 });
+ try {
+ await deleteOvertime(row.id!);
+ message.success('鍔犵彮鐢宠鍒犻櫎鎴愬姛');
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const submitModalVisible = ref(false);
+const currentRow = ref<HrmOvertimeApi.Overtime>();
+const processList = ref<HrmOvertimeApi.ApproveProcess[]>([]);
+const selectedProcessKey = ref<string>();
+const submitLoading = ref(false);
+
+async function handleSubmit(row: HrmOvertimeApi.Overtime) {
+ processList.value = await getOvertimeApproveProcessDefinitionList();
+ currentRow.value = row;
+ selectedProcessKey.value = undefined;
+ submitModalVisible.value = true;
+}
+
+async function confirmSubmit() {
+ if (!selectedProcessKey.value) {
+ message.warning('璇烽�夋嫨鍗忓悓鍔炲叕瀹℃壒娴佺▼');
+ return;
+ }
+ submitLoading.value = true;
+ try {
+ await submitOvertime(currentRow.value!.id!, selectedProcessKey.value);
+ message.success('宸叉彁浜よ嚦鍗忓悓鍔炲叕瀹℃壒');
+ submitModalVisible.value = false;
+ handleRefresh();
+ } finally {
+ submitLoading.value = false;
+ }
+}
+
+async function handleCancel(row: HrmOvertimeApi.Overtime) {
+ await cancelOvertime(row.id!);
+ message.success('鎾ら攢鎴愬姛');
+ handleRefresh();
+}
+
+function getOvertimeTypeColor(type?: number) {
+ return type === 1 ? 'blue' : type === 2 ? 'orange' : type === 3 ? 'red' : 'default';
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useGridFormSchema() },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getOvertimePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<HrmOvertimeApi.Overtime>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Grid table-title="鍔犵彮鐢宠鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鍔犵彮鐢宠']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:overtime:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #overtimeType="{ row }">
+ <Tag :color="getOvertimeTypeColor(row.overtimeType)">
+ {{ getOvertimeTypeLabel(row.overtimeType) }}
+ </Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ { label: '鏌ョ湅', type: 'link', onClick: handleDetail.bind(null, row) },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['hrm:overtime:update'],
+ ifShow: row.status === 0,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦',
+ type: 'link',
+ auth: ['hrm:overtime:submit'],
+ ifShow: row.status === 0,
+ onClick: handleSubmit.bind(null, row),
+ },
+ {
+ label: '鎾ら攢',
+ type: 'link',
+ auth: ['hrm:overtime:cancel'],
+ ifShow: row.status === 10,
+ onClick: handleCancel.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:overtime:delete'],
+ ifShow: row.status === 0 || row.status === 40,
+ popConfirm: {
+ title: '纭鍒犻櫎璇ュ姞鐝敵璇峰悧锛�',
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ <Modal
+ v-model:open="submitModalVisible"
+ title="鎻愪氦鑷冲崗鍚屽姙鍏�"
+ width="420px"
+ :confirm-loading="submitLoading"
+ @ok="confirmSubmit"
+ >
+ <Select
+ v-model:value="selectedProcessKey"
+ class="w-full"
+ placeholder="璇烽�夋嫨鍔犵彮瀹℃壒娴佺▼"
+ :options="processList.map((item) => ({ label: item.name, value: item.key }))"
+ />
+ </Modal>
+ </Page>
+</template>
diff --git a/src/views/hrm/overtime/modules/form.vue b/src/views/hrm/overtime/modules/form.vue
new file mode 100644
index 0000000..4613de8
--- /dev/null
+++ b/src/views/hrm/overtime/modules/form.vue
@@ -0,0 +1,159 @@
+<script lang="ts" setup>
+import type { HrmOvertimeApi } from '#/api/hrm/overtime';
+import type { FormType } from '../data';
+
+import { computed, ref } from 'vue';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createOvertime,
+ getCurrentOvertimeApplicant,
+ getOvertime,
+ updateOvertime,
+} from '#/api/hrm/overtime';
+import { $t } from '#/locales';
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (isDetail.value) return '鏌ョ湅鍔犵彮鐢宠';
+ return formType.value === 'update' ? '淇敼鍔犵彮鐢宠' : '鏂板鍔犵彮鐢宠';
+});
+
+/** 鏍规嵁鏃舵銆佽法澶╂爣璇嗗拰鐢ㄩ鎵i櫎璁$畻鍔犵彮鏃堕暱锛屽苟鎸� 0.5 灏忔椂鍙栨暣 */
+function calculateDuration(values: Record<string, any>) {
+ const { startTime, endTime } = values;
+ if (!startTime || !endTime) return { crossDay: false, duration: 0 };
+
+ const toMinutes = (time: string) => {
+ const [hour = 0, minute = 0] = time.split(':').map(Number);
+ return hour * 60 + minute;
+ };
+ const startMinutes = toMinutes(startTime);
+ let endMinutes = toMinutes(endTime);
+ const crossDay = endMinutes <= startMinutes;
+ if (crossDay) endMinutes += 24 * 60;
+
+ const deductionMinutes = values.mealDeduction
+ ? Number(values.mealDeductionHours || 0) * 60
+ : 0;
+ const actualHours = Math.max(0, (endMinutes - startMinutes - deductionMinutes) / 60);
+ return {
+ crossDay,
+ duration: Math.round(actualHours * 2) / 2,
+ };
+}
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ scrollToFirstError: true,
+ showDefaultActions: false,
+ handleValuesChange: (values, changedFields) => {
+ const durationFields = [
+ 'startTime',
+ 'endTime',
+ 'mealDeduction',
+ 'mealDeductionHours',
+ ];
+ if (changedFields.some((field) => durationFields.includes(field))) {
+ const result = calculateDuration(values);
+ void formApi.setValues(result);
+ }
+ if (changedFields.includes('relatedType') && !values.relatedType) {
+ void formApi.setFieldValue('relatedCode', undefined);
+ }
+ },
+});
+
+async function setCurrentEmployee() {
+ const employee = await getCurrentOvertimeApplicant();
+ await formApi.setValues({
+ employeeId: employee.employeeId,
+ employeeName: employee.employeeName,
+ employeeNo: employee.employeeNo,
+ deptName: employee.deptName,
+ postName: employee.postName,
+ mealDeduction: false,
+ mealDeductionHours: 1,
+ crossDay: false,
+ duration: 0,
+ });
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+
+ const data = (await formApi.getValues()) as HrmOvertimeApi.Overtime;
+ if (!data.employeeId) {
+ message.warning('褰撳墠璐﹀彿鏈叧鑱斿憳宸ユ。妗堬紝鏆傛椂鏃犳硶鎻愪氦鍔犵彮鐢宠');
+ return;
+ }
+ if (!data.duration || data.duration <= 0) {
+ message.warning('鍔犵彮鏃堕暱蹇呴』澶т簬 0 灏忔椂锛岃妫�鏌ユ椂娈垫垨鐢ㄩ鎵i櫎');
+ return;
+ }
+ if (data.relatedType && !data.relatedCode?.trim()) {
+ message.warning('璇烽�夋嫨鍏宠仈绫诲瀷鍚庡~鍐欏搴旂殑椤圭洰鎴栧伐鍗曠紪鍙�');
+ return;
+ }
+
+ modalApi.lock();
+ try {
+ if (data.id) await updateOvertime(data);
+ else await createOvertime(data);
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) return;
+
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema() });
+ formApi.setDisabled(isDetail.value);
+ modalApi.setState({ showConfirmButton: !isDetail.value });
+
+ if (!data.id) {
+ await setCurrentEmployee();
+ return;
+ }
+
+ modalApi.lock();
+ try {
+ await formApi.setValues(await getOvertime(data.id));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-[900px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/hrm/resignation/detail/approve/index.vue b/src/views/hrm/resignation/detail/approve/index.vue
new file mode 100644
index 0000000..4e3a587
--- /dev/null
+++ b/src/views/hrm/resignation/detail/approve/index.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { HrmResignationApi } from '#/api/hrm/resignation';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { getResignation } from '#/api/hrm/resignation';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'HrmResignationApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<HrmResignationApi.Resignation>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鍛樺伐濮撳悕', field: 'employeeName', render: textRender },
+ { label: '鎵�灞為儴闂�', field: 'deptName', render: textRender },
+ { label: '鑱屼綅', field: 'postName', render: textRender },
+ {
+ label: '绂昏亴绫诲瀷',
+ field: 'resignationType',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_RESIGNATION_TYPE, value: val }),
+ },
+ {
+ label: '棰勮绂昏亴鏃ユ湡',
+ field: 'expectedDate',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '绂昏亴鍘熷洜', field: 'reason', render: textRender },
+ { label: '浜ゆ帴浜�', field: 'handoverUserName', render: textRender },
+ {
+ label: '浜ゆ帴鐘舵��',
+ field: 'handoverStatus',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_HANDOVER_STATUS, value: val }),
+ },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getResignation(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 绂昏亴鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">绂昏亴鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/hrm/salary/calculation/data.ts b/src/views/hrm/salary/calculation/data.ts
index 17860e1..bd96225 100644
--- a/src/views/hrm/salary/calculation/data.ts
+++ b/src/views/hrm/salary/calculation/data.ts
@@ -89,6 +89,7 @@
{ field: 'actualSalary', title: '瀹炲彂宸ヨ祫', width: 100, formatter: 'formatAmount2' },
{ field: 'workDays', title: '鍑哄嫟澶╂暟', width: 80 },
{ field: 'overtimeHours', title: '鍔犵彮鏃堕暱(h)', width: 100 },
+ { field: 'totalWorkHours', title: '鎺掔彮宸ユ椂(h)', width: 100 },
{
field: 'status',
title: '鐘舵��',
@@ -127,6 +128,7 @@
{ title: '璇峰亣鎵f', dataIndex: 'leaveDeduction', width: 110 },
{ title: '鍑哄嫟澶╂暟', dataIndex: 'workDays', width: 90 },
{ title: '鍔犵彮鏃堕暱', dataIndex: 'overtimeHours', width: 100 },
+ { title: '鎺掔彮宸ユ椂(h)', dataIndex: 'totalWorkHours', width: 100 },
{ title: '瀹炲彂宸ヨ祫', dataIndex: 'actualSalary', width: 100, fixed: 'right' },
];
}
diff --git a/src/views/hrm/salary/calculation/index.vue b/src/views/hrm/salary/calculation/index.vue
index 8536a41..33a1f36 100644
--- a/src/views/hrm/salary/calculation/index.vue
+++ b/src/views/hrm/salary/calculation/index.vue
@@ -16,9 +16,11 @@
import {
calculateSalary,
confirmSalary,
+ deleteSalaryCalculation,
exportSalaryCalculation,
getSalaryCalculationPage,
previewSalaryCalculation,
+ revokeSalaryCalculation,
saveSalaryCalculation,
} from '#/api/hrm/salary/calculation';
import { getDeptList } from '#/api/system/dept';
@@ -153,7 +155,7 @@
handleRefresh();
}
-/** 鎵归噺纭 */
+/** 鎵归噺纭閫変腑鐨勮褰� */
async function handleBatchConfirm() {
const records = gridApi.grid.getCheckboxRecords() as HrmSalaryCalculationApi.SalaryCalculation[];
if (records.length === 0) {
@@ -164,6 +166,58 @@
await confirmSalary(ids);
message.success('纭鎴愬姛');
handleRefresh();
+}
+
+/** 鎵归噺淇濆瓨閫変腑鐨勬牳绠楄褰曪紙浠呭緟纭鐘舵�侊級 */
+async function handleBatchSave() {
+ const records = gridApi.grid.getCheckboxRecords() as HrmSalaryCalculationApi.SalaryCalculation[];
+ if (records.length === 0) {
+ message.warning('璇烽�夋嫨瑕佷繚瀛樼殑璁板綍');
+ return;
+ }
+ const pending = records.filter((r) => r.status === 0);
+ if (pending.length === 0) {
+ message.warning('閫変腑鐨勮褰曞潎闈炲緟纭鐘舵�侊紝鏃犻渶淇濆瓨');
+ return;
+ }
+ const first = pending[0];
+ if (!first?.period) {
+ message.warning('缂哄皯鏍哥畻鍛ㄦ湡');
+ return;
+ }
+ await saveSalaryCalculation({ period: first.period, list: pending });
+ message.success('鎵归噺淇濆瓨鎴愬姛');
+ handleRefresh();
+}
+
+/** 鎾ら攢纭锛堝凡纭 鈫� 寰呯‘璁わ紝鍙噸鏂版牳绠楋級 */
+async function handleRevoke(row: HrmSalaryCalculationApi.SalaryCalculation) {
+ Modal.confirm({
+ title: '鎾ら攢纭',
+ content: `纭畾鎾ら攢銆�${row.no}銆戠殑纭鍚楋紵鎾ら攢鍚庡彲閲嶆柊鏍哥畻銆俙,
+ okText: '纭畾',
+ cancelText: '鍙栨秷',
+ onOk: async () => {
+ await revokeSalaryCalculation([row.id!]);
+ message.success('鎾ら攢鎴愬姛');
+ handleRefresh();
+ },
+ });
+}
+
+/** 鍒犻櫎寰呯‘璁ょ殑鏍哥畻璁板綍 */
+async function handleDelete(row: HrmSalaryCalculationApi.SalaryCalculation) {
+ Modal.confirm({
+ title: '纭鍒犻櫎',
+ content: `纭畾鍒犻櫎銆�${row.no}銆戠殑鏍哥畻璁板綍鍚楋紵鍒犻櫎鍚庨渶閲嶆柊鏍哥畻銆俙,
+ okText: '纭畾',
+ cancelText: '鍙栨秷',
+ onOk: async () => {
+ await deleteSalaryCalculation(row.id!);
+ message.success('鍒犻櫎鎴愬姛');
+ handleRefresh();
+ },
+ });
}
/** 瀵煎嚭钖叕鏍哥畻 */
@@ -182,7 +236,6 @@
keepSource: true,
checkboxConfig: {
highlight: true,
- reserve: true,
},
proxyConfig: {
ajax: {
@@ -193,6 +246,10 @@
...formValues,
}),
},
+ },
+ pagingConfig: {
+ pageSize: 20,
+ pageSizes: [20, 50, 100],
},
rowConfig: {
keyField: 'id',
@@ -261,7 +318,7 @@
v-if="previewData.length > 0"
:columns="usePreviewColumns()"
:data-source="previewData"
- :pagination="false"
+ :pagination="{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `鍏� ${t} 浜篳 }"
:scroll="{ x: 1800, y: 400 }"
size="small"
row-key="userId"
@@ -430,6 +487,12 @@
onClick: handleBatchConfirm,
},
{
+ label: '鎵归噺淇濆瓨',
+ type: 'primary',
+ auth: ['hrm:salary-calculation:save'],
+ onClick: handleBatchSave,
+ },
+ {
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
@@ -454,6 +517,21 @@
ifShow: row.status === 0,
onClick: handleConfirm.bind(null, row),
},
+ {
+ label: '鍒犻櫎',
+ type: 'link',
+ danger: true,
+ auth: ['hrm:salary-calculation:delete'],
+ ifShow: row.status === 0,
+ onClick: handleDelete.bind(null, row),
+ },
+ {
+ label: '鎾ら攢纭',
+ type: 'link',
+ auth: ['hrm:salary-calculation:confirm'],
+ ifShow: row.status === 10,
+ onClick: handleRevoke.bind(null, row),
+ },
]"
/>
</template>
diff --git a/src/views/hrm/salary/leave-type-config/data.ts b/src/views/hrm/salary/leave-type-config/data.ts
index f8e53f1..14cb42d 100644
--- a/src/views/hrm/salary/leave-type-config/data.ts
+++ b/src/views/hrm/salary/leave-type-config/data.ts
@@ -3,7 +3,7 @@
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
/** 琛ㄥ崟绫诲瀷 */
-export type FormType = 'update';
+export type FormType = 'create' | 'update';
/** 鏂板/淇敼璇峰亣鎵f閰嶇疆鐨勮〃鍗� */
export function useFormSchema(): VbenFormSchema[] {
@@ -17,13 +17,29 @@
},
},
{
- fieldName: 'name',
+ fieldName: 'leaveType',
label: '璇峰亣绫诲瀷',
- component: 'Input',
+ component: 'Select',
componentProps: {
- disabled: true,
- placeholder: '',
+ options: [
+ { label: '浜嬪亣', value: 1 },
+ { label: '鐥呭亣', value: 2 },
+ { label: '骞村亣', value: 3 },
+ { label: '濠氬亣', value: 4 },
+ { label: '浜у亣', value: 5 },
+ { label: '涓у亣', value: 6 },
+ ],
+ placeholder: '璇烽�夋嫨璇峰亣绫诲瀷',
+ style: { width: '100%' },
},
+ dependencies: {
+ triggerFields: ['id'],
+ // 缂栬緫鏃朵笉鍏佽淇敼璇峰亣绫诲瀷
+ componentProps: (values: Record<string, unknown>) => ({
+ disabled: Boolean(values.id),
+ }),
+ },
+ rules: 'required',
formItemClass: 'col-span-2',
},
{
@@ -47,6 +63,7 @@
fieldName: 'status',
label: '鐘舵��',
component: 'RadioGroup',
+ defaultValue: 0,
componentProps: {
options: [
{ label: '鍚敤', value: 0 },
diff --git a/src/views/hrm/salary/leave-type-config/index.vue b/src/views/hrm/salary/leave-type-config/index.vue
index a22d4c7..c92af92 100644
--- a/src/views/hrm/salary/leave-type-config/index.vue
+++ b/src/views/hrm/salary/leave-type-config/index.vue
@@ -7,7 +7,10 @@
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getLeaveTypeConfigList } from '#/api/hrm/leave-type-config';
+import {
+ deleteLeaveTypeConfig,
+ getLeaveTypeConfigList,
+} from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useGridColumns } from './data';
@@ -23,9 +26,21 @@
gridApi.query();
}
+/** 鏂板閰嶇疆 */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
/** 缂栬緫閰嶇疆 */
function handleEdit(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
- formModalApi.setData({ id: row.id, row }).open();
+ formModalApi.setData({ formType: 'update', row }).open();
+}
+
+/** 鍒犻櫎閰嶇疆 */
+async function handleDelete(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
+ await deleteLeaveTypeConfig(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', ['璇峰亣鎵f閰嶇疆']));
+ handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -56,6 +71,19 @@
<Page auto-content-height>
<FormModal @success="handleRefresh" />
<Grid table-title="璇峰亣鎵f閰嶇疆鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['璇峰亣鎵f閰嶇疆']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:leave-type-config:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
<template #deductionRatio="{ row }">
<Tag :color="row.deductionRatio === 0 ? 'success' : row.deductionRatio === 1 ? 'error' : 'warning'">
{{ row.deductionRatio === 0 ? '涓嶆墸娆�' : `${(row.deductionRatio * 100).toFixed(0)}%` }}
@@ -76,6 +104,17 @@
auth: ['hrm:leave-type-config:update'],
onClick: handleEdit.bind(null, row),
},
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:leave-type-config:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', ['璇峰亣鎵f閰嶇疆']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
]"
/>
</template>
diff --git a/src/views/hrm/salary/leave-type-config/modules/form.vue b/src/views/hrm/salary/leave-type-config/modules/form.vue
index f016033..dbe2aab 100644
--- a/src/views/hrm/salary/leave-type-config/modules/form.vue
+++ b/src/views/hrm/salary/leave-type-config/modules/form.vue
@@ -1,19 +1,28 @@
<script lang="ts" setup>
+import type { FormType } from '../data';
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
-import { updateLeaveTypeConfig } from '#/api/hrm/leave-type-config';
+import {
+ createLeaveTypeConfig,
+ updateLeaveTypeConfig,
+} from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() =>
+ formType.value === 'update' ? '缂栬緫鎵f姣斾緥' : '鏂板璇峰亣鎵f閰嶇疆',
+);
const [Form, formApi] = useVbenForm({
commonConfig: {
@@ -36,11 +45,35 @@
return;
}
modalApi.lock();
- const data = await formApi.getValues();
+ const data = (await formApi.getValues()) as HrmLeaveTypeConfigApi.LeaveTypeConfig;
// 杞崲姣斾緥涓哄皬鏁�
- data.deductionRatio = data.deductionRatio / 100;
+ const deductionRatio = data.deductionRatio! / 100;
try {
- await updateLeaveTypeConfig(data);
+ if (formType.value === 'update') {
+ await updateLeaveTypeConfig({
+ id: data.id,
+ deductionRatio,
+ remark: data.remark,
+ status: data.status,
+ });
+ } else {
+ // 璇峰亣绫诲瀷鍚嶇О鐢辨墍閫夌被鍨嬭嚜鍔ㄥ甫鍑猴紝鏃犻渶鎵嬪姩濉啓
+ const NAME_MAP: Record<number, string> = {
+ 1: '浜嬪亣',
+ 2: '鐥呭亣',
+ 3: '骞村亣',
+ 4: '濠氬亣',
+ 5: '浜у亣',
+ 6: '涓у亣',
+ };
+ await createLeaveTypeConfig({
+ leaveType: data.leaveType,
+ name: NAME_MAP[data.leaveType!] ?? '',
+ deductionRatio,
+ remark: data.remark,
+ status: data.status,
+ });
+ }
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
@@ -52,16 +85,24 @@
if (!isOpen) {
return;
}
- const data = modalApi.getData<{ id: number; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>();
+ const data = modalApi.getData<{ formType: FormType; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>();
+ formType.value = data.formType;
+ if (data.formType === 'create' || !data.row) {
+ await formApi.resetForm();
+ return;
+ }
// 杞崲姣斾緥涓虹櫨鍒嗘瘮鏄剧ず
- const row = { ...data.row, deductionRatio: (data.row.deductionRatio || 0) * 100 };
+ const row = {
+ ...data.row,
+ deductionRatio: (data.row.deductionRatio || 0) * 100,
+ };
await formApi.setValues(row);
},
});
</script>
<template>
- <Modal title="缂栬緫鎵f姣斾緥" class="w-[600px]">
+ <Modal :title="getTitle" class="w-[600px]">
<Form class="mx-4" />
</Modal>
-</template>
\ No newline at end of file
+</template>
diff --git a/src/views/hrm/salary/structure/data.ts b/src/views/hrm/salary/structure/data.ts
new file mode 100644
index 0000000..68d1ddf
--- /dev/null
+++ b/src/views/hrm/salary/structure/data.ts
@@ -0,0 +1,163 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
+
+export type FormType = 'create' | 'update';
+
+const statusOptions = [
+ { label: '鍚敤', value: 0 },
+ { label: '绂佺敤', value: 1 },
+];
+
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'name',
+ label: '缁撴瀯鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ maxlength: 100,
+ placeholder: '渚嬪锛氭爣鍑嗗憳宸ヨ柂閰�佺鐞嗗眰钖叕',
+ showCount: true,
+ },
+ rules: 'required',
+ formItemClass: 'col-span-2',
+ },
+ {
+ fieldName: 'overtimePayRatio',
+ label: '宸ヤ綔鏃ュ�嶇巼',
+ component: 'InputNumber',
+ componentProps: {
+ addonAfter: '鍊�',
+ max: 10,
+ min: 0,
+ precision: 2,
+ step: 0.1,
+ placeholder: '0 琛ㄧず涓嶈鍔犵彮宸ヨ祫',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'restDayOvertimePayRatio',
+ label: '浼戞伅鏃ュ�嶇巼',
+ component: 'InputNumber',
+ componentProps: {
+ addonAfter: '鍊�',
+ max: 10,
+ min: 0,
+ precision: 2,
+ step: 0.1,
+ placeholder: '0 琛ㄧず涓嶈璇ョ被鍨嬪姞鐝伐璧�',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'legalHolidayOvertimePayRatio',
+ label: '娉曞畾鑺傚亣鏃ュ�嶇巼',
+ component: 'InputNumber',
+ componentProps: {
+ addonAfter: '鍊�',
+ max: 10,
+ min: 0,
+ precision: 2,
+ step: 0.1,
+ placeholder: '0 琛ㄧず涓嶈璇ョ被鍨嬪姞鐝伐璧�',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'RadioGroup',
+ componentProps: { options: statusOptions },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ maxlength: 500,
+ placeholder: '濉啓閫傜敤宀椾綅銆佽亴绾ф垨鐗规畩璁¤柂璇存槑',
+ rows: 3,
+ showCount: true,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '缁撴瀯鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ョ粨鏋勫悕绉�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: statusOptions,
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>['columns'] {
+ return [
+ { field: 'name', title: '缁撴瀯鍚嶇О', minWidth: 180 },
+ {
+ field: 'overtimePayRatio',
+ title: '宸ヤ綔鏃ュ姞鐝�嶇巼',
+ width: 145,
+ slots: { default: 'overtimePayRatio' },
+ },
+ {
+ field: 'restDayOvertimePayRatio',
+ title: '浼戞伅鏃ュ姞鐝�嶇巼',
+ width: 145,
+ slots: { default: 'restDayOvertimePayRatio' },
+ },
+ {
+ field: 'legalHolidayOvertimePayRatio',
+ title: '娉曞畾鑺傚亣鏃ュ�嶇巼',
+ width: 165,
+ slots: { default: 'legalHolidayOvertimePayRatio' },
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 90,
+ slots: { default: 'status' },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'actions',
+ title: '鎿嶄綔',
+ fixed: 'right',
+ slots: { default: 'actions' },
+ width: 150,
+ },
+ ];
+}
diff --git a/src/views/hrm/salary/structure/index.vue b/src/views/hrm/salary/structure/index.vue
new file mode 100644
index 0000000..4427175
--- /dev/null
+++ b/src/views/hrm/salary/structure/index.vue
@@ -0,0 +1,137 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteSalaryStructure,
+ getSalaryStructureList,
+} from '#/api/hrm/salary/structure';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+defineOptions({ name: 'HrmSalaryStructure' });
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+function handleEdit(row: HrmSalaryStructureApi.SalaryStructure) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+async function handleDelete(row: HrmSalaryStructureApi.SalaryStructure) {
+ await deleteSalaryStructure(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', ['钖叕缁撴瀯']));
+ handleRefresh();
+}
+
+function formatRatio(value?: number) {
+ return value === undefined || value === null ? '-' : `${value.toFixed(2)} 鍊峘;
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useGridFormSchema() },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ pagerConfig: { enabled: false },
+ proxyConfig: {
+ ajax: {
+ query: async (_params, formValues) =>
+ await getSalaryStructureList(formValues),
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Alert
+ class="mb-4"
+ description="鍩烘湰宸ヨ祫鍙栧憳宸ユ。妗堬紱绀句繚鍜屽叕绉噾鍩烘暟鍙栧憳宸ョぞ淇濇。妗堛�傝繖閲屼粎缁存姢宸ヤ綔鏃ャ�佷紤鎭棩鍜屾硶瀹氳妭鍋囨棩鍔犵彮鍊嶇巼銆�"
+ message="钖叕缁撴瀯缁存姢鍔犵彮璁¤柂瑙勫垯"
+ show-icon
+ type="info"
+ />
+ <Grid table-title="钖叕缁撴瀯鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['钖叕缁撴瀯']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:salary-structure:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #overtimePayRatio="{ row }">
+ <Tag :color="(row.overtimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
+ {{ (row.overtimePayRatio ?? 0) > 0 ? formatRatio(row.overtimePayRatio) : '涓嶈鍔犵彮宸ヨ祫' }}
+ </Tag>
+ </template>
+ <template #restDayOvertimePayRatio="{ row }">
+ <Tag :color="(row.restDayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
+ {{ (row.restDayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.restDayOvertimePayRatio) : '涓嶈鍔犵彮宸ヨ祫' }}
+ </Tag>
+ </template>
+ <template #legalHolidayOvertimePayRatio="{ row }">
+ <Tag :color="(row.legalHolidayOvertimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
+ {{ (row.legalHolidayOvertimePayRatio ?? 0) > 0 ? formatRatio(row.legalHolidayOvertimePayRatio) : '涓嶈鍔犵彮宸ヨ祫' }}
+ </Tag>
+ </template>
+ <template #status="{ row }">
+ <Tag :color="row.status === 0 ? 'success' : 'default'">
+ {{ row.status === 0 ? '鍚敤' : '绂佺敤' }}
+ </Tag>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['hrm:salary-structure:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['hrm:salary-structure:delete'],
+ popConfirm: {
+ title: '纭鍒犻櫎璇ヨ柂閰粨鏋勫悧锛熷凡琚憳宸ヤ娇鐢ㄧ殑缁撴瀯涓嶅彲鍒犻櫎銆�',
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/hrm/salary/structure/modules/form.vue b/src/views/hrm/salary/structure/modules/form.vue
new file mode 100644
index 0000000..0330821
--- /dev/null
+++ b/src/views/hrm/salary/structure/modules/form.vue
@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
+import type { FormType } from '../data';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createSalaryStructure,
+ getSalaryStructure,
+ updateSalaryStructure,
+} from '#/api/hrm/salary/structure';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const getTitle = computed(() =>
+ formType.value === 'update' ? '淇敼钖叕缁撴瀯' : '鏂板钖叕缁撴瀯',
+);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+ modalApi.lock();
+ try {
+ const data =
+ (await formApi.getValues()) as HrmSalaryStructureApi.SalaryStructure;
+ if (data.id) {
+ await updateSalaryStructure(data);
+ } else {
+ await createSalaryStructure(data);
+ }
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) return;
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ await formApi.resetForm();
+ if (!data.id) {
+ await formApi.setValues({
+ legalHolidayOvertimePayRatio: 3,
+ overtimePayRatio: 1.5,
+ restDayOvertimePayRatio: 2,
+ status: 0,
+ });
+ return;
+ }
+ modalApi.lock();
+ try {
+ await formApi.setValues(await getSalaryStructure(data.id));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-[680px]">
+ <Alert
+ class="mx-4 mb-4"
+ description="涓夌鍔犵彮绫诲瀷鍒嗗埆璁剧疆鍊嶇巼锛涙煇涓�椤硅涓� 0 鏃讹紝浠呰绫诲瀷涓嶈绠楀姞鐝伐璧勶紝瀹℃壒閫氳繃鐨勫姞鐝椂闀夸粛姝e父缁熻銆�"
+ message="鍔犵彮宸ヨ祫璁$畻璇存槑"
+ show-icon
+ type="info"
+ />
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/hrm/schedule/data.ts b/src/views/hrm/schedule/data.ts
new file mode 100644
index 0000000..821428e
--- /dev/null
+++ b/src/views/hrm/schedule/data.ts
@@ -0,0 +1,46 @@
+/** 鎺掔彮缃戞牸鐨勬棩鏈熷伐鍏� */
+
+const WEEK_LABELS = ['鏃�', '涓�', '浜�', '涓�', '鍥�', '浜�', '鍏�'];
+
+/** 杩斿洖鏌愭湀澶╂暟鏁扮粍锛屽 2026-08 鈫� [1..31] */
+export function getDaysOfMonth(yearMonth: string): number[] {
+ const [yearStr, monthStr] = yearMonth.split('-');
+ const days = new Date(Number(yearStr), Number(monthStr), 0).getDate();
+ return Array.from({ length: days }, (_, i) => i + 1);
+}
+
+/** 杩斿洖鏌愭棩鐨勬槦鏈熶腑鏂囨爣绛撅紙鏃�/涓�/浜屸�︼級 */
+export function getWeekLabel(yearMonth: string, day: number): string {
+ const date = new Date(`${yearMonth}-${String(day).padStart(2, '0')}`);
+ return WEEK_LABELS[date.getDay()] ?? '';
+}
+
+/** 鍒ゆ柇鏌愭棩鏄惁涓哄懆鏈� */
+export function isWeekend(yearMonth: string, day: number): boolean {
+ const date = new Date(`${yearMonth}-${String(day).padStart(2, '0')}`);
+ const week = date.getDay();
+ return week === 0 || week === 6;
+}
+
+/** 鐢熸垚鏌愭湀鏌愭棩鐨勬棩鏈熷瓧绗︿覆 yyyy-MM-dd */
+export function formatDate(yearMonth: string, day: number): string {
+ return `${yearMonth}-${String(day).padStart(2, '0')}`;
+}
+
+/** 鑷姩鎺掔彮浼戞伅妯″紡 */
+export const REST_MODE = {
+ /** 鍙屼紤锛氬懆鍏�佸懆鏃ヤ紤鎭� */
+ DOUBLE: 'double',
+ /** 鍗曚紤锛氬懆鏃ヤ紤鎭� */
+ SINGLE: 'single',
+ /** 涓嶄紤鎭細姣忓ぉ鎺掔彮 */
+ NONE: 'none',
+} as const;
+
+export type RestMode = (typeof REST_MODE)[keyof typeof REST_MODE];
+
+export const REST_MODE_OPTIONS: { label: string; value: RestMode }[] = [
+ { label: '鍙屼紤锛堝叚鏃ヤ紤锛�', value: REST_MODE.DOUBLE },
+ { label: '鍗曚紤锛堝懆鏃ヤ紤锛�', value: REST_MODE.SINGLE },
+ { label: '涓嶄紤鎭�', value: REST_MODE.NONE },
+];
diff --git a/src/views/hrm/schedule/index.vue b/src/views/hrm/schedule/index.vue
new file mode 100644
index 0000000..6f4c443
--- /dev/null
+++ b/src/views/hrm/schedule/index.vue
@@ -0,0 +1,743 @@
+<script lang="ts" setup>
+import type { HrmEmployeeApi } from '#/api/hrm/employee';
+import type { HrmEmployeeScheduleApi } from '#/api/hrm/schedule';
+import type { HrmShiftApi } from '#/api/hrm/shift';
+
+import { computed, onMounted, ref, watch } from 'vue';
+
+import dayjs from 'dayjs';
+
+import {
+ Button,
+ Checkbox,
+ DatePicker,
+ Empty,
+ Input,
+ message,
+ Modal,
+ Pagination,
+ Radio,
+ Segmented,
+ Select,
+ TreeSelect,
+} from 'ant-design-vue';
+
+import { Page } from '#/packages/effects/common-ui/src';
+
+import { getEmployeePage } from '#/api/hrm/employee';
+import { getShiftList } from '#/api/hrm/shift';
+import { getEmployeeScheduleList, saveEmployeeSchedule } from '#/api/hrm/schedule';
+import { getDeptList } from '#/api/system/dept';
+import { handleTree } from '#/packages/utils/src';
+
+import {
+ REST_MODE,
+ REST_MODE_OPTIONS,
+ formatDate,
+ getDaysOfMonth,
+} from './data';
+import type { RestMode } from './data';
+
+defineOptions({ name: 'HrmSchedule' });
+
+// ========== 鏌ヨ鏉′欢 ==========
+const month = ref<string>('');
+const deptId = ref<number>();
+const deptTree = ref<any[]>([]);
+
+/** 鍛樺伐鍒嗛〉锛堟帓鐝綉鏍兼瘡娆″彧鏄剧ず涓�椤靛憳宸ワ級 */
+const employeePageNo = ref(1);
+const employeePageSize = ref(20);
+const employeeTotal = ref(0);
+
+/** 鎸夊憳宸ョ紪鍙�/濮撳悕/鎵嬫満鍙锋悳绱㈠崟涓憳宸� */
+const keyword = ref('');
+
+/** 淇濆瓨鏃跺崟娆¤姹傜殑鎺掔彮鏉$洰鏁帮紙闃叉澶ф壒閲忚姹傝繃澶э級 */
+const SAVE_BATCH_SIZE = 1000;
+
+// ========== 鏁版嵁 ==========
+const employees = ref<HrmEmployeeApi.Employee[]>([]);
+const shiftList = ref<HrmShiftApi.Shift[]>([]);
+const loading = ref(false);
+const saving = ref(false);
+/** 鏄惁宸叉墽琛岃繃鏌ヨ */
+const loaded = ref(false);
+
+// ========== 鍛樺伐鍕鹃�� ==========
+/** Checkbox 鍙樻洿浜嬩欢锛堜粎鍙� checked 瀛楁锛� */
+interface CheckChangeEvent {
+ target: { checked: boolean };
+}
+
+/** 鍕鹃�夌殑鍛樺伐ID闆嗗悎锛屼负绌烘椂鑷姩鎺掔彮浣滅敤浜庡叏閮ㄥ憳宸� */
+const checkedEmployeeIds = ref<Set<number>>(new Set());
+
+const isAllChecked = computed(
+ () =>
+ employees.value.length > 0 &&
+ checkedEmployeeIds.value.size === employees.value.length,
+);
+
+function isChecked(id: number): boolean {
+ return checkedEmployeeIds.value.has(id);
+}
+
+function handleCheck(id: number, e: CheckChangeEvent) {
+ const set = new Set(checkedEmployeeIds.value);
+ if (e.target.checked) {
+ set.add(id);
+ } else {
+ set.delete(id);
+ }
+ checkedEmployeeIds.value = set;
+}
+
+function handleCheckAll(e: CheckChangeEvent) {
+ if (e.target.checked) {
+ checkedEmployeeIds.value = new Set(
+ employees.value
+ .map((item) => item.id)
+ .filter((id): id is number => id != null),
+ );
+ } else {
+ checkedEmployeeIds.value = new Set();
+ }
+}
+
+// ========== 鑷姩鎺掔彮 ==========
+const autoOpen = ref(false);
+const autoShiftId = ref<number>();
+const autoRestMode = ref<RestMode>(REST_MODE.DOUBLE);
+
+/** 鑷姩鎺掔彮鐝閫夋嫨閫夐」 */
+const autoShiftOptions = computed(() =>
+ shiftList.value.map((shift) => ({
+ label: `${shift.name} (${shift.startTime}~${shift.endTime})`,
+ value: shift.id,
+ })),
+);
+
+/** 鎸変紤鎭ā寮忎负褰撳墠鍛樺伐鐢熸垚鏁存湀鎺掔彮锛堜紤鎭棩娓呴櫎锛屽伐浣滄棩濉彮娆★級 */
+function applyAutoSchedule() {
+ if (!employees.value.length || !days.value.length) {
+ message.warning('璇峰厛鏌ヨ鍑哄憳宸�');
+ return;
+ }
+ if (autoShiftId.value == null) {
+ message.warning('璇烽�夋嫨鎺掔彮鐝');
+ return;
+ }
+ // 鍕鹃�変簡鍛樺伐鍒欏彧瀵瑰嬀閫変汉鐢熸晥锛屾湭鍕鹃�夊垯浣滅敤浜庡叏閮ㄥ憳宸�
+ const targets =
+ checkedEmployeeIds.value.size > 0
+ ? employees.value.filter(
+ (item) =>
+ item.id != null && checkedEmployeeIds.value.has(item.id),
+ )
+ : employees.value;
+ if (!targets.length) {
+ message.warning('璇峰厛鍕鹃�夊憳宸�');
+ return;
+ }
+ const map = new Map(scheduleMap.value);
+ for (const emp of targets) {
+ if (emp.id == null) {
+ continue;
+ }
+ for (const day of days.value) {
+ const date = formatDate(month.value, day);
+ const week = new Date(
+ `${month.value}-${String(day).padStart(2, '0')}`,
+ ).getDay();
+ let rest = false;
+ if (autoRestMode.value === REST_MODE.DOUBLE) {
+ rest = week === 0 || week === 6;
+ } else if (autoRestMode.value === REST_MODE.SINGLE) {
+ rest = week === 0;
+ }
+ map.set(`${emp.id}_${date}`, rest ? null : autoShiftId.value);
+ }
+ }
+ scheduleMap.value = map;
+ autoOpen.value = false;
+ message.success('鑷姩鎺掔彮宸茬敓鎴愶紝鍙墜鍔ㄥ井璋冨悗淇濆瓨');
+}
+
+/** 鐝 id 鈫� 鐝 鏄犲皠 */
+const shiftMap = computed(() => {
+ const map = new Map<number, HrmShiftApi.Shift>();
+ for (const shift of shiftList.value) {
+ if (shift.id != null) {
+ map.set(shift.id, shift);
+ }
+ }
+ return map;
+});
+
+/**
+ * 鎺掔彮鏁版嵁銆俴ey = `${employeeId}_${date}`锛寁alue = shiftId锛坣ull 琛ㄧず浼戞伅锛�
+ */
+const scheduleMap = ref(new Map<string, number | null>());
+
+/** 褰撴湀澶╂暟鏁扮粍 */
+const days = computed(() => (month.value ? getDaysOfMonth(month.value) : []));
+
+// ========== 瑙嗗浘鍒囨崲锛堝懆/鏈堬級==========
+const WEEK_LABELS = ['鏃�', '涓�', '浜�', '涓�', '鍥�', '浜�', '鍏�'];
+
+interface ViewDay {
+ day: number;
+ date: string;
+ weekLabel: string;
+ isWeekend: boolean;
+}
+
+const viewMode = ref<'week' | 'month'>('week');
+/** 鏈堣鍥炬槸鍚︽樉绀哄懆鏈垪锛堥粯璁ら殣钘忥紝鍑忓垪缂撹В鎷ユ尋锛� */
+const showWeekend = ref(false);
+/** 鍛ㄨ鍥鹃敋鐐规棩鏈� */
+const viewAnchor = ref<Date>(new Date());
+
+/** 褰撳墠瑙嗗浘瑕佸睍绀虹殑鏃ユ湡鍒楄〃 */
+const viewDays = computed<ViewDay[]>(() => {
+ if (viewMode.value === 'week') {
+ // 浠ュ懆涓�涓轰竴鍛ㄨ捣鐐�
+ const base = dayjs(viewAnchor.value).day(1);
+ return Array.from({ length: 7 }, (_, i) => {
+ const date = base.add(i, 'day');
+ const week = date.day();
+ return {
+ day: date.date(),
+ date: date.format('YYYY-MM-DD'),
+ weekLabel: WEEK_LABELS[week] ?? '',
+ isWeekend: week === 0 || week === 6,
+ };
+ });
+ }
+ return days.value
+ .filter((day) => {
+ if (showWeekend.value) {
+ return true;
+ }
+ const week = new Date(
+ `${month.value}-${String(day).padStart(2, '0')}`,
+ ).getDay();
+ return week !== 0 && week !== 6;
+ })
+ .map((day) => {
+ const week = new Date(
+ `${month.value}-${String(day).padStart(2, '0')}`,
+ ).getDay();
+ return {
+ day,
+ date: formatDate(month.value, day),
+ weekLabel: WEEK_LABELS[week] ?? '',
+ isWeekend: week === 0 || week === 6,
+ };
+ });
+});
+
+/** 鍛ㄨ鍥惧綋鍓嶆樉绀鸿寖鍥存爣绛� */
+const weekRangeLabel = computed(() => {
+ const list = viewDays.value;
+ const first = list[0];
+ const last = list[list.length - 1];
+ if (!first || !last) {
+ return '';
+ }
+ return `${first.date} ~ ${last.date}`;
+});
+
+function shiftWeek(delta: number) {
+ viewAnchor.value = dayjs(viewAnchor.value).add(delta, 'week').toDate();
+}
+
+// 鏈堜唤鍙樺寲鏃讹紝鍛ㄨ鍥鹃敋鐐硅惤鍒拌鏈� 1 鍙锋墍鍦ㄥ懆
+watch(month, (value) => {
+ if (value) {
+ viewAnchor.value = new Date(`${value}-01`);
+ }
+});
+
+function getShiftByCell(employeeId: number, date: string): HrmShiftApi.Shift | undefined {
+ const shiftId = scheduleMap.value.get(`${employeeId}_${date}`);
+ if (shiftId == null) {
+ return undefined;
+ }
+ return shiftMap.value.get(shiftId);
+}
+
+// ========== 鐝閫夋嫨寮圭獥 ==========
+const pickerOpen = ref(false);
+const pickerEmployee = ref<HrmEmployeeApi.Employee>();
+const pickerDate = ref<string>('');
+
+const pickerTitle = computed(() => {
+ if (!pickerEmployee.value || !pickerDate.value) {
+ return '閫夋嫨鐝';
+ }
+ return `閫夋嫨鐝 路 ${pickerEmployee.value.name} 路 ${pickerDate.value}`;
+});
+
+function openPicker(emp: HrmEmployeeApi.Employee, date: string) {
+ pickerEmployee.value = emp;
+ pickerDate.value = date;
+ pickerOpen.value = true;
+}
+
+/** 褰撳墠寮圭獥鏍煎瓙鏄惁宸查�夎鐝 */
+function isCurrentPick(shiftId: number): boolean {
+ if (!pickerEmployee.value || !pickerDate.value) {
+ return false;
+ }
+ return (
+ scheduleMap.value.get(`${pickerEmployee.value.id}_${pickerDate.value}`) ===
+ shiftId
+ );
+}
+
+function pickShift(shiftId: number | null) {
+ if (!pickerEmployee.value || !pickerDate.value) {
+ return;
+ }
+ scheduleMap.value.set(`${pickerEmployee.value.id}_${pickerDate.value}`, shiftId);
+ pickerOpen.value = false;
+}
+
+// ========== 鍔犺浇 ==========
+async function loadDeptTree() {
+ const data = await getDeptList();
+ deptTree.value = handleTree(data);
+}
+
+/**
+ * 鍒嗛〉鎷夊彇閮ㄩ棬涓嬬殑鍏ㄩ儴鍛樺伐锛堝崟椤典笂闄� 200锛屽惊鐜嫾鍏級锛屼緵鎺掔彮缃戞牸鏁磋〃灞曠ず
+ */
+/** 鍔犺浇褰撳墠椤靛憳宸ワ紙缈婚〉鏃跺彧閲嶆柊鎷夊憳宸ワ紝鎺掔彮鏁版嵁淇濈暀锛� */
+async function loadEmployeePage() {
+ loading.value = true;
+ try {
+ const res = await getEmployeePage({
+ pageNo: employeePageNo.value,
+ pageSize: employeePageSize.value,
+ deptId: deptId.value,
+ keyword: keyword.value || undefined,
+ });
+ employees.value = res.list ?? [];
+ employeeTotal.value = res.total ?? 0;
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 鍛樺伐缈婚〉 */
+function handlePageChange(page: number, pageSize: number) {
+ if (pageSize !== employeePageSize.value) {
+ employeePageSize.value = pageSize;
+ employeePageNo.value = 1;
+ } else {
+ employeePageNo.value = page;
+ }
+ loadEmployeePage();
+}
+
+/** 鎸夊叧閿瘝鎼滅储鍛樺伐 */
+function handleSearch() {
+ employeePageNo.value = 1;
+ loadEmployeePage();
+}
+
+async function loadData() {
+ if (!month.value) {
+ message.warning('璇烽�夋嫨鏈堜唤');
+ return;
+ }
+ if (!deptId.value) {
+ message.warning('璇峰厛閫夋嫨閮ㄩ棬');
+ return;
+ }
+ loading.value = true;
+ try {
+ const [shifts, schedules] = await Promise.all([
+ getShiftList(),
+ getEmployeeScheduleList({
+ yearMonth: month.value,
+ deptId: deptId.value,
+ }),
+ ]);
+ shiftList.value = shifts;
+ const map = new Map<string, number | null>();
+ for (const item of schedules) {
+ if (item.employeeId != null && item.date) {
+ map.set(`${item.employeeId}_${item.date}`, item.shiftId ?? null);
+ }
+ }
+ scheduleMap.value = map;
+ loaded.value = true;
+ } finally {
+ loading.value = false;
+ }
+ // 鍔犺浇绗竴椤靛憳宸�
+ employeePageNo.value = 1;
+ await loadEmployeePage();
+}
+
+// ========== 淇濆瓨锛堟暣鏈堝叏閲忥級 ==========
+async function handleSave() {
+ if (!month.value || !employees.value.length) {
+ return;
+ }
+ Modal.confirm({
+ title: '纭淇濆瓨',
+ content: `灏嗕繚瀛樺綋鍓嶉〉 ${employees.value.length} 鍚嶅憳宸ュ湪 ${month.value} 鐨勫叏閮ㄦ帓鐝紝纭畾缁х画鍚楋紵`,
+ okText: '纭畾',
+ cancelText: '鍙栨秷',
+ onOk: async () => {
+ saving.value = true;
+ try {
+ const items: HrmEmployeeScheduleApi.ScheduleItem[] = [];
+ for (const emp of employees.value) {
+ if (emp.id == null) {
+ continue;
+ }
+ for (const day of days.value) {
+ const date = formatDate(month.value, day);
+ items.push({
+ employeeId: emp.id,
+ date,
+ shiftId: scheduleMap.value.get(`${emp.id}_${date}`) ?? null,
+ });
+ }
+ }
+ // 澶ф壒閲忔椂鍒嗘壒鎻愪氦锛岄伩鍏嶅崟娆¤姹傝繃澶�
+ for (let i = 0; i < items.length; i += SAVE_BATCH_SIZE) {
+ await saveEmployeeSchedule(items.slice(i, i + SAVE_BATCH_SIZE));
+ }
+ message.success('鎺掔彮淇濆瓨鎴愬姛');
+ } finally {
+ saving.value = false;
+ }
+ },
+ });
+}
+
+onMounted(() => {
+ month.value = dayjs().format('YYYY-MM');
+ loadDeptTree();
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <!-- 鏌ヨ宸ュ叿鏍� -->
+ <div
+ class="mb-4 flex flex-wrap items-center gap-3 rounded-lg bg-white p-4"
+ >
+ <DatePicker
+ v-model:value="month"
+ picker="month"
+ :allow-clear="false"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ style="width: 140px"
+ />
+ <TreeSelect
+ v-model:value="deptId"
+ :tree-data="deptTree"
+ :field-names="{ label: 'name', value: 'id', children: 'children' }"
+ allow-clear
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ tree-default-expand-all
+ style="width: 200px"
+ />
+ <Input
+ v-model:value="keyword"
+ allow-clear
+ placeholder="鍛樺伐缂栧彿/濮撳悕/鎵嬫満鍙�"
+ style="width: 200px"
+ @pressEnter="handleSearch"
+ />
+ <Button type="primary" :loading="loading" @click="loadData">
+ 鏌ヨ鎺掔彮
+ </Button>
+ <Button :disabled="!employees.length" @click="autoOpen = true">
+ 鑷姩鎺掔彮
+ </Button>
+ <Button
+ type="primary"
+ :loading="saving"
+ :disabled="!employees.length"
+ @click="handleSave"
+ >
+ 淇濆瓨鎺掔彮
+ </Button>
+ <Segmented
+ v-model:value="viewMode"
+ :options="[
+ { label: '鍛ㄨ鍥�', value: 'week' },
+ { label: '鏈堣鍥�', value: 'month' },
+ ]"
+ />
+ <template v-if="viewMode === 'week'">
+ <Button @click="shiftWeek(-1)">涓婁竴鍛�</Button>
+ <span class="text-sm text-gray-500">{{ weekRangeLabel }}</span>
+ <Button @click="shiftWeek(1)">涓嬩竴鍛�</Button>
+ </template>
+ <template v-else>
+ <Checkbox v-model:checked="showWeekend">鏄剧ず鍛ㄦ湯</Checkbox>
+ </template>
+ </div>
+
+ <!-- 鐝鍥句緥 -->
+ <div
+ v-if="shiftList.length"
+ class="mb-4 flex flex-wrap items-center gap-4 rounded-lg bg-white px-4 py-3"
+ >
+ <span class="text-sm text-gray-500">鐝鍥句緥锛�</span>
+ <span
+ v-for="shift in shiftList"
+ :key="shift.id"
+ class="inline-flex items-center gap-1.5 text-sm"
+ >
+ <span
+ class="inline-block h-3 w-3 rounded"
+ :style="{ backgroundColor: shift.color || '#1677ff' }"
+ />
+ <span>{{ shift.name }}</span>
+ <span class="text-xs text-gray-400"
+ >{{ shift.startTime }}~{{ shift.endTime }}</span
+ >
+ </span>
+ </div>
+
+ <!-- 鎺掔彮缃戞牸 -->
+ <div class="overflow-auto rounded-lg bg-white">
+ <Empty
+ v-if="!employees.length && !loading"
+ :description="
+ loaded
+ ? '璇ラ儴闂ㄦ殏鏃犵鍚堟潯浠剁殑鍛樺伐锛岃璋冩暣閮ㄩ棬鎴栧憳宸ョ姸鎬�'
+ : '璇烽�夋嫨鏈堜唤銆侀儴闂ㄥ悗锛岀偣鍑汇�屾煡璇㈡帓鐝��'
+ "
+ class="py-20"
+ />
+ <table v-else class="schedule-table w-full text-sm">
+ <thead>
+ <tr>
+ <th class="schedule-check-col bg-gray-100">
+ <Checkbox :checked="isAllChecked" @change="handleCheckAll" />
+ </th>
+ <th class="schedule-name-col bg-gray-100 text-left">鍛樺伐</th>
+ <th
+ v-for="vd in viewDays"
+ :key="vd.date"
+ :class="[
+ 'schedule-date-col text-center',
+ vd.isWeekend ? 'bg-red-50' : 'bg-gray-50',
+ ]"
+ >
+ <div class="font-medium">{{ vd.day }}</div>
+ <div class="text-xs text-gray-400">{{ vd.weekLabel }}</div>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="emp in employees" :key="emp.id">
+ <td class="schedule-check-col" @click.stop>
+ <Checkbox
+ :checked="isChecked(emp.id!)"
+ @change="handleCheck(emp.id!, $event)"
+ />
+ </td>
+ <td class="schedule-name-col">
+ <div class="font-medium">{{ emp.name }}</div>
+ <div class="text-xs text-gray-400">{{ emp.employeeNo }}</div>
+ </td>
+ <td
+ v-for="vd in viewDays"
+ :key="vd.date"
+ class="schedule-cell"
+ :class="[vd.isWeekend ? 'bg-red-50/40' : '']"
+ @click="openPicker(emp, vd.date)"
+ >
+ <span
+ v-if="getShiftByCell(emp.id!, vd.date)"
+ class="shift-chip"
+ :title="getShiftByCell(emp.id!, vd.date)!.name"
+ :style="{
+ backgroundColor:
+ getShiftByCell(emp.id!, vd.date)!.color || '#1677ff',
+ }"
+ >
+ {{ getShiftByCell(emp.id!, vd.date)!.name }}
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- 鍛樺伐鍒嗛〉 -->
+ <div
+ v-if="loaded && employees.length"
+ class="mt-4 flex items-center justify-end rounded-lg bg-white p-3"
+ >
+ <Pagination
+ :current="employeePageNo"
+ :page-size="employeePageSize"
+ :total="employeeTotal"
+ show-size-changer
+ show-quick-jumper
+ :show-total="(total: number) => `鍏� ${total} 浜篳"
+ @change="handlePageChange"
+ />
+ </div>
+
+ <!-- 鐝閫夋嫨寮圭獥 -->
+ <Modal
+ v-model:open="pickerOpen"
+ :title="pickerTitle"
+ :footer="null"
+ :width="440"
+ >
+ <div class="grid grid-cols-2 gap-3">
+ <button
+ v-for="shift in shiftList"
+ :key="shift.id"
+ type="button"
+ class="flex cursor-pointer flex-col items-center gap-1 rounded-lg border px-2 py-3 transition-colors hover:border-blue-500"
+ :class="[
+ isCurrentPick(shift.id!)
+ ? 'border-blue-500 bg-blue-50'
+ : 'border-gray-200',
+ ]"
+ @click="pickShift(shift.id!)"
+ >
+ <span class="inline-flex items-center gap-2 text-sm">
+ <span
+ class="inline-block h-3 w-3 rounded"
+ :style="{ backgroundColor: shift.color || '#1677ff' }"
+ />
+ <span class="font-medium">{{ shift.name }}</span>
+ </span>
+ <span class="text-xs text-gray-400"
+ >{{ shift.startTime }} ~ {{ shift.endTime }}</span
+ >
+ </button>
+ </div>
+ <div class="mt-4 flex items-center justify-between">
+ <Button danger @click="pickShift(null)">娓呴櫎鎺掔彮锛堜紤鎭級</Button>
+ <Button @click="pickerOpen = false">鍙栨秷</Button>
+ </div>
+ </Modal>
+
+ <!-- 鑷姩鎺掔彮寮圭獥 -->
+ <Modal v-model:open="autoOpen" title="鑷姩鎺掔彮" :footer="null" :width="440">
+ <div class="space-y-4">
+ <div>
+ <div class="mb-1.5 text-sm text-gray-500">鎺掔彮鐝</div>
+ <Select
+ v-model:value="autoShiftId"
+ :options="autoShiftOptions"
+ placeholder="璇烽�夋嫨鎺掔彮鐝"
+ style="width: 100%"
+ />
+ </div>
+ <div>
+ <div class="mb-1.5 text-sm text-gray-500">浼戞伅妯″紡</div>
+ <Radio.Group
+ v-model:value="autoRestMode"
+ :options="REST_MODE_OPTIONS"
+ option-type="button"
+ />
+ </div>
+ <div class="text-xs text-gray-400">
+ 灏嗘寜鎵�閫夋ā寮忎负{{
+ checkedEmployeeIds.size > 0
+ ? checkedEmployeeIds.size
+ : employees.length
+ }}
+ 鍚嶅憳宸ョ敓鎴愭暣鏈堟帓鐝瓄{
+ checkedEmployeeIds.size > 0
+ ? '锛堜粎鍕鹃�夌殑鍛樺伐锛�'
+ : '锛堟湭鍕鹃�夋椂涓哄綋鍓嶉〉鍏ㄩ儴鍛樺伐锛�'
+ }}锛岀敓鎴愬悗鍙啀鎵嬪姩璋冩暣銆�
+ </div>
+ <div class="flex justify-end gap-2">
+ <Button @click="autoOpen = false">鍙栨秷</Button>
+ <Button type="primary" @click="applyAutoSchedule">鐢熸垚鎺掔彮</Button>
+ </div>
+ </div>
+ </Modal>
+ </Page>
+</template>
+
+<style lang="scss" scoped>
+.schedule-table {
+ width: 100%;
+ min-width: max-content;
+ border-collapse: separate;
+ border-spacing: 0;
+
+ th,
+ td {
+ height: 44px;
+ border: 1px solid #f0f0f0;
+ }
+
+ .schedule-date-col {
+ width: 52px;
+ min-width: 52px;
+ }
+
+ .schedule-check-col,
+ .schedule-name-col {
+ position: sticky;
+ z-index: 10;
+ background-color: #fafafa;
+ }
+
+ .schedule-check-col {
+ left: 0;
+ width: 40px;
+ min-width: 40px;
+ padding: 0 8px;
+ text-align: center;
+ }
+
+ .schedule-name-col {
+ left: 40px;
+ min-width: 180px;
+ padding: 0 12px;
+ line-height: 1.3;
+ }
+
+ thead .schedule-check-col,
+ thead .schedule-name-col {
+ z-index: 20;
+ }
+
+ .schedule-cell {
+ padding: 2px;
+ cursor: pointer;
+ text-align: center;
+
+ &:hover {
+ background-color: #e6f4ff;
+ }
+ }
+
+ .shift-chip {
+ display: inline-block;
+ max-width: 100%;
+ padding: 1px 6px;
+ overflow: hidden;
+ color: #fff;
+ font-size: 11px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-radius: 3px;
+ }
+}
+</style>
diff --git a/src/views/hrm/shift/data.ts b/src/views/hrm/shift/data.ts
new file mode 100644
index 0000000..123b748
--- /dev/null
+++ b/src/views/hrm/shift/data.ts
@@ -0,0 +1,186 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmShiftApi } from '#/api/hrm/shift';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'update' | 'detail';
+
+/** 鐝鏍囩棰滆壊棰勮 */
+export const SHIFT_COLORS = [
+ { label: '钃濊壊', value: '#1677ff' },
+ { label: '缁胯壊', value: '#52c41a' },
+ { label: '姗欒壊', value: '#fa8c16' },
+ { label: '绱壊', value: '#722ed1' },
+ { label: '绮夎壊', value: '#eb2f96' },
+ { label: '榛勮壊', value: '#faad14' },
+ { label: '闈掕壊', value: '#13c2c2' },
+ { label: '鐏拌壊', value: '#8c8c8c' },
+];
+
+/** 鏂板/缂栬緫/鏌ョ湅鐝鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鐝鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ彮娆″悕绉帮紝濡傦細鐧界彮銆佸鐝�',
+ maxlength: 50,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'startTime',
+ label: '涓婄彮鏃堕棿',
+ component: 'TimePicker',
+ componentProps: {
+ placeholder: '閫夋嫨涓婄彮鏃堕棿',
+ valueFormat: 'HH:mm:ss',
+ format: 'HH:mm:ss',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'endTime',
+ label: '涓嬬彮鏃堕棿',
+ component: 'TimePicker',
+ componentProps: {
+ placeholder: '閫夋嫨涓嬬彮鏃堕棿',
+ valueFormat: 'HH:mm:ss',
+ format: 'HH:mm:ss',
+ style: { width: '100%' },
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'workHours',
+ label: '鏍囧噯宸ユ椂(灏忔椂)',
+ component: 'InputNumber',
+ componentProps: {
+ placeholder: '濡� 8銆�8.5',
+ min: 0.5,
+ max: 24,
+ precision: 2,
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'crossDay',
+ label: '鏄惁璺ㄥぉ',
+ component: 'Switch',
+ componentProps: {
+ checkedValue: true,
+ unCheckedValue: false,
+ },
+ },
+ {
+ fieldName: 'color',
+ label: '鏍囩棰滆壊',
+ component: 'Select',
+ componentProps: {
+ options: SHIFT_COLORS,
+ placeholder: '璇烽�夋嫨棰滆壊',
+ style: { width: '100%' },
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ maxlength: 500,
+ showCount: true,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '鐝鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ョ彮娆″悕绉�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<HrmShiftApi.Shift>['columns'] {
+ return [
+ { field: 'name', title: '鐝鍚嶇О', minWidth: 120 },
+ { field: 'startTime', title: '涓婄彮鏃堕棿', width: 100 },
+ { field: 'endTime', title: '涓嬬彮鏃堕棿', width: 100 },
+ { field: 'workHours', title: '鏍囧噯宸ユ椂(h)', width: 110 },
+ {
+ field: 'crossDay',
+ title: '璺ㄥぉ',
+ width: 80,
+ formatter: ({ cellValue }) => (cellValue ? '鏄�' : '鍚�'),
+ },
+ {
+ field: 'color',
+ title: '鏍囩棰滆壊',
+ width: 110,
+ slots: {
+ default: 'color',
+ },
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 150,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/src/views/hrm/shift/index.vue b/src/views/hrm/shift/index.vue
new file mode 100644
index 0000000..1e98746
--- /dev/null
+++ b/src/views/hrm/shift/index.vue
@@ -0,0 +1,162 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { HrmShiftApi } from '#/api/hrm/shift';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { message, Modal } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteShift,
+ exportShift,
+ getShiftPage,
+} from '#/api/hrm/shift';
+import { $t } from '#/locales';
+
+import { SHIFT_COLORS, useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鏂板鐝 */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫鐝 */
+function handleUpdate(row: HrmShiftApi.Shift) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 鏌ョ湅鐝 */
+function handleDetail(row: HrmShiftApi.Shift) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎鐝 */
+async function handleDelete(id: number) {
+ Modal.confirm({
+ title: '纭鍒犻櫎',
+ content: '纭畾瑕佸垹闄よ鐝鍚楋紵鍒犻櫎鍚庡凡鏈夋帓鐝皢澶卞幓鐝淇℃伅銆�',
+ okText: '纭畾',
+ cancelText: '鍙栨秷',
+ onOk: async () => {
+ await deleteShift(id);
+ message.success('鍒犻櫎鎴愬姛');
+ handleRefresh();
+ },
+ });
+}
+
+/** 瀵煎嚭鐝 */
+async function handleExport() {
+ const data = await exportShift(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '鐝.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getShiftPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<HrmShiftApi.Shift>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Grid table-title="鐝鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鐝']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['hrm:shift:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['hrm:shift:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #color="{ row }">
+ <div class="flex items-center gap-2">
+ <span
+ class="inline-block h-4 w-4 rounded"
+ :style="{ backgroundColor: row.color || '#8c8c8c' }"
+ />
+ <span>
+ {{
+ SHIFT_COLORS.find((item) => item.value === row.color)?.label ||
+ row.color ||
+ '-'
+ }}
+ </span>
+ </div>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鏌ョ湅',
+ type: 'link',
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('ui.actionTitle.edit'),
+ type: 'link',
+ auth: ['hrm:shift:update'],
+ onClick: handleUpdate.bind(null, row),
+ },
+ {
+ label: $t('ui.actionTitle.delete'),
+ type: 'link',
+ danger: true,
+ auth: ['hrm:shift:delete'],
+ onClick: handleDelete.bind(null, row.id!),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/hrm/shift/modules/form.vue b/src/views/hrm/shift/modules/form.vue
new file mode 100644
index 0000000..7c69b65
--- /dev/null
+++ b/src/views/hrm/shift/modules/form.vue
@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { HrmShiftApi } from '#/api/hrm/shift';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createShift, getShift, updateShift } from '#/api/hrm/shift';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅鐝';
+ }
+ if (formType.value === 'update') {
+ return '缂栬緫鐝';
+ }
+ return '鏂板鐝';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 120,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as HrmShiftApi.Shift;
+ try {
+ if (formType.value === 'update') {
+ await updateShift(data);
+ } else {
+ await createShift(data);
+ }
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema(data.formType) });
+ formApi.setDisabled(data.formType === 'detail');
+ modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ await formApi.setValues(await getShift(data.id));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-[720px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/hrm/transfer/detail/approve/index.vue b/src/views/hrm/transfer/detail/approve/index.vue
new file mode 100644
index 0000000..5d983ef
--- /dev/null
+++ b/src/views/hrm/transfer/detail/approve/index.vue
@@ -0,0 +1,90 @@
+<script lang="ts" setup>
+import type { HrmTransferApi } from '#/api/hrm/transfer';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { formatDateTime } from '@vben/utils';
+
+import { Spin } from 'ant-design-vue';
+
+import { getTransfer } from '#/api/hrm/transfer';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+defineOptions({ name: 'HrmTransferApproveDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<HrmTransferApi.Transfer>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鍛樺伐濮撳悕', field: 'employeeName', render: textRender },
+ { label: '褰撳墠閮ㄩ棬', field: 'currentDeptName', render: textRender },
+ { label: '褰撳墠鑱屼綅', field: 'currentPostName', render: textRender },
+ { label: '鐩爣閮ㄩ棬', field: 'targetDeptName', render: textRender },
+ { label: '鐩爣鑱屼綅', field: 'targetPostName', render: textRender },
+ {
+ label: '璋冨矖绫诲瀷',
+ field: 'transferType',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_TRANSFER_TYPE, value: val }),
+ },
+ {
+ label: '鐢熸晥鏃ユ湡',
+ field: 'effectiveDate',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '璋冨矖鍘熷洜', field: 'reason', render: textRender },
+ {
+ label: '瀹℃壒鐘舵��',
+ field: 'status',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.HRM_AUDIT_STATUS, value: val }),
+ },
+ { label: '澶囨敞', field: 'remark', render: textRender },
+ {
+ label: '鍒涘缓鏃堕棿',
+ field: 'createTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getTransfer(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="space-y-4">
+ <!-- 璋冨矖鍩烘湰淇℃伅 -->
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">璋冨矖鍩烘湰淇℃伅</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/im/home/index.vue b/src/views/im/home/index.vue
index 5f7bdc5..74fee42 100644
--- a/src/views/im/home/index.vue
+++ b/src/views/im/home/index.vue
@@ -194,7 +194,7 @@
[() => conversationStore.getTotalUnreadCount, () => route.fullPath],
([count]) => {
nextTick(() => {
- const base = preferences.app.name
+ const base = preferences.copyright.companyName
document.title = count > 0 ? `(${count > 99 ? '99+' : count}鏉℃湭璇�)${base}` : base
})
},
diff --git a/src/views/mes/dv/machinery/type/components/tree.vue b/src/views/mes/dv/machinery/type/components/tree.vue
index 46b8e7f..70971bf 100644
--- a/src/views/mes/dv/machinery/type/components/tree.vue
+++ b/src/views/mes/dv/machinery/type/components/tree.vue
@@ -5,7 +5,6 @@
import { ref } from 'vue';
import { CommonStatusEnum } from '@vben/constants';
-import { handleTree } from '@vben/utils';
import { Card, Input } from 'ant-design-vue';
@@ -30,12 +29,10 @@
proxyConfig: {
ajax: {
query: async () =>
- handleTree(
- await getMachineryTypeList({
- name: searchKeyword.value || undefined,
- status: CommonStatusEnum.ENABLE,
- }),
- ),
+ await getMachineryTypeList({
+ name: searchKeyword.value || undefined,
+ status: CommonStatusEnum.ENABLE,
+ }),
},
},
rowConfig: {
diff --git a/src/views/mes/dv/machinery/type/index.vue b/src/views/mes/dv/machinery/type/index.vue
index e12ad96..1c47fbe 100644
--- a/src/views/mes/dv/machinery/type/index.vue
+++ b/src/views/mes/dv/machinery/type/index.vue
@@ -5,7 +5,6 @@
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
-import { handleTree } from '@vben/utils';
import { message } from 'ant-design-vue';
@@ -81,7 +80,7 @@
proxyConfig: {
ajax: {
query: async (_, formValues) =>
- handleTree(await getMachineryTypeList(formValues)),
+ await getMachineryTypeList(formValues),
},
},
rowConfig: {
diff --git a/src/views/mes/dv/metering/components/index.ts b/src/views/mes/dv/metering/components/index.ts
new file mode 100644
index 0000000..feacb92
--- /dev/null
+++ b/src/views/mes/dv/metering/components/index.ts
@@ -0,0 +1,2 @@
+export { default as DvMeteringSelectDialog } from './select-dialog.vue';
+export { default as DvMeteringSelect } from './select.vue';
diff --git a/src/views/mes/dv/metering/components/select-dialog.vue b/src/views/mes/dv/metering/components/select-dialog.vue
new file mode 100644
index 0000000..5d2cd21
--- /dev/null
+++ b/src/views/mes/dv/metering/components/select-dialog.vue
@@ -0,0 +1,213 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { nextTick, ref } from 'vue';
+
+import { message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getMeteringPage } from '#/api/mes/dv/metering';
+
+import {
+ useMeteringSelectGridColumns,
+ useMeteringSelectGridFormSchema,
+} from '../data';
+
+defineOptions({ name: 'DvMeteringSelectDialog' });
+
+const emit = defineEmits<{
+ selected: [rows: MesDvMeteringApi.Metering[]];
+}>();
+
+const open = ref(false); // 寮圭獥鏄惁鎵撳紑
+const multiple = ref(false); // 鏄惁澶氶��
+const selectedRows = ref<MesDvMeteringApi.Metering[]>([]); // 宸查�夊櫒鍏峰垪琛�
+const preSelectedIds = ref<number[]>([]); // 棰勯�夊櫒鍏风紪鍙峰垪琛�
+
+/** 鑾峰彇褰撳墠琛ㄦ牸鏁版嵁 */
+function getTableRows() {
+ return gridApi.grid.getTableData().fullData as MesDvMeteringApi.Metering[];
+}
+
+/** 鑾峰彇澶氶�夎褰曪紝鍖呭惈 VXE reserve 璺ㄩ〉璁板綍 */
+function getMultipleSelectedRows() {
+ const selectedMap = new Map<number, MesDvMeteringApi.Metering>();
+ const records = [
+ ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
+ ...(gridApi.grid.getCheckboxRecords?.() ?? []),
+ ] as MesDvMeteringApi.Metering[];
+ records.forEach((row) => {
+ const rowId = row.id;
+ if (rowId !== undefined && rowId !== null) {
+ selectedMap.set(rowId, row);
+ }
+ });
+ return [...selectedMap.values()];
+}
+
+/** 澶勭悊鍕鹃�夊彉鍖� */
+function handleCheckboxSelectChange() {
+ selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊鍗曢�夊彉鍖� */
+function handleRadioChange(row: MesDvMeteringApi.Metering) {
+ selectedRows.value = [row];
+}
+
+/** 澶氶�夋ā寮忎笅鍒囨崲琛屽嬀閫� */
+async function toggleMultipleRow(row: MesDvMeteringApi.Metering) {
+ const selected = gridApi.grid.isCheckedByCheckboxRow(row);
+ await gridApi.grid.setCheckboxRow(row, !selected);
+ selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊琛屽弻鍑� */
+async function handleCellDblclick({
+ row,
+}: {
+ row: MesDvMeteringApi.Metering;
+}) {
+ if (multiple.value) {
+ await toggleMultipleRow(row);
+ return;
+ }
+ selectedRows.value = [row];
+ await gridApi.grid.setRadioRow(row);
+ handleConfirm();
+}
+
+/** 鍥炴樉棰勯�夊櫒鍏� */
+async function applyPreSelection() {
+ if (preSelectedIds.value.length === 0) {
+ return;
+ }
+ const rows = getTableRows();
+ for (const row of rows) {
+ if (
+ row.id === undefined ||
+ row.id === null ||
+ !preSelectedIds.value.includes(row.id)
+ ) {
+ continue;
+ }
+ if (multiple.value) {
+ await gridApi.grid.setCheckboxRow(row, true);
+ } else {
+ await gridApi.grid.setRadioRow(row);
+ selectedRows.value = [row];
+ return;
+ }
+ }
+ if (multiple.value) {
+ selectedRows.value = getMultipleSelectedRows();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useMeteringSelectGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useMeteringSelectGridColumns(false),
+ height: 480,
+ keepSource: true,
+ checkboxConfig: {
+ highlight: true,
+ range: true,
+ reserve: true,
+ },
+ radioConfig: {
+ highlight: true,
+ trigger: 'row',
+ },
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getMeteringPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesDvMeteringApi.Metering>,
+ gridEvents: {
+ cellDblclick: handleCellDblclick,
+ checkboxAll: handleCheckboxSelectChange,
+ checkboxChange: handleCheckboxSelectChange,
+ radioChange: ({ row }: { row: MesDvMeteringApi.Metering }) => {
+ handleRadioChange(row);
+ },
+ },
+});
+
+/** 閲嶇疆鏌ヨ鍜岄�夋嫨鐘舵�� */
+async function resetQueryState() {
+ selectedRows.value = [];
+ await gridApi.grid.clearCheckboxRow();
+ await gridApi.grid.clearCheckboxReserve();
+ await gridApi.grid.clearRadioRow();
+ await gridApi.formApi.resetForm();
+}
+
+/** 鎵撳紑璁¢噺鍣ㄥ叿閫夋嫨寮圭獥 */
+async function openModal(
+ selectedIds?: number[],
+ options?: { multiple?: boolean },
+) {
+ open.value = true;
+ multiple.value = options?.multiple ?? false;
+ preSelectedIds.value = selectedIds || [];
+ await nextTick();
+ gridApi.setGridOptions({
+ columns: useMeteringSelectGridColumns(multiple.value),
+ });
+ await resetQueryState();
+ await gridApi.query();
+ await nextTick();
+ await applyPreSelection();
+}
+
+/** 鍏抽棴璁¢噺鍣ㄥ叿閫夋嫨寮圭獥 */
+async function closeModal() {
+ open.value = false;
+ await resetQueryState();
+}
+
+/** 纭閫夋嫨璁¢噺鍣ㄥ叿 */
+function handleConfirm() {
+ const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
+ if (rows.length === 0) {
+ message.warning(multiple.value ? '璇疯嚦灏戦�夋嫨涓�鏉℃暟鎹�' : '璇烽�夋嫨涓�鏉℃暟鎹�');
+ return;
+ }
+ emit('selected', multiple.value ? rows : [rows[0]!]);
+ open.value = false;
+}
+
+defineExpose({ open: openModal });
+</script>
+
+<template>
+ <Modal
+ v-model:open="open"
+ title="璁¢噺鍣ㄥ叿閫夋嫨"
+ width="70%"
+ :destroy-on-close="true"
+ @ok="handleConfirm"
+ @cancel="closeModal"
+ >
+ <Grid table-title="璁¢噺鍣ㄥ叿鍒楄〃" />
+ </Modal>
+</template>
diff --git a/src/views/mes/dv/metering/components/select.vue b/src/views/mes/dv/metering/components/select.vue
new file mode 100644
index 0000000..018e89d
--- /dev/null
+++ b/src/views/mes/dv/metering/components/select.vue
@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { computed, ref, useAttrs, watch } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Input, Tooltip } from 'ant-design-vue';
+
+import { getMetering } from '#/api/mes/dv/metering';
+
+import DvMeteringSelectDialog from './select-dialog.vue';
+
+defineOptions({ name: 'DvMeteringSelect', inheritAttrs: false });
+
+const props = withDefaults(
+ defineProps<{
+ allowClear?: boolean;
+ disabled?: boolean;
+ modelValue?: number;
+ placeholder?: string;
+ }>(),
+ {
+ allowClear: true,
+ disabled: false,
+ modelValue: undefined,
+ placeholder: '璇烽�夋嫨璁¢噺鍣ㄥ叿',
+ },
+);
+const emit = defineEmits<{
+ change: [item: MesDvMeteringApi.Metering | undefined];
+ 'update:modelValue': [value: number | undefined];
+}>();
+const attrs = useAttrs(); // 閫忎紶灞炴��
+const dialogRef = ref<InstanceType<typeof DvMeteringSelectDialog>>(); // 璁¢噺鍣ㄥ叿閫夋嫨寮圭獥
+const hovering = ref(false); // 鏄惁鎮仠
+const selectedItem = ref<MesDvMeteringApi.Metering>(); // 褰撳墠閫変腑鐨勮閲忓櫒鍏�
+
+const displayLabel = computed(() => selectedItem.value?.name ?? ''); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
+const showClear = computed(
+ () =>
+ props.allowClear &&
+ !props.disabled &&
+ hovering.value &&
+ props.modelValue !== null,
+);
+
+/** 鏍规嵁鍣ㄥ叿缂栧彿鍥炴樉閫夋嫨鍣� */
+async function resolveItemById(id: number | undefined) {
+ if (!id) {
+ selectedItem.value = undefined;
+ return;
+ }
+ if (selectedItem.value?.id === id) {
+ return;
+ }
+ selectedItem.value = await getMetering(id);
+}
+
+watch(
+ () => props.modelValue,
+ (value) => {
+ resolveItemById(value);
+ },
+ { immediate: true },
+);
+
+/** 娓呯┖宸查�夊櫒鍏� */
+function clearSelected() {
+ selectedItem.value = undefined;
+ emit('update:modelValue', undefined);
+ emit('change', undefined);
+}
+
+/** 鎵撳紑璁¢噺鍣ㄥ叿閫夋嫨寮圭獥 */
+function handleClick(event: MouseEvent) {
+ if (props.disabled) {
+ return;
+ }
+ const target = event.target as HTMLElement;
+ if (showClear.value && target.closest('.ant-input-suffix')) {
+ event.stopPropagation();
+ clearSelected();
+ return;
+ }
+ const selectedIds = (
+ props.modelValue === null ? [] : [props.modelValue]
+ ) as number[];
+ dialogRef.value?.open(selectedIds, { multiple: false });
+}
+
+/** 鍥炲~閫変腑鐨勮閲忓櫒鍏� */
+function handleSelected(rows: MesDvMeteringApi.Metering[]) {
+ const item = rows[0];
+ if (!item) {
+ return;
+ }
+ selectedItem.value = item;
+ emit('update:modelValue', item.id);
+ emit('change', item);
+}
+</script>
+
+<template>
+ <div
+ v-bind="attrs"
+ class="w-full"
+ :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
+ @click="handleClick"
+ @mouseenter="hovering = true"
+ @mouseleave="hovering = false"
+ >
+ <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+ <template #title>
+ <div v-if="selectedItem" class="leading-6">
+ <div>鍣ㄥ叿缂栫爜锛歿{ selectedItem.code || '-' }}</div>
+ <div>鍣ㄥ叿鍚嶇О锛歿{ selectedItem.name || '-' }}</div>
+ <div v-if="selectedItem.specification">
+ 瑙勬牸鍨嬪彿锛歿{ selectedItem.specification }}
+ </div>
+ <div v-if="selectedItem.deptName">
+ 浣跨敤閮ㄩ棬锛歿{ selectedItem.deptName }}
+ </div>
+ </div>
+ </template>
+ <Input
+ :disabled="disabled"
+ :placeholder="placeholder"
+ :value="displayLabel"
+ readonly
+ >
+ <template #suffix>
+ <IconifyIcon
+ class="size-4"
+ :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
+ />
+ </template>
+ </Input>
+ </Tooltip>
+ </div>
+ <DvMeteringSelectDialog ref="dialogRef" @selected="handleSelected" />
+</template>
diff --git a/src/views/mes/dv/metering/data.ts b/src/views/mes/dv/metering/data.ts
new file mode 100644
index 0000000..1f57561
--- /dev/null
+++ b/src/views/mes/dv/metering/data.ts
@@ -0,0 +1,437 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import {
+ DICT_TYPE,
+ MesDvMeteringExpireStatusEnum,
+ MesDvMeteringStatusEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+import { handleTree } from '@vben/utils';
+
+import { z } from '#/adapter/form';
+import { getSimpleDeptList } from '#/api/system/dept';
+import { getSimpleUserList } from '#/api/system/user';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鍒版湡鐘舵�佸睍绀洪厤缃� */
+export const METERING_EXPIRE_STATUS_OPTIONS: {
+ color: string;
+ label: string;
+ value: number;
+}[] = [
+ {
+ label: '宸茶繃鏈�',
+ value: MesDvMeteringExpireStatusEnum.OVERDUE,
+ color: 'red',
+ },
+ {
+ label: '鍗冲皢鍒版湡',
+ value: MesDvMeteringExpireStatusEnum.WITHIN,
+ color: 'orange',
+ },
+ {
+ label: '姝e父',
+ value: MesDvMeteringExpireStatusEnum.NORMAL,
+ color: 'green',
+ },
+];
+
+/** 鏂板/淇敼璁¢噺鍣ㄥ叿鐨勮〃鍗� */
+export function useFormSchema(formType: FormType): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '鍣ㄥ叿缂栫爜',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ櫒鍏风紪鐮�',
+ },
+ dependencies: {
+ triggerFields: ['id'],
+ componentProps: (values) => ({ disabled: !!values.id }),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'name',
+ label: '鍣ㄥ叿鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ櫒鍏峰悕绉�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'category',
+ label: '鍣ㄥ叿绫诲埆',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_DV_METERING_CATEGORY, 'number'),
+ placeholder: '璇烽�夋嫨鍣ㄥ叿绫诲埆',
+ },
+ help: '濡傚崱灏恒�佸帇鍔涜〃銆佹俯搴﹁绛夛紝涓嶅悓绫诲埆鏈夊父瑙佺殑鏍″噯鍛ㄦ湡',
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'specification',
+ label: '瑙勬牸鍨嬪彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ鏍煎瀷鍙�',
+ },
+ },
+ {
+ fieldName: 'brand',
+ label: '鍝佺墝/鐢熶骇鍘傚',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ搧鐗�/鐢熶骇鍘傚',
+ },
+ },
+ {
+ fieldName: 'precision',
+ label: '绮惧害',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ョ簿搴�',
+ },
+ help: '娴嬮噺绮惧害锛屽 0.01mm',
+ },
+ {
+ fieldName: 'range',
+ label: '閲忕▼',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ噺绋�',
+ },
+ help: '娴嬮噺鑼冨洿锛屽 0~150mm',
+ },
+ {
+ fieldName: 'deptId',
+ label: '浣跨敤閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ api: async () => handleTree(await getSimpleDeptList()),
+ childrenField: 'children',
+ labelField: 'name',
+ placeholder: '璇烽�夋嫨浣跨敤閮ㄩ棬',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'chargeUserId',
+ label: '璐d换浜�',
+ component: 'ApiSelect',
+ componentProps: {
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ placeholder: '璇烽�夋嫨璐d换浜�',
+ valueField: 'id',
+ },
+ help: '璇ュ櫒鍏风殑缁存姢璐d换浜猴紝鍒版湡鎻愰啋鐨勭珯鍐呬俊浼氬彂缁欎粬',
+ },
+ {
+ fieldName: 'checkCycleCount',
+ label: '鏍″噯鍛ㄦ湡',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 1,
+ precision: 0,
+ placeholder: '璇疯緭鍏ユ牎鍑嗗懆鏈�',
+ suffix: '鏈�',
+ },
+ help: '鍣ㄥ叿姣忛殧 N 涓湀椤绘牎鍑�/妫�瀹氫竴娆★紱鏂板妫�娴嬭褰曟椂涓嶅~涓嬫妫�娴嬫棩鏈燂紝灏嗘寜銆屾娴嬫棩鏈� + 鏍″噯鍛ㄦ湡銆嶈嚜鍔ㄨ绠�',
+ rules: 'required',
+ },
+ {
+ fieldName: 'useDate',
+ label: '鎶曞叆浣跨敤鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨鎶曞叆浣跨敤鏃ユ湡',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ help: '鍣ㄥ叿寮�濮嬫姇鍏ヤ娇鐢ㄧ殑鏃ユ湡锛堜粎浣滆褰曪級',
+ },
+ {
+ fieldName: 'lastCheckDate',
+ label: '涓婃妫�娴嬫棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ disabled: true,
+ format: 'YYYY-MM-DD',
+ placeholder: '涓婃妫�娴嬫棩鏈�',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ help: '鐢辨渶鏂颁竴鏉℃娴嬭褰曡嚜鍔ㄥ洖鍐欙紝涓嶅彲鎵嬪姩淇敼',
+ dependencies: {
+ triggerFields: ['id'],
+ show: () => formType === 'detail',
+ },
+ },
+ {
+ fieldName: 'nextCheckDate',
+ label: '涓嬫妫�娴嬫棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ disabled: true,
+ format: 'YYYY-MM-DD',
+ placeholder: '涓嬫妫�娴嬫棩鏈�',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ help: '鍒版湡鏃ワ紝鐢辨娴嬭褰曡嚜鍔ㄨ绠楀洖鍐欙紱鍒版湡鍓� N 澶╋紙鍩虹璁炬柦閰嶇疆锛夊皢鎻愰啋璐d换浜�',
+ dependencies: {
+ triggerFields: ['id'],
+ show: () => formType === 'detail',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鍣ㄥ叿鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ optionType: 'button',
+ options: getDictOptions(DICT_TYPE.MES_DV_METERING_STATUS, 'number'),
+ },
+ help: '鍦ㄧ敤/鍋滅敤/鎶ュ簾',
+ rules: z.number().default(MesDvMeteringStatusEnum.IN_USE),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '鍣ㄥ叿缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ櫒鍏风紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鍣ㄥ叿鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ櫒鍏峰悕绉�',
+ },
+ },
+ {
+ fieldName: 'category',
+ label: '鍣ㄥ叿绫诲埆',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_DV_METERING_CATEGORY, 'number'),
+ placeholder: '璇烽�夋嫨鍣ㄥ叿绫诲埆',
+ },
+ },
+ {
+ fieldName: 'deptId',
+ label: '浣跨敤閮ㄩ棬',
+ component: 'ApiTreeSelect',
+ componentProps: {
+ allowClear: true,
+ api: async () => handleTree(await getSimpleDeptList()),
+ childrenField: 'children',
+ labelField: 'name',
+ placeholder: '璇烽�夋嫨浣跨敤閮ㄩ棬',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'chargeUserId',
+ label: '璐d换浜�',
+ component: 'ApiSelect',
+ componentProps: {
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ placeholder: '璇烽�夋嫨璐d换浜�',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鍣ㄥ叿鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_DV_METERING_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鍣ㄥ叿鐘舵��',
+ },
+ },
+ {
+ fieldName: 'expireStatus',
+ label: '鍒版湡鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: METERING_EXPIRE_STATUS_OPTIONS,
+ placeholder: '璇烽�夋嫨鍒版湡鐘舵��',
+ },
+ help: '涓嬫妫�娴嬫棩鏈熻窛浠婂ぉ 鈮� 鎻愰啋澶╂暟锛堝熀纭�璁炬柦閰嶇疆 mes.dv-metering.remind-days锛夊嵆瑙嗕负銆屽嵆灏嗗埌鏈熴��',
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesDvMeteringApi.Metering>['columns'] {
+ return [
+ {
+ field: 'code',
+ title: '鍣ㄥ叿缂栫爜',
+ minWidth: 150,
+ slots: {
+ default: 'code',
+ },
+ },
+ { field: 'name', title: '鍣ㄥ叿鍚嶇О', minWidth: 150 },
+ {
+ field: 'category',
+ title: '鍣ㄥ叿绫诲埆',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_DV_METERING_CATEGORY },
+ },
+ },
+ { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 130 },
+ { field: 'brand', title: '鍝佺墝/鐢熶骇鍘傚', minWidth: 130 },
+ { field: 'deptName', title: '浣跨敤閮ㄩ棬', minWidth: 130 },
+ { field: 'chargeUserNickname', title: '璐d换浜�', minWidth: 100 },
+ {
+ field: 'expireStatus',
+ title: '鍒版湡鐘舵��',
+ width: 110,
+ slots: {
+ default: 'expireStatus',
+ },
+ },
+ {
+ field: 'nextCheckDate',
+ title: '涓嬫妫�娴嬫棩鏈�',
+ width: 130,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'status',
+ title: '鍣ㄥ叿鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_DV_METERING_STATUS },
+ },
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 210,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
+
+/** 璁¢噺鍣ㄥ叿閫夋嫨寮圭獥鐨勬悳绱㈣〃鍗� */
+export function useMeteringSelectGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '鍣ㄥ叿缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ櫒鍏风紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鍣ㄥ叿鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ櫒鍏峰悕绉�',
+ },
+ },
+ ];
+}
+
+/** 璁¢噺鍣ㄥ叿閫夋嫨寮圭獥鐨勫瓧娈� */
+export function useMeteringSelectGridColumns(
+ multiple = false,
+): VxeTableGridOptions<MesDvMeteringApi.Metering>['columns'] {
+ return [
+ { type: multiple ? 'checkbox' : 'radio', width: 50 },
+ { field: 'code', title: '鍣ㄥ叿缂栫爜', width: 140 },
+ { field: 'name', title: '鍣ㄥ叿鍚嶇О', minWidth: 140 },
+ {
+ field: 'category',
+ title: '鍣ㄥ叿绫诲埆',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_DV_METERING_CATEGORY },
+ },
+ },
+ { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 120 },
+ { field: 'deptName', title: '浣跨敤閮ㄩ棬', width: 120 },
+ { field: 'chargeUserNickname', title: '璐d换浜�', width: 100 },
+ {
+ field: 'nextCheckDate',
+ title: '涓嬫妫�娴嬫棩鏈�',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'status',
+ title: '鍣ㄥ叿鐘舵��',
+ width: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_DV_METERING_STATUS },
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/dv/metering/index.vue b/src/views/mes/dv/metering/index.vue
new file mode 100644
index 0000000..eddc3a1
--- /dev/null
+++ b/src/views/mes/dv/metering/index.vue
@@ -0,0 +1,195 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteMetering,
+ exportMetering,
+ getMeteringPage,
+} from '#/api/mes/dv/metering';
+import { $t } from '#/locales';
+
+import {
+ METERING_EXPIRE_STATUS_OPTIONS,
+ useGridColumns,
+ useGridFormSchema,
+} from './data';
+import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 瀵煎叆璁¢噺鍣ㄥ叿 */
+function handleImport() {
+ importModalApi.open();
+}
+
+/** 鍒涘缓璁¢噺鍣ㄥ叿 */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 鏌ョ湅璁¢噺鍣ㄥ叿 */
+function handleDetail(row: MesDvMeteringApi.Metering) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 缂栬緫璁¢噺鍣ㄥ叿 */
+function handleEdit(row: MesDvMeteringApi.Metering) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 鍒犻櫎璁¢噺鍣ㄥ叿 */
+async function handleDelete(row: MesDvMeteringApi.Metering) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.name]),
+ duration: 0,
+ });
+ try {
+ await deleteMetering(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 瀵煎嚭璁¢噺鍣ㄥ叿 */
+async function handleExport() {
+ const data = await exportMetering(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '璁¢噺鍣ㄥ叿.xls', source: data });
+}
+
+/** 鍒版湡鐘舵�佸睍绀� */
+function getExpireStatusConfig(status?: number) {
+ return METERING_EXPIRE_STATUS_OPTIONS.find((item) => item.value === status);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getMeteringPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesDvMeteringApi.Metering>,
+});
+</script>
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
+ <Grid table-title="璁¢噺鍣ㄥ叿鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['璁¢噺鍣ㄥ叿']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:dv-metering:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:dv-metering:export'],
+ onClick: handleExport,
+ },
+ {
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['mes:dv-metering:import'],
+ onClick: handleImport,
+ },
+ ]"
+ />
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.code }}
+ </Button>
+ </template>
+ <template #expireStatus="{ row }">
+ <Tag
+ v-if="getExpireStatusConfig(row.expireStatus)"
+ :color="getExpireStatusConfig(row.expireStatus)!.color"
+ >
+ {{ getExpireStatusConfig(row.expireStatus)!.label }}
+ </Tag>
+ <span v-else>-</span>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:dv-metering:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:dv-metering:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:dv-metering:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/dv/metering/modules/form.vue b/src/views/mes/dv/metering/modules/form.vue
new file mode 100644
index 0000000..62535f4
--- /dev/null
+++ b/src/views/mes/dv/metering/modules/form.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesDvMeteringApi } from '#/api/mes/dv/metering';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createMetering,
+ getMetering,
+ updateMetering,
+} from '#/api/mes/dv/metering';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesDvMeteringApi.Metering>();
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅璁¢噺鍣ㄥ叿';
+ }
+ return formType.value === 'update' ? '淇敼璁¢噺鍣ㄥ叿' : '鏂板璁¢噺鍣ㄥ叿';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as MesDvMeteringApi.Metering;
+ try {
+ if (formType.value === 'create') {
+ await createMetering(data);
+ } else {
+ await updateMetering(data);
+ }
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema(formType.value) });
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getMetering(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/dv/metering/modules/import-form.vue b/src/views/mes/dv/metering/modules/import-form.vue
new file mode 100644
index 0000000..a9e91bc
--- /dev/null
+++ b/src/views/mes/dv/metering/modules/import-form.vue
@@ -0,0 +1,155 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { ImportResult } from '#/api/mes/dv/metering';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import { importMetering, importMeteringTemplate } from '#/api/mes/dv/metering';
+
+const emit = defineEmits(['success']);
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importMetering(fileList.value[0] as File);
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureList ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭柊澧� ${createCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importMeteringTemplate();
+ downloadFileFromBlobPart({ fileName: '璁¢噺鍣ㄥ叿瀵煎叆妯℃澘.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createList?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateList?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureList ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆璁¢噺鍣ㄥ叿" class="w-[720px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div class="mt-2 rounded-md bg-gray-50 p-2 text-xs leading-5 text-gray-500">
+ <p>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢銆傚繀濉垪锛氳閲忓櫒鍏风紪鐮併�佽閲忓櫒鍏峰悕绉般�佸櫒鍏风被鍒�佺姸鎬侊紱鈥滃櫒鍏风被鍒� / 鐘舵�佲�濆~瀛楀吀涓枃锛堝 鍗″昂/鍏朵粬銆佸湪鐢�/鍋滅敤/鎶ュ簾锛夈��</p>
+ <p>鈥滀娇鐢ㄩ儴闂� / 璐d换浜衡�濇寜绯荤粺鍐呯殑閮ㄩ棬鍚嶇О / 鐢ㄦ埛鏄电О鍖归厤锛屽尮閰嶄笉鍒拌琛屼細澶辫触锛涒�滄牎鍑嗗懆鏈�(鏈�)鈥濈暀绌洪粯璁� 12锛涒�滄姇鍏ヤ娇鐢ㄦ棩鏈熲�濇牸寮忓 2026-01-01銆�</p>
+ <p>璁¢噺鍣ㄥ叿缂栫爜宸插瓨鍦ㄦ椂浼氬鍏ュけ璐ワ紙浠呮柊澧烇紝涓嶈鐩栵級銆�</p>
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆璁¢噺鍣ㄥ叿鏁版嵁锛岃绋嶅��...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex gap-4">
+ <span class="text-green-600">鏂板锛歿{ createCount }} 鏉�</span>
+ <span class="text-blue-600">瑕嗙洊鏇存柊锛歿{ updateCount }} 鏉�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 鏉�</span>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureList"
+ :key="name"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/dv/meteringcheck/data.ts b/src/views/mes/dv/meteringcheck/data.ts
new file mode 100644
index 0000000..4e1e4e9
--- /dev/null
+++ b/src/views/mes/dv/meteringcheck/data.ts
@@ -0,0 +1,218 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
+
+import { markRaw } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import { getSimpleUserList } from '#/api/system/user';
+import { DvMeteringSelect } from '#/views/mes/dv/metering/components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼璁¢噺妫�娴嬭褰曠殑琛ㄥ崟 */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'meteringId',
+ label: '璁¢噺鍣ㄥ叿',
+ component: markRaw(DvMeteringSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨璁¢噺鍣ㄥ叿',
+ },
+ help: '閫夋嫨鍣ㄥ叿鍚庯紝鍏舵牎鍑嗗懆鏈熷皢鐢ㄤ簬鑷姩璁$畻涓嬫妫�娴嬫棩鏈�',
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'checkCode',
+ label: '妫�娴嬬紪鍙�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ娴嬬紪鍙�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'checkDate',
+ label: '妫�娴嬫棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨妫�娴嬫棩鏈�',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'checkOrg',
+ label: '妫�娴嬫満鏋�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ユ娴嬫満鏋�',
+ },
+ },
+ {
+ fieldName: 'checkResult',
+ label: '妫�娴嬬粨璁�',
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ optionType: 'button',
+ options: getDictOptions(DICT_TYPE.MES_DV_METERING_CHECK_RESULT, 'number'),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'certNo',
+ label: '璇佷功缂栧彿',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ヨ瘉涔︾紪鍙�',
+ },
+ },
+ {
+ fieldName: 'checkUserId',
+ label: '妫�娴嬩汉',
+ component: 'ApiSelect',
+ componentProps: {
+ allowClear: true,
+ api: getSimpleUserList,
+ labelField: 'nickname',
+ placeholder: '璇烽�夋嫨妫�娴嬩汉',
+ valueField: 'id',
+ },
+ },
+ {
+ fieldName: 'nextCheckDate',
+ label: '涓嬫妫�娴嬫棩鏈�',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD',
+ placeholder: '璇烽�夋嫨涓嬫妫�娴嬫棩鏈�',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ help: '鍒版湡鏃ワ紝寤鸿涓嶅~锛氬皢鎸夈�屾娴嬫棩鏈� + 鏍″噯鍛ㄦ湡銆嶈嚜鍔ㄨ绠楀苟鍥炲啓鍒板櫒鍏凤紱涔熷彲鎵嬪姩鎸囧畾',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'meteringId',
+ label: '璁¢噺鍣ㄥ叿',
+ component: markRaw(DvMeteringSelect),
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇烽�夋嫨璁¢噺鍣ㄥ叿',
+ },
+ },
+ {
+ fieldName: 'checkCode',
+ label: '妫�娴嬬紪鍙�',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ユ娴嬬紪鍙�',
+ },
+ },
+ {
+ fieldName: 'checkResult',
+ label: '妫�娴嬬粨璁�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_DV_METERING_CHECK_RESULT, 'number'),
+ placeholder: '璇烽�夋嫨妫�娴嬬粨璁�',
+ },
+ },
+ {
+ fieldName: 'checkDate',
+ label: '妫�娴嬫棩鏈�',
+ component: 'RangePicker',
+ componentProps: {
+ allowClear: true,
+ format: 'YYYY-MM-DD',
+ valueFormat: 'YYYY-MM-DD',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesDvMeteringCheckApi.MeteringCheck>['columns'] {
+ return [
+ {
+ field: 'meteringCode',
+ title: '鍣ㄥ叿缂栫爜',
+ minWidth: 140,
+ slots: {
+ default: 'meteringCode',
+ },
+ },
+ { field: 'meteringName', title: '鍣ㄥ叿鍚嶇О', minWidth: 150 },
+ { field: 'checkCode', title: '妫�娴嬬紪鍙�', minWidth: 140 },
+ {
+ field: 'checkDate',
+ title: '妫�娴嬫棩鏈�',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ { field: 'checkOrg', title: '妫�娴嬫満鏋�', minWidth: 140 },
+ {
+ field: 'checkResult',
+ title: '妫�娴嬬粨璁�',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_DV_METERING_CHECK_RESULT },
+ },
+ },
+ { field: 'certNo', title: '璇佷功缂栧彿', minWidth: 130 },
+ { field: 'checkUserNickname', title: '妫�娴嬩汉', minWidth: 100 },
+ {
+ field: 'nextCheckDate',
+ title: '涓嬫妫�娴嬫棩鏈�',
+ width: 130,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/dv/meteringcheck/index.vue b/src/views/mes/dv/meteringcheck/index.vue
new file mode 100644
index 0000000..c8039f3
--- /dev/null
+++ b/src/views/mes/dv/meteringcheck/index.vue
@@ -0,0 +1,158 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteMeteringCheck,
+ exportMeteringCheck,
+ getMeteringCheckPage,
+} from '#/api/mes/dv/meteringcheck';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鍒涘缓璁¢噺妫�娴嬭褰� */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 鏌ョ湅璁¢噺妫�娴嬭褰� */
+function handleDetail(row: MesDvMeteringCheckApi.MeteringCheck) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 缂栬緫璁¢噺妫�娴嬭褰� */
+function handleEdit(row: MesDvMeteringCheckApi.MeteringCheck) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 鍒犻櫎璁¢噺妫�娴嬭褰� */
+async function handleDelete(row: MesDvMeteringCheckApi.MeteringCheck) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.checkCode]),
+ duration: 0,
+ });
+ try {
+ await deleteMeteringCheck(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.checkCode]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 瀵煎嚭璁¢噺妫�娴嬭褰� */
+async function handleExport() {
+ const data = await exportMeteringCheck(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '璁¢噺妫�娴嬭褰�.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) =>
+ await getMeteringCheckPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ }),
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesDvMeteringCheckApi.MeteringCheck>,
+});
+</script>
+<template>
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <Grid table-title="璁¢噺妫�娴嬭褰曞垪琛�">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['妫�娴嬭褰�']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:dv-metering-check:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:dv-metering-check:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #meteringCode="{ row }">
+ <Button type="link" @click="handleDetail(row)">
+ {{ row.meteringCode }}
+ </Button>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:dv-metering-check:update'],
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:dv-metering-check:delete'],
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.checkCode]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ {
+ label: $t('common.detail'),
+ type: 'link',
+ icon: ACTION_ICON.VIEW,
+ auth: ['mes:dv-metering-check:query'],
+ onClick: handleDetail.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/dv/meteringcheck/modules/form.vue b/src/views/mes/dv/meteringcheck/modules/form.vue
new file mode 100644
index 0000000..308d312
--- /dev/null
+++ b/src/views/mes/dv/meteringcheck/modules/form.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesDvMeteringCheckApi } from '#/api/mes/dv/meteringcheck';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createMeteringCheck,
+ getMeteringCheck,
+ updateMeteringCheck,
+} from '#/api/mes/dv/meteringcheck';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesDvMeteringCheckApi.MeteringCheck>();
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅璁¢噺妫�娴嬭褰�';
+ }
+ return formType.value === 'update' ? '淇敼璁¢噺妫�娴嬭褰�' : '鏂板璁¢噺妫�娴嬭褰�';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as MesDvMeteringCheckApi.MeteringCheck;
+ try {
+ if (formType.value === 'create') {
+ await createMeteringCheck(data);
+ } else {
+ await updateMeteringCheck(data);
+ }
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useFormSchema() });
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getMeteringCheck(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/md/item/components/product-bom-select-dialog.vue b/src/views/mes/md/item/components/product-bom-select-dialog.vue
index 9718d32..64eeccb 100644
--- a/src/views/mes/md/item/components/product-bom-select-dialog.vue
+++ b/src/views/mes/md/item/components/product-bom-select-dialog.vue
@@ -13,26 +13,115 @@
defineOptions({ name: 'MdProductBomSelectDialog' });
+const props = withDefaults(
+ defineProps<{
+ multiple?: boolean;
+ }>(),
+ {
+ multiple: false,
+ },
+);
+
const emit = defineEmits<{
- selected: [row: MesMdProductBomApi.ProductBom];
+ selected: [rows: MesMdProductBomApi.ProductBom[]];
}>();
const open = ref(false); // 寮圭獥鏄惁鎵撳紑
const list = ref<MesMdProductBomApi.ProductBom[]>([]); // BOM 瀛愮墿鏂欏垪琛�
-const selectedRow = ref<MesMdProductBomApi.ProductBom>(); // 褰撳墠閫変腑鐨� BOM 琛�
+const selectedRows = ref<MesMdProductBomApi.ProductBom[]>([]); // 宸查�� BOM 琛岋紙鍗曢�夋椂鏈�澶氫竴琛岋級
+const preSelectedIds = ref<number[]>([]); // 棰勯�� BOM 鐗╂枡缂栧彿
+
+/** 鑾峰彇澶氶�夎褰曪紝鍖呭惈 VXE reserve 璁板綍 */
+function getMultipleSelectedRows() {
+ const selectedMap = new Map<number, MesMdProductBomApi.ProductBom>();
+ const records = [
+ ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
+ ...(gridApi.grid.getCheckboxRecords?.() ?? []),
+ ] as MesMdProductBomApi.ProductBom[];
+ records.forEach((row) => {
+ if (row.bomItemId != null) {
+ selectedMap.set(row.bomItemId, row);
+ }
+ });
+ return [...selectedMap.values()];
+}
+
+/** 澶勭悊鍕鹃�夊彉鍖� */
+function handleCheckboxSelectChange() {
+ selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 涓婂懆鍗曞嚮琛岀棔杩癸紝鐢ㄤ簬鍚炴帀鍙屽嚮瑙﹀彂鐨勭浜屾 click */
+let lastClick = { bomItemId: undefined as number | undefined, time: 0 };
+
+/** 鍗曞嚮琛岋細澶氶�夊垏鎹㈠嬀閫夛紱鍗曢�夌敱 radioConfig.trigger='row' 澶勭悊 */
+async function handleCellClick({
+ row,
+ column,
+}: {
+ row: MesMdProductBomApi.ProductBom;
+ column?: { type?: string };
+}) {
+ // 鐐瑰嚮鍕鹃��/鍗曢�夊垪鑷韩鐢辨帶浠跺鐞嗭紝閬垮厤浜屾鍒囨崲
+ if (column?.type === 'checkbox' || column?.type === 'radio') {
+ return;
+ }
+ if (!props.multiple) {
+ return;
+ }
+ const now = Date.now();
+ if (lastClick.bomItemId === row.bomItemId && now - lastClick.time < 300) {
+ lastClick.time = 0;
+ return;
+ }
+ lastClick = { bomItemId: row.bomItemId, time: now };
+ const selected = gridApi.grid.isCheckedByCheckboxRow(row);
+ await gridApi.grid.setCheckboxRow(row, !selected);
+ selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊琛屽弻鍑� */
+async function handleCellDblclick({
+ row,
+}: {
+ row: MesMdProductBomApi.ProductBom;
+}) {
+ if (props.multiple) {
+ return; // 澶氶�夌敱鍗曞嚮 cellClick 鍒囨崲锛屽弻鍑讳笉鍐嶉噸澶嶅鐞�
+ }
+ selectedRows.value = [row];
+ gridApi.grid.setRadioRow(row);
+ handleConfirm();
+}
+
+/** 澶勭悊鍗曢�夊彉鍖� */
+function handleRadioChange(row: MesMdProductBomApi.ProductBom) {
+ selectedRows.value = [row];
+}
+
+/** 褰撳墠閫夋嫨鍒楃被鍨� */
+function getSelectColumn() {
+ return {
+ type: props.multiple ? 'checkbox' : 'radio',
+ width: 55,
+ align: 'center',
+ } as const;
+}
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
border: true,
- columns: [
- { type: 'radio', width: 55, align: 'center' },
- ...(useProductBomGridColumns(true) || []),
- ],
+ columns: [getSelectColumn(), ...(useProductBomGridColumns(true) || [])],
data: list.value,
height: 500,
keepSource: true,
pagerConfig: {
enabled: false,
+ },
+ checkboxConfig: {
+ highlight: true,
+ range: true,
+ reserve: true,
},
radioConfig: {
highlight: true,
@@ -48,35 +137,62 @@
},
} as VxeTableGridOptions<MesMdProductBomApi.ProductBom>,
gridEvents: {
- cellDblclick: ({ row }: { row: MesMdProductBomApi.ProductBom }) => {
- selectedRow.value = row;
- gridApi.grid.setRadioRow(row);
- handleConfirm();
- },
+ cellClick: handleCellClick,
+ cellDblclick: handleCellDblclick,
+ checkboxChange: handleCheckboxSelectChange,
+ checkboxAll: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesMdProductBomApi.ProductBom }) => {
- selectedRow.value = row;
+ handleRadioChange(row);
},
},
});
-/** 鎵撳紑 BOM 鐗╂枡閫夋嫨寮圭獥 */
-async function openModal(itemId: number, selectedBomItemId?: number) {
+/** 鍥炴樉棰勯�� BOM 鐗╂枡锛堟暟鎹竴娆℃�у姞杞斤紝鏃犲垎椤碉紝鐩存帴鍕鹃�夊嵆鍙級 */
+async function applyPreSelection() {
+ if (preSelectedIds.value.length === 0) {
+ return;
+ }
+ for (const row of list.value) {
+ if (
+ row.bomItemId === undefined ||
+ row.bomItemId === null ||
+ !preSelectedIds.value.includes(row.bomItemId)
+ ) {
+ continue;
+ }
+ if (props.multiple) {
+ await gridApi.grid.setCheckboxRow(row, true);
+ } else {
+ await gridApi.grid.setRadioRow(row);
+ selectedRows.value = [row];
+ return;
+ }
+ }
+ if (props.multiple) {
+ selectedRows.value = getMultipleSelectedRows();
+ }
+}
+
+/** 鎵撳紑 BOM 鐗╂枡閫夋嫨寮圭獥锛坰electedBomItemIds 鍗曢�変紶鍗曚釜缂栧彿锛屽閫変紶缂栧彿鏁扮粍锛� */
+async function openModal(itemId: number, selectedBomItemIds?: number | number[]) {
open.value = true;
- selectedRow.value = undefined;
+ preSelectedIds.value = (
+ Array.isArray(selectedBomItemIds)
+ ? selectedBomItemIds
+ : selectedBomItemIds === undefined || selectedBomItemIds === null
+ ? []
+ : [selectedBomItemIds]
+ ).filter((id): id is number => id != null);
+ selectedRows.value = [];
gridApi.setLoading(true);
try {
list.value = await getProductBomListByItemId(itemId);
- gridApi.setGridOptions({ data: list.value });
+ gridApi.setGridOptions({
+ data: list.value,
+ columns: [getSelectColumn(), ...(useProductBomGridColumns(true) || [])],
+ });
await nextTick();
- if (selectedBomItemId !== null) {
- const match = list.value.find(
- (row) => row.bomItemId === selectedBomItemId,
- );
- if (match) {
- selectedRow.value = match;
- gridApi.grid.setRadioRow(match);
- }
- }
+ await applyPreSelection();
} finally {
gridApi.setLoading(false);
}
@@ -85,17 +201,23 @@
/** 鍏抽棴 BOM 鐗╂枡閫夋嫨寮圭獥 */
async function closeModal() {
open.value = false;
- selectedRow.value = undefined;
+ selectedRows.value = [];
+ preSelectedIds.value = [];
+ await gridApi.grid.clearCheckboxRow();
+ await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
}
/** 纭閫夋嫨 BOM 鐗╂枡 */
function handleConfirm() {
- if (!selectedRow.value) {
- message.warning('璇烽�夋嫨涓�鏉℃暟鎹�');
+ const rows = props.multiple
+ ? getMultipleSelectedRows()
+ : selectedRows.value.slice(0, 1);
+ if (rows.length === 0) {
+ message.warning(props.multiple ? '璇疯嚦灏戦�夋嫨涓�鏉℃暟鎹�' : '璇烽�夋嫨涓�鏉℃暟鎹�');
return;
}
- emit('selected', selectedRow.value);
+ emit('selected', rows);
open.value = false;
}
@@ -113,4 +235,4 @@
>
<Grid table-title="BOM 瀛愮墿鏂欏垪琛�" />
</Modal>
-</template>
+</template>
\ No newline at end of file
diff --git a/src/views/mes/md/item/components/product-bom-select.vue b/src/views/mes/md/item/components/product-bom-select.vue
index fb056f2..2637585 100644
--- a/src/views/mes/md/item/components/product-bom-select.vue
+++ b/src/views/mes/md/item/components/product-bom-select.vue
@@ -18,7 +18,8 @@
allowClear?: boolean;
disabled?: boolean;
itemId?: number;
- modelValue?: number;
+ modelValue?: number | number[];
+ multiple?: boolean;
placeholder?: string;
}>(),
{
@@ -26,50 +27,68 @@
disabled: false,
itemId: undefined,
modelValue: undefined,
+ multiple: false,
placeholder: '璇烽�夋嫨 BOM 鐗╂枡',
},
);
const emit = defineEmits<{
- change: [bom: MesMdProductBomApi.ProductBom | undefined];
- 'update:modelValue': [value: number | undefined];
+ change: [
+ bom: MesMdProductBomApi.ProductBom[] | MesMdProductBomApi.ProductBom | undefined,
+ ];
+ 'update:modelValue': [value: number[] | number | undefined];
}>();
const attrs = useAttrs(); // 閫忎紶灞炴��
const dialogRef = ref<InstanceType<typeof MdProductBomSelectDialog>>(); // BOM 鐗╂枡閫夋嫨寮圭獥
const hovering = ref(false); // 鏄惁鎮仠
-const selectedBom = ref<MesMdProductBomApi.ProductBom>(); // 褰撳墠閫変腑鐨� BOM 鐗╂枡
+const selectedBoms = ref<MesMdProductBomApi.ProductBom[]>([]); // 宸查�� BOM 鐗╂枡锛堝崟閫夋椂鏈�澶氫竴椤癸級
-const displayLabel = computed(() => selectedBom.value?.bomItemName ?? ''); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
-const showClear = computed(
- () =>
- props.allowClear &&
- !props.disabled &&
- hovering.value &&
- props.modelValue !== undefined &&
- props.modelValue !== null,
-);
-
-/** 鏍规嵁 BOM 瀛愮墿鏂欑紪鍙峰洖鏄鹃�夋嫨鍣� */
-async function resolveBomById(bomItemId: number | undefined) {
- if (
- bomItemId === undefined ||
- bomItemId === null ||
- props.itemId === undefined ||
- props.itemId === null
- ) {
- selectedBom.value = undefined;
- return;
+const selectedBom = computed(() => selectedBoms.value[0]); // 鍗曢�夋椂鐨勫綋鍓嶉」
+const displayLabel = computed(() => {
+ // 澶氶�夊睍绀恒�屽悕绉般�佸悕绉般�嶏紝鍗曢�夊睍绀哄崟涓悕绉�
+ if (props.multiple) {
+ return selectedBoms.value
+ .map((bom) => bom.bomItemName)
+ .filter(Boolean)
+ .join('銆�');
}
- if (selectedBom.value?.bomItemId === bomItemId) {
+ return selectedBom.value?.bomItemName ?? '';
+}); // 閫夋嫨鍣ㄥ睍绀哄悕绉�
+const showClear = computed(() => {
+ if (!props.allowClear || props.disabled || !hovering.value) {
+ return false;
+ }
+ if (props.multiple) {
+ return Array.isArray(props.modelValue) && props.modelValue.length > 0;
+ }
+ return props.modelValue !== undefined && props.modelValue !== null;
+});
+
+/** 浠� modelValue 瑙f瀽鍑� BOM 鐗╂枡缂栧彿鍒楄〃 */
+function getModelIds() {
+ if (props.multiple) {
+ return Array.isArray(props.modelValue)
+ ? props.modelValue.filter((id): id is number => id != null)
+ : [];
+ }
+ return typeof props.modelValue === 'number' ? [props.modelValue] : [];
+}
+
+/** 鏍规嵁 BOM 鐗╂枡缂栧彿鍥炴樉閫夋嫨鍣� */
+async function resolveBomsById(ids: number[]) {
+ if (ids.length === 0 || props.itemId === undefined || props.itemId === null) {
+ selectedBoms.value = [];
return;
}
const list = await getProductBomListByItemId(props.itemId as number);
- selectedBom.value = list.find((item) => item.bomItemId === bomItemId);
+ selectedBoms.value = list.filter(
+ (item) => item.bomItemId != null && ids.includes(item.bomItemId),
+ );
}
watch(
() => props.modelValue,
- (value) => {
- resolveBomById(value);
+ () => {
+ resolveBomsById(getModelIds());
},
{ immediate: true },
);
@@ -77,17 +96,17 @@
watch(
() => props.itemId,
() => {
- selectedBom.value = undefined;
- emit('update:modelValue', undefined);
- emit('change', undefined);
+ selectedBoms.value = [];
+ emit('update:modelValue', props.multiple ? [] : undefined);
+ emit('change', props.multiple ? [] : undefined);
},
);
/** 娓呯┖宸查�� BOM 鐗╂枡 */
function clearSelected() {
- selectedBom.value = undefined;
- emit('update:modelValue', undefined);
- emit('change', undefined);
+ selectedBoms.value = [];
+ emit('update:modelValue', props.multiple ? [] : undefined);
+ emit('change', props.multiple ? [] : undefined);
}
/** 鎵撳紑 BOM 鐗╂枡閫夋嫨寮圭獥 */
@@ -101,14 +120,22 @@
clearSelected();
return;
}
- dialogRef.value?.open(props.itemId as number, props.modelValue);
+ const ids = getModelIds();
+ dialogRef.value?.open(props.itemId as number, props.multiple ? ids : ids[0]);
}
/** 鍥炲~閫変腑鐨� BOM 鐗╂枡 */
-function handleSelected(row: MesMdProductBomApi.ProductBom) {
- selectedBom.value = row;
- emit('update:modelValue', row.bomItemId);
- emit('change', row);
+function handleSelected(rows: MesMdProductBomApi.ProductBom[]) {
+ selectedBoms.value = rows;
+ emit(
+ 'update:modelValue',
+ props.multiple
+ ? rows
+ .map((row) => row.bomItemId)
+ .filter((id): id is number => id != null)
+ : rows[0]?.bomItemId,
+ );
+ emit('change', props.multiple ? rows : rows[0]);
}
</script>
@@ -121,9 +148,14 @@
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
- <Tooltip :mouse-enter-delay="0.5" :open="selectedBom ? undefined : false">
+ <Tooltip :mouse-enter-delay="0.5" :open="selectedBoms.length ? undefined : false">
<template #title>
- <div v-if="selectedBom" class="leading-6">
+ <div v-if="multiple" class="leading-6">
+ <div v-for="bom in selectedBoms" :key="bom.bomItemId">
+ {{ bom.bomItemName || '-' }}{{ bom.bomItemCode ? `锛�${bom.bomItemCode}锛塦 : '' }}
+ </div>
+ </div>
+ <div v-else-if="selectedBom" class="leading-6">
<div>缂栫爜锛歿{ selectedBom.bomItemCode || '-' }}</div>
<div>鍚嶇О锛歿{ selectedBom.bomItemName || '-' }}</div>
<div>瑙勬牸锛歿{ selectedBom.bomItemSpecification || '-' }}</div>
@@ -146,5 +178,9 @@
</Input>
</Tooltip>
</div>
- <MdProductBomSelectDialog ref="dialogRef" @selected="handleSelected" />
-</template>
+ <MdProductBomSelectDialog
+ ref="dialogRef"
+ :multiple="multiple"
+ @selected="handleSelected"
+ />
+</template>
\ No newline at end of file
diff --git a/src/views/mes/md/item/components/select-dialog.vue b/src/views/mes/md/item/components/select-dialog.vue
index d377f66..61f3fd1 100644
--- a/src/views/mes/md/item/components/select-dialog.vue
+++ b/src/views/mes/md/item/components/select-dialog.vue
@@ -67,11 +67,38 @@
selectedRows.value = getMultipleSelectedRows();
}
+ /** 涓婃鍗曞嚮琛岀棔杩癸紝鐢ㄤ簬鍚炴帀鍙屽嚮瑙﹀彂鐨勭浜屾 click锛岄伩鍏�"閫変簡鍙堝彇娑�" */
+ let lastClick = { rowId: undefined as number | undefined, time: 0 };
+
+ /** 鍗曞嚮琛岋細澶氶�夌洿鎺ュ垏鎹㈠嬀閫夛紱鍗曢�夌敱 radioConfig.trigger='row' 澶勭悊 */
+ async function handleCellClick({
+ row,
+ column,
+ }: {
+ row: MesMdItemApi.Item;
+ column?: { type?: string };
+ }) {
+ // 鐐瑰嚮鍕鹃�夊垪鑷韩鐢� checkbox 鎺т欢澶勭悊锛岄伩鍏嶄簩娆″垏鎹�
+ if (column?.type === "checkbox") {
+ return;
+ }
+ if (!multiple.value) {
+ return;
+ }
+ const rowId = row.id;
+ const now = Date.now();
+ if (lastClick.rowId === rowId && now - lastClick.time < 300) {
+ lastClick.time = 0;
+ return;
+ }
+ lastClick = { rowId, time: now };
+ await toggleMultipleRow(row);
+ }
+
/** 澶勭悊琛屽弻鍑� */
async function handleCellDblclick({ row }: { row: MesMdItemApi.Item }) {
if (multiple.value) {
- await toggleMultipleRow(row);
- return;
+ return; // 澶氶�夌敱鍗曞嚮 cellClick 鍒囨崲锛屽弻鍑讳笉鍐嶉噸澶嶅鐞�
}
selectedRows.value = [row];
await gridApi.grid.setRadioRow(row);
@@ -84,18 +111,20 @@
gridApi.query();
}
- /** 鍥炴樉棰勯�夌墿鏂� */
+ /** 鍥炴樉棰勯�夌墿鏂欙細褰撳墠椤电洿鎺ュ嬀閫夛紝璺ㄩ〉棰勯�夋敞鍐屼负 reserve锛岀炕椤靛悗鑷姩鍥炴樉 */
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = getTableRows();
+ const currentIds = new Set(
+ rows.filter((row) => row.id != null).map((row) => row.id),
+ );
for (const row of rows) {
- if (
- row.id === undefined ||
- row.id === null ||
- !preSelectedIds.value.includes(row.id)
- ) {
+ if (row.id === undefined || row.id === null) {
+ continue;
+ }
+ if (!preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
@@ -107,6 +136,21 @@
}
}
if (multiple.value) {
+ // 鎷夊彇涓嶅湪褰撳墠椤电殑棰勯�夌墿鏂欏畬鏁存暟鎹紝娉ㄥ唽涓� checkbox reserve锛堢炕椤靛悗鑷姩鍕鹃�夛級
+ const missingIds = preSelectedIds.value.filter(
+ (id) => !currentIds.has(id),
+ );
+ if (missingIds.length > 0) {
+ const result = await getItemPage({
+ pageNo: 1,
+ pageSize: 200,
+ ids: missingIds.join(","),
+ } as never);
+ const fullRows = result?.list as MesMdItemApi.Item[] | undefined;
+ if (fullRows && fullRows.length > 0) {
+ await gridApi.grid.setCheckboxReserve(fullRows, true);
+ }
+ }
selectedRows.value = getMultipleSelectedRows();
}
}
@@ -140,7 +184,8 @@
};
// 闄愬畾鍙�夌墿鏂欒寖鍥�
if (productIds.value.length > 0) {
- params.pageSize = 1000;
+ // 鍚庣鍒嗛〉姣忛〉鏉℃暟涓婇檺涓� 200锛岀櫧鍚嶅崟鍦烘櫙涓�娆℃媺鍙栧敖閲忓鐨勫彲閫夌墿鏂�
+ params.pageSize = 200;
params.ids = productIds.value.join(',');
}
const result = await getItemPage(params as never);
@@ -158,6 +203,7 @@
},
} as VxeTableGridOptions<MesMdItemApi.Item>,
gridEvents: {
+ cellClick: handleCellClick,
cellDblclick: handleCellDblclick,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index 5425bc8..5d74bb2 100644
--- a/src/views/mes/md/item/data.ts
+++ b/src/views/mes/md/item/data.ts
@@ -111,7 +111,7 @@
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
- disabled: false,
+ disabled: true,
optionType: 'button',
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
},
@@ -257,12 +257,7 @@
}
/** 鍒楄〃鐨勫瓧娈� */
-export function useGridColumns(
- onStatusChange?: (
- newStatus: number,
- row: MesMdItemApi.Item,
- ) => PromiseLike<boolean | undefined>,
-): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
+export function useGridColumns(): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
return [
{
field: 'code',
@@ -315,15 +310,11 @@
{
field: 'status',
title: '鐘舵��',
- width: 120,
+ width: 100,
align: 'center',
cellRender: {
- attrs: { beforeChange: onStatusChange },
- name: 'CellSwitch',
- props: {
- checkedValue: CommonStatusEnum.ENABLE,
- unCheckedValue: CommonStatusEnum.DISABLE,
- },
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
},
},
{
diff --git a/src/views/mes/md/item/index.vue b/src/views/mes/md/item/index.vue
index 66c6421..55dd8cd 100644
--- a/src/views/mes/md/item/index.vue
+++ b/src/views/mes/md/item/index.vue
@@ -6,12 +6,9 @@
import { ref } from "vue";
import {
- confirm,
Page,
useVbenModal,
} from "@vben/common-ui";
- import { DICT_TYPE } from "@vben/constants";
- import { getDictLabel } from "@vben/hooks";
import { downloadFileFromBlobPart } from "@vben/utils";
import { Button, Card, message } from "ant-design-vue";
@@ -21,11 +18,10 @@
deleteItem,
exportItem,
getItemPage,
- updateItemStatus,
} from "#/api/mes/md/item";
import { $t } from "#/locales";
import { MdItemTypeTree } from "#/views/mes/md/item/type/components";
- import { PrinterLabel } from "#/views/wls/barcode/components";
+ // import { PrinterLabel } from "#/views/wls/barcode/components";
import { useGridColumns, useGridFormSchema } from "./data";
import Form from "./modules/form.vue";
@@ -95,32 +91,12 @@
handleRefresh();
}
- /** 鏇存柊鐗╂枡鐘舵�� */
- async function handleStatusChange(
- newStatus: number,
- row: MesMdItemApi.Item
- ): Promise<boolean | undefined> {
- try {
- await confirm(
- `纭瑕佸皢"${row.name}"鐗╂枡鍒囨崲涓恒��${getDictLabel(
- DICT_TYPE.COMMON_STATUS,
- newStatus
- )}銆戝悧锛焋
- );
- } catch {
- return false;
- }
- await updateItemStatus(row.id!, newStatus);
- message.success($t("ui.actionMessage.operationSuccess"));
- return true;
- }
-
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useGridFormSchema(),
},
gridOptions: {
- columns: useGridColumns(handleStatusChange),
+ columns: useGridColumns(),
height: "auto",
keepSource: true,
proxyConfig: {
@@ -160,13 +136,6 @@
<template #toolbar-tools>
<TableAction :actions="[
{
- label: $t('ui.actionTitle.import'),
- type: 'primary',
- icon: ACTION_ICON.UPLOAD,
- auth: ['mes:md-item:import'],
- onClick: handleImport,
- },
- {
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
@@ -203,9 +172,9 @@
},
},
]" />
- <PrinterLabel :biz-id="row.id"
+ <!-- <PrinterLabel :biz-id="row.id"
:biz-code="row.code"
- biz-type="ITEM" />
+ biz-type="ITEM" /> -->
</div>
</template>
</Grid>
diff --git a/src/views/mes/md/item/modules/form.vue b/src/views/mes/md/item/modules/form.vue
index db322fd..1639098 100644
--- a/src/views/mes/md/item/modules/form.vue
+++ b/src/views/mes/md/item/modules/form.vue
@@ -8,7 +8,7 @@
import { useVbenModal } from '@vben/common-ui';
import { BarcodeBizTypeEnum } from '@vben/constants';
-import { Button, Empty, message, Tabs } from 'ant-design-vue';
+import { Button, message, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createItem, getItem, updateItem } from '#/api/mes/md/item';
@@ -129,22 +129,30 @@
class="mx-4 mt-4"
>
<Tabs.TabPane key="bom" tab="BOM 缁勬垚">
+ <p class="mb-3 text-xs text-gray-400">
+ 姝ゅ缁存姢鐨勬槸浜у搧鑷韩鐨勭墿鏂欐瀯鎴愶紙閰嶆柟锛夛紝骞堕潪宸ヨ壓璺嚎鐨勬姇鏂� BOM锛涘湪銆屽伐鑹烘祦绋嬨�嶅伐浣滃彴閰嶇疆銆屾姇鍏ョ墿鏂欍�嶆椂锛屼細浠庤 BOM 涓�夋嫨鐗╂枡銆�
+ </p>
<ProductBomForm :form-type="formType" :item-id="formData.id" />
</Tabs.TabPane>
<Tabs.TabPane v-if="formData.batchFlag" key="batch" tab="鎵规灞炴��">
+ <p class="mb-3 text-xs text-gray-400">
+ 姝ゅ閰嶇疆姣忎釜鎵规闇�瑕佺櫥璁扮殑灞炴�х淮搴︼紝鍕鹃�夊悗鍏ュ簱銆佹姤宸ャ�佽川妫�绛夌幆鑺傚皢鎸夋墍閫夌淮搴︾敓鎴愭壒娆′俊鎭�傜墿鏂欎笌浜у搧鐨勫彲閫夊睘鎬т笉鍚岋細鐗╂枡渚ч噸渚涘簲鍟嗐�佹湁鏁堟湡绛夛紝浜у搧渚ч噸璁㈠崟銆佸伐搴忋�佸伐浣滅珯绛夈��
+ </p>
<ItemBatchConfigForm
:form-type="formType"
:item-id="formData.id"
:item-or-product="currentItemOrProduct"
/>
</Tabs.TabPane>
+ <!-- 鏇夸唬鍝佹爣绛鹃〉鏈疄鐜帮紝鏆傛椂闅愯棌
<Tabs.TabPane key="substitute" tab="鏇夸唬鍝�">
<Empty description="鏇夸唬鍝侊紙寰呭疄鐜帮級" />
</Tabs.TabPane>
- <Tabs.TabPane key="sip" tab="SIP">
+ -->
+ <Tabs.TabPane key="sip" tab="妫�楠屾寚瀵间功(SIP)">
<ProductSipForm :form-type="formType" :item-id="formData.id" />
</Tabs.TabPane>
- <Tabs.TabPane key="sop" tab="SOP">
+ <Tabs.TabPane key="sop" tab="浣滀笟鎸囧涔�(SOP)">
<ProductSopForm :form-type="formType" :item-id="formData.id" />
</Tabs.TabPane>
</Tabs>
diff --git a/src/views/mes/md/item/modules/product-media-list.vue b/src/views/mes/md/item/modules/product-media-list.vue
index 777e9fd..804e178 100644
--- a/src/views/mes/md/item/modules/product-media-list.vue
+++ b/src/views/mes/md/item/modules/product-media-list.vue
@@ -48,14 +48,17 @@
);
const isReadOnly = computed(() => props.formType === 'detail'); // 鏄惁鍙
-const title = computed(() => props.kind); // 褰撳墠濯掍綋绫诲瀷鏍囬
+const title = computed(() =>
+ props.kind === 'SIP' ? '妫�楠屾寚瀵间功(SIP)' : '浣滀笟鎸囧涔�(SOP)',
+);
/** 灏嗗畬鏁� URL 杞负鐩稿璺緞锛岀‘淇濊蛋鍓嶇浠g悊 */
function resolveImageUrl(url?: string): string {
if (!url) return '';
try {
- const { pathname } = new URL(url, window.location.origin);
- return pathname;
+ const { pathname, search } = new URL(url, window.location.origin);
+ // 淇濈暀 pathname + 鏌ヨ鍙傛暟锛坧ublicKey锛夛紝纭繚鍚庣鑳介獙璇�
+ return pathname + search;
} catch {
return url;
}
diff --git a/src/views/mes/md/item/type/data.ts b/src/views/mes/md/item/type/data.ts
index 3858003..f36b63a 100644
--- a/src/views/mes/md/item/type/data.ts
+++ b/src/views/mes/md/item/type/data.ts
@@ -223,3 +223,27 @@
},
];
}
+
+/** 鐗╂枡鍒嗙被瀵煎叆鐨勮〃鍗� */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '鍒嗙被鏁版嵁',
+ component: 'Upload',
+ rules: 'required',
+ help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '鏄惁瑕嗙洊',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勫垎绫绘暟鎹�',
+ },
+ ];
+}
diff --git a/src/views/mes/md/item/type/index.vue b/src/views/mes/md/item/type/index.vue
index 062b7d9..859dcb2 100644
--- a/src/views/mes/md/item/type/index.vue
+++ b/src/views/mes/md/item/type/index.vue
@@ -5,18 +5,31 @@
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { deleteItemType, getItemTypeList } from '#/api/mes/md/item/type';
+import {
+ deleteItemType,
+ exportItemType,
+ getItemTypeList,
+} from '#/api/mes/md/item/type';
+import { getItemPage as getMesItemPage } from '#/api/mes/md/item';
+import { getItemPage as getMdmItemPage } from '#/api/mdm/item';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -38,6 +51,17 @@
formModalApi.setData(null).open();
}
+/** 瀵煎叆鍒嗙被 */
+function handleImport() {
+ importModalApi.open();
+}
+
+/** 瀵煎嚭鍒嗙被锛堟寜褰撳墠鍒楄〃鏌ヨ鏉′欢瀵煎嚭鍏ㄩ儴鍛戒腑鏁版嵁锛� */
+async function handleExport() {
+ const data = await exportItemType(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '鐗╂枡鍒嗙被鏁版嵁.xls', source: data });
+}
+
/** 娣诲姞涓嬬骇鍒嗙被 */
function handleAppend(row: MesMdItemTypeApi.ItemType) {
formModalApi.setData({ parentId: row.id }).open();
@@ -50,6 +74,18 @@
/** 鍒犻櫎鍒嗙被 */
async function handleDelete(row: MesMdItemTypeApi.ItemType) {
+ try {
+ const [mesResult, mdmResult] = await Promise.all([
+ getMesItemPage({ pageNo: 1, pageSize: 1, itemTypeId: row.id }),
+ getMdmItemPage({ pageNo: 1, pageSize: 1, categoryId: row.id } as any),
+ ]);
+ if (mesResult.total > 0 || mdmResult.total > 0) {
+ message.error('璇ュ垎绫讳笅瀛樺湪鐗╂枡锛屾棤娉曞垹闄�');
+ return;
+ }
+ } catch {
+ // 鏌ヨ寮傚父鏃剁户缁墽琛屽垹闄わ紝鐢卞悗绔厹搴曟牎楠�
+ }
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
@@ -101,7 +137,7 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Page auto-content-height><FormModal @success="handleRefresh" /><ImportModal @success="handleRefresh" />
<Grid table-title="鐗╂枡鍒嗙被鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -114,6 +150,20 @@
onClick: handleCreate,
},
{
+ label: $t('ui.actionTitle.import', ['鐗╂枡鍒嗙被']),
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['mes:md-item-type:import'],
+ onClick: handleImport,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:md-item-type:export'],
+ onClick: handleExport,
+ },
+ {
label: isExpanded ? '鏀剁缉' : '灞曞紑',
type: 'primary',
onClick: handleExpand,
diff --git a/src/views/mes/md/item/type/modules/import-form.vue b/src/views/mes/md/item/type/modules/import-form.vue
new file mode 100644
index 0000000..db5c74e
--- /dev/null
+++ b/src/views/mes/md/item/type/modules/import-form.vue
@@ -0,0 +1,105 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+
+import { h } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, notification, Upload } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { importItemType, importItemTypeTemplate } from '#/api/mes/md/item/type';
+import { $t } from '#/locales';
+
+import { useImportFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ formItemClass: 'col-span-2',
+ labelWidth: 120,
+ },
+ layout: 'horizontal',
+ schema: useImportFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = await formApi.getValues();
+ try {
+ const response = (await importItemType(data.file, data.updateSupport)) as any;
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+
+ const resultData = response?.data || response || {};
+ const failureList = resultData.failureList || {};
+
+ if (Object.keys(failureList).length > 0) {
+ const errorNodes = Object.entries(failureList).map(([name, reason]) => {
+ return h('div', `${name}: ${reason}`);
+ });
+
+ notification.warning({
+ message: '瀵煎叆瀹屾垚锛岄儴鍒嗙墿鏂欏垎绫诲鍏ュけ璐�',
+ description: () => h('div', errorNodes),
+ duration: null,
+ });
+ } else {
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+/** 涓婁紶鍓� */
+function beforeUpload(file: FileType) {
+ formApi.setFieldValue('file', file);
+ return false;
+}
+
+/** 涓嬭浇妯$増 */
+async function handleDownload() {
+ const data = await importItemTypeTemplate();
+ downloadFileFromBlobPart({ fileName: '鐗╂枡鍒嗙被瀵煎叆妯℃澘.xls', source: data });
+}
+</script>
+
+<template>
+ <Modal title="瀵煎叆鐗╂枡鍒嗙被" class="w-1/3">
+ <Form class="mx-4">
+ <template #file>
+ <div class="w-full">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :before-upload="beforeUpload"
+ >
+ <Button type="primary"> 閫夋嫨 Excel 鏂囦欢 </Button>
+ </Upload>
+ </div>
+ </template>
+ </Form>
+ <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
+ 鎻愮ず锛氭ā鏉夸腑鐨勨�滅埗鍒嗙被缂栫爜鈥濊濉啓鐖跺垎绫荤殑缂栫爜锛堝 RAW锛夛紝椤剁骇鍒嗙被璇ュ垪鐣欑┖鍗冲彲銆�
+ <div>鈥滅墿鏂�/浜у搧鏍囪瘑鈥濆~ ITEM锛堢墿鏂欙級鎴� PRODUCT锛堜骇鍝侊級銆�</div>
+ <div>鈥滅姸鎬佲�濆~ 0锛堝惎鐢級/1锛堢鐢級銆�</div>
+ </div>
+ <template #prepend-footer>
+ <div class="flex flex-auto items-center">
+ <Button @click="handleDownload"> 涓嬭浇瀵煎叆妯℃澘 </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/md/workstation/index.vue b/src/views/mes/md/workstation/index.vue
index 6f505e4..8baf7e2 100644
--- a/src/views/mes/md/workstation/index.vue
+++ b/src/views/mes/md/workstation/index.vue
@@ -5,7 +5,7 @@
import { ref } from 'vue';
import { Page, useVbenModal } from '@vben/common-ui';
-import { BarcodeBizTypeEnum } from '@vben/constants';
+// import { BarcodeBizTypeEnum } from '@vben/constants';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -17,12 +17,12 @@
getWorkstationPage,
} from '#/api/mes/md/workstation';
import { $t } from '#/locales';
-import { BarcodeDetail } from '#/views/wls/barcode/components';
+// import { BarcodeDetail } from '#/views/wls/barcode/components';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
-const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
+// const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
@@ -65,17 +65,17 @@
}
/** 鏌ョ湅宸ヤ綔绔欐潯鐮� */
-function handleBarcode(row: MesMdWorkstationApi.Workstation) {
- if (!row.id) {
- return;
- }
- barcodeDetailRef.value?.openByBusiness(
- row.id,
- BarcodeBizTypeEnum.WORKSTATION,
- row.code,
- row.name,
- );
-}
+// function handleBarcode(row: MesMdWorkstationApi.Workstation) {
+// if (!row.id) {
+// return;
+// }
+// barcodeDetailRef.value?.openByBusiness(
+// row.id,
+// BarcodeBizTypeEnum.WORKSTATION,
+// row.code,
+// row.name,
+// );
+// }
/** 瀵煎嚭宸ヤ綔绔� */
async function handleExport() {
@@ -116,7 +116,7 @@
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
- <BarcodeDetail ref="barcodeDetailRef" />
+ <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
<Grid table-title="宸ヤ綔绔欏垪琛�">
<template #toolbar-tools>
@@ -165,12 +165,12 @@
confirm: handleDelete.bind(null, row),
},
},
- {
- label: '鏉$爜',
- type: 'link',
- auth: ['mes:md-workstation:query'],
- onClick: handleBarcode.bind(null, row),
- },
+ // {
+ // label: '鏉$爜',
+ // type: 'link',
+ // auth: ['mes:md-workstation:query'],
+ // onClick: handleBarcode.bind(null, row),
+ // },
]"
/>
</template>
diff --git a/src/views/mes/md/workstation/modules/form.vue b/src/views/mes/md/workstation/modules/form.vue
index 0c47ba3..0258a32 100644
--- a/src/views/mes/md/workstation/modules/form.vue
+++ b/src/views/mes/md/workstation/modules/form.vue
@@ -6,7 +6,7 @@
import { computed, ref } from "vue";
import { useVbenModal } from "@vben/common-ui";
- import { BarcodeBizTypeEnum } from "@vben/constants";
+ // import { BarcodeBizTypeEnum } from "@vben/constants";
import { Button, message, Tabs } from "ant-design-vue";
@@ -17,7 +17,7 @@
updateWorkstation,
} from "#/api/mes/md/workstation";
import { $t } from "#/locales";
- import { BarcodeDetail } from "#/views/wls/barcode/components";
+ // import { BarcodeDetail } from "#/views/wls/barcode/components";
import { useFormSchema } from "../data";
import MachineList from "./machine-list.vue";
@@ -28,7 +28,7 @@
const formType = ref<FormType>("create"); // 琛ㄥ崟妯″紡
const subTabsName = ref("machine"); // 褰撳墠璧勬簮椤电
const formData = ref<MesMdWorkstationApi.Workstation>();
- const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
+ // const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
const isDetail = computed(() => formType.value === "detail"); // 鏄惁鏌ョ湅妯″紡
const getTitle = computed(() => {
@@ -53,17 +53,17 @@
});
/** 鏌ョ湅宸ヤ綔绔欐潯鐮� */
- function handleBarcode() {
- if (!formData.value?.id) {
- return;
- }
- barcodeDetailRef.value?.openByBusiness(
- formData.value.id,
- BarcodeBizTypeEnum.WORKSTATION,
- formData.value.code,
- formData.value.name
- );
- }
+ // function handleBarcode() {
+ // if (!formData.value?.id) {
+ // return;
+ // }
+ // barcodeDetailRef.value?.openByBusiness(
+ // formData.value.id,
+ // BarcodeBizTypeEnum.WORKSTATION,
+ // formData.value.code,
+ // formData.value.name
+ // );
+ // }
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
@@ -146,13 +146,13 @@
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center">
- <Button v-if="isDetail && formData?.id"
+ <!-- <Button v-if="isDetail && formData?.id"
type="primary"
@click="handleBarcode">
鏌ョ湅鏉$爜
- </Button>
+ </Button> -->
</div>
</template>
- <BarcodeDetail ref="barcodeDetailRef" />
+ <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
</Modal>
</template>
diff --git a/src/views/mes/md/workstation/workshop/modules/form.vue b/src/views/mes/md/workstation/workshop/modules/form.vue
index 2dd8b84..ec8f682 100644
--- a/src/views/mes/md/workstation/workshop/modules/form.vue
+++ b/src/views/mes/md/workstation/workshop/modules/form.vue
@@ -6,7 +6,7 @@
import { computed, ref } from "vue";
import { useVbenModal } from "@vben/common-ui";
- import { BarcodeBizTypeEnum } from "@vben/constants";
+ // import { BarcodeBizTypeEnum } from "@vben/constants";
import { Button, message } from "ant-design-vue";
@@ -17,14 +17,14 @@
updateWorkshop,
} from "#/api/mes/md/workstation/workshop";
import { $t } from "#/locales";
- import { BarcodeDetail } from "#/views/wls/barcode/components";
+ // import { BarcodeDetail } from "#/views/wls/barcode/components";
import { useFormSchema } from "../data";
const emit = defineEmits(["success"]);
const formType = ref<FormType>("create"); // 琛ㄥ崟妯″紡
const formData = ref<MesMdWorkshopApi.Workshop>();
- const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
+ // const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
const isDetail = computed(() => formType.value === "detail"); // 鏄惁鏌ョ湅妯″紡
const getTitle = computed(() => {
@@ -49,17 +49,17 @@
});
/** 鏌ョ湅杞﹂棿鏉$爜 */
- function handleBarcode() {
- if (!formData.value?.id) {
- return;
- }
- barcodeDetailRef.value?.openByBusiness(
- formData.value.id,
- BarcodeBizTypeEnum.WORKSHOP,
- formData.value.code,
- formData.value.name
- );
- }
+ // function handleBarcode() {
+ // if (!formData.value?.id) {
+ // return;
+ // }
+ // barcodeDetailRef.value?.openByBusiness(
+ // formData.value.id,
+ // BarcodeBizTypeEnum.WORKSHOP,
+ // formData.value.code,
+ // formData.value.name
+ // );
+ // }
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
@@ -117,9 +117,9 @@
<template v-if="isDetail && formData?.id"
#prepend-footer>
<div class="flex flex-auto items-center">
- <Button @click="handleBarcode">鏌ョ湅鏉$爜</Button>
+ <!-- <Button @click="handleBarcode">鏌ョ湅鏉$爜</Button> -->
</div>
</template>
</Modal>
- <BarcodeDetail ref="barcodeDetailRef" />
+ <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
</template>
diff --git a/src/views/mes/pd/archive/modules/detail.vue b/src/views/mes/pd/archive/modules/detail.vue
index e13a306..d11f471 100644
--- a/src/views/mes/pd/archive/modules/detail.vue
+++ b/src/views/mes/pd/archive/modules/detail.vue
@@ -54,23 +54,33 @@
watch(() => props.visible, async (newVal) => {
if (newVal && props.productCode) {
- open.value = true;
- loading.value = true;
- detailList.value = [];
- activeProject.value = '0';
- try {
- detailList.value = await getPdArchiveDetailByProduct(props.productCode);
- } catch {
- message.error('鑾峰彇褰掓。璇︽儏澶辫触');
- } finally {
- loading.value = false;
- }
+ loadData(props.productCode);
} else if (!newVal) {
open.value = false;
detailList.value = [];
}
});
+async function loadData(code: string) {
+ open.value = true;
+ loading.value = true;
+ detailList.value = [];
+ activeProject.value = '0';
+ try {
+ detailList.value = await getPdArchiveDetailByProduct(code);
+ } catch {
+ message.error('鑾峰彇褰掓。璇︽儏澶辫触');
+ } finally {
+ loading.value = false;
+ }
+}
+
+function openModal(code: string) {
+ loadData(code);
+}
+
+defineExpose({ open: openModal });
+
function handleClose() {
emit('update:visible', false);
}
diff --git a/src/views/mes/pd/project/detail/index.vue b/src/views/mes/pd/project/detail/index.vue
index 36da5ce..a7f2c26 100644
--- a/src/views/mes/pd/project/detail/index.vue
+++ b/src/views/mes/pd/project/detail/index.vue
@@ -13,8 +13,8 @@
import {
archivePdProject,
- auditPdProject,
getPdProject,
+ getPdProjectApproveProcessList,
publishPdProject,
uploadPdProject,
} from '#/api/mes/pd/project';
@@ -25,6 +25,7 @@
import Form from '../modules/form.vue';
import DocumentAuditList from '../modules/document-audit-list.vue';
import DocumentList from '../modules/document-list.vue';
+import ProcessSelectModal from '../modules/process-select-modal.vue';
import WorkflowTimeline from '../modules/workflow-timeline.vue';
import TaskDraftBanner from '../modules/task-draft-banner.vue';
import { useDetailSchema } from './data';
@@ -50,6 +51,9 @@
const isPendingArchive = computed(
() => project.value.status === MesPdProjectStatusEnum.PENDING_ARCHIVE,
);
+const isApproving = computed(
+ () => project.value.status === MesPdProjectStatusEnum.APPROVING,
+);
const isArchived = computed(
() => project.value.status === MesPdProjectStatusEnum.ARCHIVED,
);
@@ -73,14 +77,26 @@
destroyOnClose: true,
});
+const [ProcessModal, processModalApi] = useVbenModal({
+ connectedComponent: ProcessSelectModal,
+ destroyOnClose: true,
+});
+
async function loadProjectDetail() {
loading.value = true;
try {
project.value = await getPdProject(projectId.value);
- if (isPendingUpload.value) {
+ if (route.query.tab === 'audit' && isPendingAudit.value) {
+ // 浠庡垪琛ㄩ〉銆屽鏍告枃浠躲�嶆寜閽繘鍏ワ紝涓斿綋鍓嶇‘瀹炲浜庡緟瀹℃牳鐘舵�佹椂锛屾墠瀹氫綅鍒拌祫鏂欏鏍� Tab
+ activeTab.value = 'audit';
+ } else if (isPendingUpload.value) {
activeTab.value = 'material';
} else if (isPendingAudit.value) {
activeTab.value = 'audit';
+ } else {
+ // 瀹℃壒涓�/寰呭綊妗�/宸插綊妗g瓑鐘舵�侊細閲嶇疆鍒般�岃璁¤祫鏂欍�嶉〉绛撅紝
+ // 閬垮厤 activeTab 浠嶆寚鍚戦殢鐘舵�佸彉鍖栬�岀Щ闄ょ殑銆岃祫鏂欏鏍搞�嶉〉绛撅紝瀵艰嚧涓嬫柟鍐呭绌虹櫧
+ activeTab.value = 'material';
}
} finally {
loading.value = false;
@@ -89,7 +105,7 @@
function handleBack() {
tabs.closeCurrentTab();
- router.push({ path: '/mes/pd/project' });
+ router.back();
}
function handleEdit() {
@@ -119,16 +135,38 @@
}
}
-async function handleAudit() {
- const hideLoading = message.loading({ content: '瀹℃牳涓��', duration: 0 });
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit() {
+ const hideLoading = message.loading({ content: '鑾峰彇瀹℃壒娴佺▼...', duration: 0 });
try {
- await auditPdProject(projectId.value);
- message.success('瀹℃牳閫氳繃锛岃繘鍏ュ緟褰掓。鐘舵��');
- tabs.closeCurrentTab();
- router.back();
- } finally {
+ const processList = await getPdProjectApproveProcessList();
+ hideLoading();
+ if (!processList || processList.length === 0) {
+ message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+ return;
+ }
+ // 鎵撳紑娴佺▼閫夋嫨寮圭獥
+ processModalApi.setData({ id: projectId.value, processList }).open();
+ } catch {
hideLoading();
}
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail() {
+ if (!project.value.processInstanceId) {
+ message.warning('璇ラ」鐩皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: project.value.processInstanceId },
+ });
+}
+
+/** 鎻愪氦瀹℃壒鎴愬姛鍚庡埛鏂拌鎯� */
+function handleSubmitSuccess() {
+ loadProjectDetail();
}
async function handleArchive() {
@@ -155,6 +193,7 @@
:title="project.taskName || '璁捐浠诲姟璇︽儏'"
>
<FormModal @success="loadProjectDetail" />
+ <ProcessModal @success="handleSubmitSuccess" />
<template #extra>
<TableAction
@@ -185,10 +224,16 @@
onClick: handleUpload,
},
{
- label: '瀹℃牳閫氳繃',
+ label: '鎻愪氦瀹℃壒',
type: 'primary',
ifShow: isPendingAudit,
- onClick: handleAudit,
+ onClick: handleSubmit,
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'primary',
+ ifShow: isApproving,
+ onClick: handleProcessDetail,
},
{
label: '褰掓。',
diff --git a/src/views/mes/pd/project/index.vue b/src/views/mes/pd/project/index.vue
index 24516e1..05c74ce 100644
--- a/src/views/mes/pd/project/index.vue
+++ b/src/views/mes/pd/project/index.vue
@@ -17,6 +17,7 @@
deletePdProject,
exportPdProject,
+ getPdProjectApproveProcessList,
getPdProjectPage,
publishPdProject,
@@ -26,6 +27,7 @@
import { useGridColumns, useGridFormSchema } from './data';
import CreateForm from './modules/create-form.vue';
import Form from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
const router = useRouter();
@@ -36,6 +38,11 @@
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ProcessModal, processModalApi] = useVbenModal({
+ connectedComponent: ProcessSelectModal,
destroyOnClose: true,
});
@@ -71,6 +78,47 @@
} finally {
hideLoading();
}
+}
+
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栨祦绋嬪垪琛� */
+async function handleSubmit(row: MesPdProjectApi.Project) {
+ const hideLoading = message.loading({
+ content: '鑾峰彇瀹℃壒娴佺▼...',
+ duration: 0,
+ });
+ try {
+ const processList = await getPdProjectApproveProcessList();
+ hideLoading();
+ if (!processList || processList.length === 0) {
+ message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堥厤缃� BPM 娴佺▼');
+ return;
+ }
+ // 鎵撳紑娴佺▼閫夋嫨寮圭獥
+ processModalApi.setData({ id: row.id, processList }).open();
+ } catch {
+ hideLoading();
+ }
+}
+
+/** 瀹℃牳鏂囦欢 - 璺宠浆璇︽儏椤佃祫鏂欏鏍� Tab */
+function handleAuditFile(row: MesPdProjectApi.Project) {
+ router.push({
+ name: 'MesPdProjectDetail',
+ params: { id: row.id },
+ query: { tab: 'audit' },
+ });
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleProcessDetail(row: MesPdProjectApi.Project) {
+ if (!row.processInstanceId) {
+ message.warning('璇ラ」鐩皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: row.processInstanceId },
+ });
}
async function handleArchive(row: MesPdProjectApi.Project) {
@@ -137,6 +185,7 @@
<Page auto-content-height>
<CreateFormModal @success="handleCreateSuccess" />
<FormModal @success="handleRefresh" />
+ <ProcessModal @success="handleRefresh" />
<Grid table-title="璁捐浠诲姟">
<template #toolbar-tools>
@@ -183,11 +232,29 @@
onClick: () => handleDetail(row),
},
{
- label: '瀹℃牳',
+ label: '瀹℃牳鏂囦欢',
type: 'link',
- auth: ['mes:pd-project:audit'],
- ifShow: row.status === MesPdProjectStatusEnum.PENDING_AUDIT,
- onClick: () => handleDetail(row),
+ auth: ['mes:pd-project:update'],
+ ifShow:
+ row.status === MesPdProjectStatusEnum.PENDING_AUDIT &&
+ (row.pendingAuditCount ?? 0) > 0,
+ onClick: handleAuditFile.bind(null, row),
+ },
+ {
+ label: '鎻愪氦瀹℃壒',
+ type: 'link',
+ auth: ['mes:pd-project:update'],
+ ifShow:
+ row.status === MesPdProjectStatusEnum.PENDING_AUDIT &&
+ (row.pendingAuditCount ?? 0) === 0,
+ onClick: handleSubmit.bind(null, row),
+ },
+ {
+ label: '鏌ョ湅瀹℃壒',
+ type: 'link',
+ auth: ['mes:pd-project:update'],
+ ifShow: row.status === MesPdProjectStatusEnum.APPROVING,
+ onClick: handleProcessDetail.bind(null, row),
},
{
label: '褰掓。',
diff --git a/src/views/mes/pd/project/modules/document-audit-form.vue b/src/views/mes/pd/project/modules/document-audit-form.vue
index 9fa4944..93f0dc0 100644
--- a/src/views/mes/pd/project/modules/document-audit-form.vue
+++ b/src/views/mes/pd/project/modules/document-audit-form.vue
@@ -26,7 +26,13 @@
fieldName: 'auditStatus',
label: '瀹℃牳缁撴灉',
component: 'Select',
- componentProps: { options: getDictOptions(DICT_TYPE.MES_PD_DOCUMENT_AUDIT_STATUS, 'number'), placeholder: '璇烽�夋嫨瀹℃牳缁撴灉' },
+ componentProps: {
+ options: getDictOptions(
+ DICT_TYPE.MES_PD_DOCUMENT_AUDIT_STATUS,
+ 'number',
+ ).filter((item) => item.value !== 0), // 0 涓哄緟瀹℃牳锛屽鏍告椂鍙厑璁搁�氳繃/涓嶉�氳繃
+ placeholder: '璇烽�夋嫨瀹℃牳缁撴灉',
+ },
rules: 'selectRequired',
},
{ fieldName: 'remark', label: '瀹℃牳鎰忚', component: 'Textarea', componentProps: { placeholder: '璇疯緭鍏ュ鏍告剰瑙�', rows: 3 } },
@@ -53,7 +59,9 @@
if (!isOpen) return;
const data = modalApi.getData<MesPdDocumentAuditApi.DocumentAudit>();
modalApi.setState({ title: '璧勬枡瀹℃牳' });
- await formApi.setValues(data);
+ const formData = { ...data };
+ delete formData.auditStatus;
+ await formApi.setValues(formData);
},
});
</script>
diff --git a/src/views/mes/pd/project/modules/process-select-modal.vue b/src/views/mes/pd/project/modules/process-select-modal.vue
new file mode 100644
index 0000000..c0d099a
--- /dev/null
+++ b/src/views/mes/pd/project/modules/process-select-modal.vue
@@ -0,0 +1,75 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitPdProjectApproval } from '#/api/mes/pd/project';
+
+interface ProcessDefinition {
+ id: string;
+ name: string;
+ key: string;
+}
+
+const emit = defineEmits(['success']);
+
+const projectId = ref<number>();
+const processList = ref<ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+const processOptions = computed(() => {
+ return processList.value.map((p) => ({
+ label: p.name,
+ value: p.key,
+ }));
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!selectedProcessKey.value) {
+ message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+ return;
+ }
+ modalApi.lock();
+ try {
+ await submitPdProjectApproval(projectId.value!, selectedProcessKey.value);
+ await modalApi.close();
+ emit('success');
+ message.success('鎻愪氦瀹℃壒鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ projectId.value = undefined;
+ selectedProcessKey.value = '';
+ processList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
+ projectId.value = data.id;
+ processList.value = data.processList || [];
+ // 榛樿閫夋嫨绗竴涓祦绋�
+ if (processList.value.length > 0) {
+ selectedProcessKey.value = processList.value[0].key;
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+ <div class="mb-4">
+ <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+ <Select
+ v-model:value="selectedProcessKey"
+ :options="processOptions"
+ style="width: 200px"
+ placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+ />
+ </div>
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/workflow-timeline.vue b/src/views/mes/pd/project/modules/workflow-timeline.vue
index 6989422..db03cc8 100644
--- a/src/views/mes/pd/project/modules/workflow-timeline.vue
+++ b/src/views/mes/pd/project/modules/workflow-timeline.vue
@@ -3,22 +3,35 @@
import { Steps } from 'ant-design-vue';
-const STEPS = ['寰呭彂甯�', '寰呬笂浼�', '寰呭鏍�', '寰呭綊妗�', '宸插綊妗�'] as const;
+import { MesPdProjectStatusEnum } from '#/packages/constants/src';
+
+const STEPS = ['寰呭彂甯�', '寰呬笂浼�', '寰呭鏍�', '瀹℃壒涓�', '寰呭綊妗�', '宸插綊妗�'] as const;
+
+// 鐘舵�佸�� 鈫� 鏃堕棿绾挎楠ょ储寮曪紙瀹℃壒涓�=5 鎻掑湪寰呭鏍镐箣鍚庯級
+const STATUS_ORDER = [
+ MesPdProjectStatusEnum.PENDING_PUBLISH,
+ MesPdProjectStatusEnum.PENDING_UPLOAD,
+ MesPdProjectStatusEnum.PENDING_AUDIT,
+ MesPdProjectStatusEnum.APPROVING,
+ MesPdProjectStatusEnum.PENDING_ARCHIVE,
+ MesPdProjectStatusEnum.ARCHIVED,
+];
const props = defineProps<{
- /** 椤圭洰褰撳墠鐘舵�� (0-4) */
+ /** 椤圭洰褰撳墠鐘舵�� */
status: number;
}>();
const current = computed(() => {
- // 鐘舵�佸�煎嵆褰撳墠宸插畬鎴愬埌绗嚑姝ワ細0=绗竴姝ヨ繘琛屼腑锛�4=鍏ㄩ儴瀹屾垚
- return props.status;
+ // 鏍规嵁鐘舵�佸�兼槧灏勫埌鏃堕棿绾挎楠ょ储寮�
+ const index = STATUS_ORDER.indexOf(props.status);
+ return index >= 0 ? index : 0;
});
const nodes = computed(() =>
STEPS.map((name, index) => ({
title: name,
- status: index < props.status ? 'finish' : index === props.status ? 'process' : 'wait',
+ status: index < current.value ? 'finish' : index === current.value ? 'process' : 'wait',
})),
);
</script>
diff --git a/src/views/mes/pro/feedback/data.ts b/src/views/mes/pro/feedback/data.ts
index affe7dd..32056e7 100644
--- a/src/views/mes/pro/feedback/data.ts
+++ b/src/views/mes/pro/feedback/data.ts
@@ -1,7 +1,8 @@
-锘縤mport type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import type { MesProTaskApi } from '#/api/mes/pro/task';
+import type { DescriptionItemSchema } from '#/components/description';
import { h, markRaw } from 'vue';
@@ -18,11 +19,13 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getSimpleUserList } from '#/api/system/user';
+import { loadParamDetails } from './param-state';
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProTaskSelect } from '#/views/mes/pro/task/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
+import { DictTag } from '#/components/dict-tag';
/** 鐢熶骇鎶ュ伐琛ㄥ崟绫诲瀷 */
export type FormType = 'approve' | 'create' | 'detail' | 'submit' | 'update';
@@ -171,9 +174,8 @@
* - create / update锛氬綍鍏ユ姤宸ヤ富浣撲俊鎭拰鏁伴噺
* - submit / approve / detail锛氫富浣撳瓧娈靛彧璇�
*
- * 鏁伴噺鍖哄煙鏍规嵁 `checkFlag` 鍜� `unqualifiedQuantity` 鍔ㄦ�佹樉绀猴細
- * - 闈炶川妫�宸ュ簭锛氭姤宸ユ暟閲� = 鍚堟牸 + 涓嶈壇锛涗笉鑹� > 0 鏃跺啀灞曞紑宸ュ簾/鏂欏簾/鍏朵粬搴熷搧
- * - 璐ㄦ宸ュ簭锛氬彧濉姤宸ユ暟閲忥紙瑙嗕负寰呮鏁伴噺锛�
+ * 鎶ュ伐鍙~鍐�"鎶ュ伐鏁伴噺"锛涘悎鏍煎搧/涓嶈壇鍝佹暟閲忕敱璐ㄦ纭畾锛屾姤宸ョ幆鑺備笉閲囬泦銆�
+ * 璐ㄦ宸ュ簭鎶ュ伐鍚庤繘鍏ュ緟妫�楠岋紙uncheckQuantity = 鎶ュ伐鏁伴噺锛夛紝闈炶川妫�宸ュ簭鐩存帴瀹岀粨銆�
*/
export function useFormSchema(
formType: FormType,
@@ -264,6 +266,7 @@
taskId: undefined,
unitMeasureName: undefined,
workstationId: undefined,
+ reportableQuantity: undefined,
});
},
},
@@ -283,9 +286,10 @@
workstationId: values.workstationId,
}),
},
- // 浠诲姟鍙樻洿锛氳嚜鍔ㄥ~鍏呭叧鑱斿瓧娈点�佷骇鍝佷俊鎭�乧heckFlag
+ // 浠诲姟鍙樻洿锛氳嚜鍔ㄥ~鍏呭叧鑱斿瓧娈点�佷骇鍝佷俊鎭�乧heckFlag锛屽苟鍔犺浇浠诲姟缁戝畾鐨勫伐鑹哄弬鏁�
componentProps: {
onChange: async (task?: MesProTaskApi.Task) => {
+ await loadParamDetails(task?.id);
if (!task) {
return;
}
@@ -298,6 +302,7 @@
routeId: task.routeId,
unitMeasureName: task.unitMeasureName,
workstationId: task.workstationId,
+ reportableQuantity: task.reportableQuantity,
});
// 宸ヨ壓璺嚎宸ュ簭鐨� checkFlag 鍐冲畾鏁伴噺鍖哄煙灞曠ず
if (task.routeId && task.processId) {
@@ -377,6 +382,20 @@
},
},
{
+ fieldName: 'reportableQuantity',
+ label: '鍙姤宸ユ暟閲�',
+ component: 'InputNumber',
+ componentProps: {
+ class: 'w-full',
+ disabled: true,
+ precision: 2,
+ },
+ dependencies: {
+ triggerFields: ['taskId'],
+ show: (values) => !!values.taskId && ['create', 'update'].includes(formType),
+ },
+ },
+ {
fieldName: 'feedbackQuantity',
label: '鎶ュ伐鏁伴噺',
component: 'InputNumber',
@@ -384,151 +403,9 @@
class: 'w-full',
min: 0,
precision: 2,
- },
- dependencies: {
- triggerFields: ['checkFlag'],
- // 闈炶川妫�宸ュ簭鏃讹紝鎶ュ伐鏁伴噺 = 鍚堟牸 + 涓嶈壇锛岀鐢ㄧ洿鎺ョ紪杈�
- componentProps: (values) => ({
- class: 'w-full',
- disabled: !values.checkFlag,
- min: 0,
- placeholder: '璇疯緭鍏ユ姤宸ユ暟閲�',
- precision: 2,
- }),
+ placeholder: '璇疯緭鍏ユ姤宸ユ暟閲�',
},
rules: 'required',
- },
- {
- fieldName: 'qualifiedQuantity',
- label: '鍚堟牸鍝佹暟閲�',
- component: 'InputNumber',
- componentProps: {
- class: 'w-full',
- min: 0,
- precision: 2,
- // 鍚堟牸/涓嶈壇鍙樻洿锛岃嚜鍔ㄧ疮璁′负鎶ュ伐鏁伴噺
- onChange: async () => {
- const values = await formApi?.getValues();
- await formApi?.setFieldValue(
- 'feedbackQuantity',
- (values?.qualifiedQuantity || 0) +
- (values?.unqualifiedQuantity || 0),
- );
- },
- },
- defaultValue: 0,
- dependencies: {
- triggerFields: ['checkFlag'],
- show: (values) => !values.checkFlag,
- },
- },
- {
- fieldName: 'unqualifiedQuantity',
- label: '涓嶈壇鍝佹暟閲�',
- component: 'InputNumber',
- componentProps: {
- class: 'w-full',
- min: 0,
- precision: 2,
- // 鍚堟牸/涓嶈壇鍙樻洿锛岃嚜鍔ㄧ疮璁′负鎶ュ伐鏁伴噺
- onChange: async () => {
- const values = await formApi?.getValues();
- await formApi?.setFieldValue(
- 'feedbackQuantity',
- (values?.qualifiedQuantity || 0) +
- (values?.unqualifiedQuantity || 0),
- );
- },
- },
- defaultValue: 0,
- dependencies: {
- triggerFields: ['checkFlag'],
- show: (values) => !values.checkFlag,
- },
- },
- {
- fieldName: 'laborScrapQuantity',
- label: '宸ュ簾鏁伴噺',
- component: 'InputNumber',
- componentProps: {
- class: 'w-full',
- min: 0,
- precision: 2,
- // 搴熷搧鍒嗙被鍙樻洿锛岃嚜鍔ㄧ疮璁′负涓嶈壇鍝佹暟閲忓強鎶ュ伐鏁伴噺
- onChange: async () => {
- const values = await formApi?.getValues();
- const unqualified =
- (values?.laborScrapQuantity || 0) +
- (values?.materialScrapQuantity || 0) +
- (values?.otherScrapQuantity || 0);
- await formApi?.setValues({
- feedbackQuantity: (values?.qualifiedQuantity || 0) + unqualified,
- unqualifiedQuantity: unqualified,
- });
- },
- },
- defaultValue: 0,
- dependencies: {
- triggerFields: ['checkFlag', 'unqualifiedQuantity'],
- show: (values) =>
- !values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
- },
- },
- {
- fieldName: 'materialScrapQuantity',
- label: '鏂欏簾鏁伴噺',
- component: 'InputNumber',
- componentProps: {
- class: 'w-full',
- min: 0,
- precision: 2,
- // 搴熷搧鍒嗙被鍙樻洿锛岃嚜鍔ㄧ疮璁′负涓嶈壇鍝佹暟閲忓強鎶ュ伐鏁伴噺
- onChange: async () => {
- const values = await formApi?.getValues();
- const unqualified =
- (values?.laborScrapQuantity || 0) +
- (values?.materialScrapQuantity || 0) +
- (values?.otherScrapQuantity || 0);
- await formApi?.setValues({
- feedbackQuantity: (values?.qualifiedQuantity || 0) + unqualified,
- unqualifiedQuantity: unqualified,
- });
- },
- },
- defaultValue: 0,
- dependencies: {
- triggerFields: ['checkFlag', 'unqualifiedQuantity'],
- show: (values) =>
- !values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
- },
- },
- {
- fieldName: 'otherScrapQuantity',
- label: '鍏朵粬搴熷搧',
- component: 'InputNumber',
- componentProps: {
- class: 'w-full',
- min: 0,
- precision: 2,
- // 搴熷搧鍒嗙被鍙樻洿锛岃嚜鍔ㄧ疮璁′负涓嶈壇鍝佹暟閲忓強鎶ュ伐鏁伴噺
- onChange: async () => {
- const values = await formApi?.getValues();
- const unqualified =
- (values?.laborScrapQuantity || 0) +
- (values?.materialScrapQuantity || 0) +
- (values?.otherScrapQuantity || 0);
- await formApi?.setValues({
- feedbackQuantity: (values?.qualifiedQuantity || 0) + unqualified,
- unqualifiedQuantity: unqualified,
- });
- },
- },
- defaultValue: 0,
- dependencies: {
- triggerFields: ['checkFlag', 'unqualifiedQuantity'],
- show: (values) =>
- !values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
- },
},
{
fieldName: 'feedbackUserId',
@@ -585,3 +462,34 @@
},
];
}
+
+/** 鐢熶骇鎶ュ伐璇︽儏鎽樿 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ { field: 'code', label: '鎶ヨ鍗曞彿' },
+ {
+ field: 'type',
+ label: '鎶ュ伐绫诲瀷',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.MES_PRO_FEEDBACK_TYPE, value: val }),
+ },
+ { field: 'workOrderId', label: '鐢熶骇璁㈠崟', render: (_val, data) => data?.workOrderCode },
+ { field: 'taskId', label: '鐢熶骇浠诲姟', render: (_val, data) => data?.taskCode },
+ { field: 'workstationName', label: '宸ヤ綔绔�' },
+ { field: 'itemCode', label: '浜у搧缂栫爜' },
+ { field: 'itemName', label: '浜у搧鍚嶇О' },
+ { field: 'unitMeasureName', label: '鍗曚綅' },
+ { field: 'itemSpecification', label: '瑙勬牸' },
+ { field: 'reportableQuantity', label: '鍙姤宸ユ暟閲�' },
+ { field: 'feedbackQuantity', label: '鎶ュ伐鏁伴噺' },
+ { field: 'qualifiedQuantity', label: '鍚堟牸鍝佹暟閲�' },
+ { field: 'unqualifiedQuantity', label: '涓嶈壇鍝佹暟閲�' },
+ { field: 'laborScrapQuantity', label: '宸ュ簾鏁伴噺' },
+ { field: 'materialScrapQuantity', label: '鏂欏簾鏁伴噺' },
+ { field: 'otherScrapQuantity', label: '鍏朵粬搴熷搧' },
+ { field: 'feedbackUserNickname', label: '鎶ュ伐浜�' },
+ { field: 'feedbackTime', label: '鎶ュ伐鏃堕棿' },
+ { field: 'approveUserNickname', label: '瀹℃牳浜�' },
+ { field: 'remark', label: '澶囨敞' },
+ ];
+}
diff --git a/src/views/mes/pro/feedback/detail/index.vue b/src/views/mes/pro/feedback/detail/index.vue
new file mode 100644
index 0000000..01acd2e
--- /dev/null
+++ b/src/views/mes/pro/feedback/detail/index.vue
@@ -0,0 +1,104 @@
+<script lang="ts" setup>
+import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
+
+import { onMounted, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page } from '@vben/common-ui';
+import { useTabs } from '@vben/hooks';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { Card, Table, Tabs } from 'ant-design-vue';
+
+import { TableAction } from '#/adapter/vxe-table';
+import { getFeedback } from '#/api/mes/pro/feedback';
+import { DictTag } from '#/components/dict-tag';
+import { useDescription } from '#/components/description';
+
+import { useDetailSchema } from '../data';
+import ItemConsumeList from '../modules/item-consume-list.vue';
+import ProductProduceList from '../modules/product-produce-list.vue';
+
+const paramColumns = [
+ { title: '鍙傛暟鍚嶇О', dataIndex: 'paramName', key: 'paramName' },
+ { title: '鍙傛暟缂栫爜', dataIndex: 'paramCode', key: 'paramCode' },
+ { title: '鍙傛暟绫诲瀷', dataIndex: 'paramType', key: 'paramType' },
+ { title: '鍗曚綅', dataIndex: 'unitMeasureName', key: 'unitMeasureName' },
+ { title: '鍙傛暟鍊�', dataIndex: 'paramValue', key: 'paramValue' },
+ { title: '鏄惁蹇呭~', dataIndex: 'required', key: 'required' },
+];
+
+const props = defineProps<{ id: string }>();
+
+const router = useRouter();
+const tabs = useTabs();
+
+const loading = ref(false);
+const feedbackId = ref(0);
+const feedback = ref<MesProFeedbackApi.Feedback>({});
+const activeTab = ref('itemConsume');
+
+const [Descriptions] = useDescription({
+ bordered: false,
+ column: 4,
+ schema: useDetailSchema(),
+});
+
+async function loadFeedbackDetail() {
+ loading.value = true;
+ try {
+ feedback.value = await getFeedback(feedbackId.value);
+ } finally {
+ loading.value = false;
+ }
+}
+
+function handleBack() {
+ tabs.closeCurrentTab();
+ router.push({ path: '/mes/pro/feedback' });
+}
+
+onMounted(() => {
+ feedbackId.value = Number(props.id);
+ loadFeedbackDetail();
+});
+</script>
+
+<template>
+ <Page
+ auto-content-height
+ :loading="loading"
+ :title="feedback.code ? `鐢熶骇鎶ュ伐璇︽儏 - ${feedback.code}` : '鐢熶骇鎶ュ伐璇︽儏'"
+ >
+ <Card class="min-h-[10%]">
+ <Descriptions :data="feedback" />
+ </Card>
+ <Card v-if="feedback.paramValues && feedback.paramValues.length > 0" class="mt-3" title="宸ヨ壓鍙傛暟">
+ <Table
+ :columns="paramColumns"
+ :data-source="feedback.paramValues"
+ :pagination="false"
+ row-key="detailId"
+ size="small"
+ >
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.key === 'paramType'">
+ <DictTag
+ v-if="record.paramType !== undefined && record.paramType !== null"
+ :type="DICT_TYPE.MES_PD_PARAM_TYPE"
+ :value="record.paramType"
+ />
+ </template>
+ <template v-else-if="column.key === 'required'">
+ <DictTag
+ :type="DICT_TYPE.INFRA_BOOLEAN_STRING"
+ :value="record.required ? 'true' : 'false'"
+ />
+ </template>
+ </template>
+ </Table>
+ </Card>
+
+ </Page>
+</template>
diff --git a/src/views/mes/pro/feedback/index.vue b/src/views/mes/pro/feedback/index.vue
index adb6ae1..c56983a 100644
--- a/src/views/mes/pro/feedback/index.vue
+++ b/src/views/mes/pro/feedback/index.vue
@@ -19,6 +19,7 @@
import { $t } from '#/locales';
import { ref } from 'vue';
+import { useRouter } from 'vue-router';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -26,6 +27,7 @@
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id; // 褰撳墠鐧诲綍鐢ㄦ埛 ID锛岀敤浜庡鎵逛汉鏉冮檺鍒ゆ柇
+const router = useRouter();
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
@@ -57,6 +59,15 @@
/** 瀹℃壒鐢熶骇鎶ュ伐 */
function handleApprove(row: MesProFeedbackApi.Feedback) {
formModalApi.setData({ formType: 'approve', id: row.id }).open();
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: MesProFeedbackApi.Feedback) {
+ if (!row.processInstanceId) {
+ message.warning('璇ユ姤宸ュ皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } });
}
/** 璇︽儏鐢熶骇鎶ュ伐 */
@@ -165,14 +176,21 @@
ifShow: () => row.status === MesProFeedbackStatusEnum.PREPARE,
onClick: handleSubmit.bind(null, row),
},
+ // {
+ // label: $t('common.approve'),
+ // type: 'link',
+ // auth: ['mes:pro-feedback:approve'],
+ // ifShow: () =>
+ // row.status === MesProFeedbackStatusEnum.APPROVING &&
+ // row.approveUserId === currentUserId,
+ // onClick: handleApprove.bind(null, row),
+ // },
{
- label: $t('common.approve'),
+ label: '鏌ョ湅瀹℃壒',
type: 'link',
- auth: ['mes:pro-feedback:approve'],
- ifShow: () =>
- row.status === MesProFeedbackStatusEnum.APPROVING &&
- row.approveUserId === currentUserId,
- onClick: handleApprove.bind(null, row),
+ icon: ACTION_ICON.VIEW,
+ ifShow: () => row.status === MesProFeedbackStatusEnum.APPROVING,
+ onClick: handleViewProcess.bind(null, row),
},
{
label: $t('common.delete'),
diff --git a/src/views/mes/pro/feedback/modules/batch-feedback.vue b/src/views/mes/pro/feedback/modules/batch-feedback.vue
index e8e08b2..7458c64 100644
--- a/src/views/mes/pro/feedback/modules/batch-feedback.vue
+++ b/src/views/mes/pro/feedback/modules/batch-feedback.vue
@@ -24,8 +24,6 @@
selectedTasks.value = [];
formData.value = {
feedbackQuantity: undefined,
- qualifiedQuantity: undefined,
- unqualifiedQuantity: undefined,
remark: '',
};
}
@@ -36,8 +34,6 @@
const selectedTasks = ref<MesProTaskApi.Task[]>([]);
const formData = ref({
feedbackQuantity: undefined as number | undefined,
- qualifiedQuantity: undefined as number | undefined,
- unqualifiedQuantity: undefined as number | undefined,
remark: '',
});
const submitting = ref(false);
@@ -68,8 +64,6 @@
const result = await batchCreateFeedback({
taskIds: selectedTasks.value.map((t) => t.id!),
feedbackQuantity: formData.value.feedbackQuantity,
- qualifiedQuantity: formData.value.qualifiedQuantity,
- unqualifiedQuantity: formData.value.unqualifiedQuantity,
remark: formData.value.remark || undefined,
});
if (result.failCount) {
@@ -113,24 +107,6 @@
:min="0.01"
:precision="2"
placeholder="璇疯緭鍏ユ姤宸ユ暟閲忥紙搴旂敤鍒版墍鏈夐�変腑浠诲姟锛�"
- class="!w-full"
- />
- </a-form-item>
- <a-form-item label="鍚堟牸鍝佹暟閲�">
- <a-input-number
- v-model:value="formData.qualifiedQuantity"
- :min="0"
- :precision="2"
- placeholder="璇疯緭鍏ュ悎鏍煎搧鏁伴噺"
- class="!w-full"
- />
- </a-form-item>
- <a-form-item label="涓嶈壇鍝佹暟閲�">
- <a-input-number
- v-model:value="formData.unqualifiedQuantity"
- :min="0"
- :precision="2"
- placeholder="璇疯緭鍏ヤ笉鑹搧鏁伴噺"
class="!w-full"
/>
</a-form-item>
diff --git a/src/views/mes/pro/feedback/modules/form.vue b/src/views/mes/pro/feedback/modules/form.vue
index d0e4edf..9e5a9a3 100644
--- a/src/views/mes/pro/feedback/modules/form.vue
+++ b/src/views/mes/pro/feedback/modules/form.vue
@@ -1,4 +1,4 @@
-锘�<script lang="ts" setup>
+<script lang="ts" setup>
import type { FormType } from '../data';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
@@ -11,7 +11,7 @@
import { MesAutoCodeRuleCode, MesProFeedbackStatusEnum } from '@vben/constants';
import { useUserStore } from '@vben/stores';
-import { Button, message, Popconfirm, Tabs } from 'ant-design-vue';
+import { Button, Input, message, Popconfirm, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
@@ -25,9 +25,11 @@
updateFeedback,
} from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
+import { getWorkOrder } from '#/api/mes/pro/workorder';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
+import { findMissingRequiredParams, loadParamDetails, paramDetails } from '../param-state';
import BatchInputDialog from './batch-input-dialog.vue';
import ItemConsumeList from './item-consume-list.vue';
import ProductProduceList from './product-produce-list.vue';
@@ -85,25 +87,20 @@
wrapperClass: 'grid-cols-3',
});
-/** 鎻愪氦鍓嶅榻愭暟閲忥細鏍规嵁 checkFlag 鍐冲畾 uncheck/鍚堟牸/涓嶈壇褰掗浂绛栫暐 */
+/** 鎻愪氦鍓嶅榻愭暟閲忥細鎶ュ伐鍙~鍐欐姤宸ユ暟閲忥紝鍚堟牸/涓嶈壇/搴熷搧鏁伴噺鐢辫川妫�纭畾锛屾姤宸ョ幆鑺備笉閲囬泦 */
function alignQuantity(data: MesProFeedbackApi.Feedback) {
- if (data.checkFlag) {
- data.uncheckQuantity = data.feedbackQuantity;
- data.qualifiedQuantity = 0;
- data.unqualifiedQuantity = 0;
- data.laborScrapQuantity = 0;
- data.materialScrapQuantity = 0;
- data.otherScrapQuantity = 0;
- } else {
- data.feedbackQuantity =
- (data.qualifiedQuantity || 0) + (data.unqualifiedQuantity || 0);
- data.uncheckQuantity = 0;
- }
+ data.uncheckQuantity = data.checkFlag ? data.feedbackQuantity : 0;
+ delete data.qualifiedQuantity;
+ delete data.unqualifiedQuantity;
+ delete data.laborScrapQuantity;
+ delete data.materialScrapQuantity;
+ delete data.otherScrapQuantity;
}
/** 璇诲彇骞舵寜鎻愪氦瑙勫垯瀵归綈鍚庣殑琛ㄥ崟鏁版嵁锛岀敤浜庡揩鐓у拰淇濆瓨 */
async function getAlignedData(): Promise<MesProFeedbackApi.Feedback> {
const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback;
+ syncParamValues(data);
alignQuantity(data);
return data;
}
@@ -112,6 +109,9 @@
async function handleSave() {
const { valid } = await formApi.validate();
if (!valid) {
+ return;
+ }
+ if (!checkRequiredParams()) {
return;
}
modalApi.lock();
@@ -142,10 +142,47 @@
}
}
+const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
+const batchActionType = ref<'' | 'submit' | 'approve'>('');
+
+function syncParamValues(data: MesProFeedbackApi.Feedback) {
+ data.paramValues = paramDetails.value.map((detail) => ({
+ detailId: detail.id,
+ value: detail.paramValue,
+ }));
+}
+
+/** 鏍¢獙蹇呭~鍙傛暟鏄惁濉啓瀹屾暣 */
+function checkRequiredParams(): boolean {
+ const missing = findMissingRequiredParams();
+ if (missing.length > 0) {
+ message.warning(`蹇呭~鍙傛暟鏈~鍐欙細${missing.join('銆�')}`);
+ return false;
+ }
+ return true;
+}
+
+/** 鎵ц鎻愪氦锛堝惈鎵规灞炴�э級 */
+async function doSubmit(batchFields?: Record<string, unknown>) {
+ if (!formData.value?.id) return;
+ modalApi.lock();
+ try {
+ await submitFeedback({ id: formData.value.id, ...batchFields });
+ await modalApi.close();
+ emit('success');
+ message.success('鎶ュ伐鍗曞凡鎻愪氦');
+ } finally {
+ modalApi.unlock();
+ }
+}
+
/** 鎻愪氦锛氫粎褰撹〃鍗曡緝蹇収鏈夊彉鏇存椂鎵嶅厛淇濆瓨锛屽啀璋冪敤鎻愪氦鎺ュ彛 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid) {
+ return;
+ }
+ if (!checkRequiredParams()) {
return;
}
modalApi.lock();
@@ -156,16 +193,40 @@
await updateFeedback(data);
originalSnapshot.value = JSON.stringify(data);
}
- await submitFeedback(formData.value!.id!);
- await modalApi.close();
- emit('success');
- message.success('鎶ヨ鍗曞凡鎻愪氦');
+
+ // 妫�鏌ユ槸鍚﹂渶瑕佸鎵癸紝鑻ラ渶瑕佸鎵癸紝鍒欎笉闇�瑕佸湪姝ら樁娈靛~鍐欐壒娆″睘鎬э紝鐩存帴鎻愪氦
+ let feedbackApprove = false;
+ if (formData.value?.workOrderId) {
+ try {
+ const workOrder = await getWorkOrder(formData.value.workOrderId);
+ feedbackApprove = workOrder?.feedbackApprove ?? false;
+ } catch {
+ // ignore
+ }
+ }
+
+ if (feedbackApprove) {
+ await doSubmit();
+ return;
+ }
+
+ let checkResult;
+ try {
+ checkResult = await getBatchRequirements(formData.value!.id!);
+ } catch {
+ // ignore
+ }
+
+ if (checkResult?.needBatchInput) {
+ batchActionType.value = 'submit';
+ batchDialogRef.value?.open({ checkResult });
+ } else {
+ await doSubmit();
+ }
} finally {
modalApi.unlock();
}
}
-
-const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
/** 鎵ц瀹℃壒锛堝惈鎵规灞炴�э級 */
async function doApprove(batchFields?: Record<string, unknown>) {
@@ -195,6 +256,7 @@
const checkResult = await getBatchRequirements(formData.value.id);
if (checkResult?.needBatchInput) {
// 闇�瑕佸~鍐欐壒娆″睘鎬э紝寮圭獥鏀堕泦
+ batchActionType.value = 'approve';
batchDialogRef.value?.open({ checkResult });
} else {
await doApprove();
@@ -207,7 +269,11 @@
/** 鎵规寮圭獥纭鍥炶皟 */
function handleBatchConfirm(batchData: Record<string, unknown>) {
- doApprove(batchData);
+ if (batchActionType.value === 'submit') {
+ doSubmit(batchData);
+ } else {
+ doApprove(batchData);
+ }
}
/** 瀹℃壒涓嶉�氳繃 */
@@ -255,6 +321,7 @@
formData.value = undefined;
subTabsName.value = 'itemConsume';
originalSnapshot.value = '';
+ paramDetails.value = [];
return;
}
// 鍔犺浇鏁版嵁
@@ -289,6 +356,7 @@
modalApi.lock();
try {
formData.value = await getFeedback(data.id);
+ await loadParamDetails(formData.value.taskId);
const checkFlag = await resolveCheckFlag(
formData.value.routeId,
formData.value.processId,
@@ -307,6 +375,18 @@
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
+ <div v-if="paramDetails.length" class="mx-4 mt-4 rounded border border-gray-200 p-4">
+ <div class="mb-3 font-medium">宸ヨ壓鍙傛暟</div>
+ <div class="grid grid-cols-2 gap-3">
+ <div v-for="detail in paramDetails" :key="detail.id" class="flex items-center gap-2">
+ <span class="w-44 text-right text-sm text-gray-600">
+ <span v-if="detail.required" class="text-red-500">* </span>
+ {{ detail.paramName || detail.paramCode }}{{ detail.unitMeasureName ? `锛�${detail.unitMeasureName}锛塦 : '' }}
+ </span>
+ <Input v-model:value="detail.paramValue" :disabled="!isEditable" placeholder="璇疯緭鍏ュ弬鏁板��" />
+ </div>
+ </div>
+ </div>
<Tabs
v-if="showSubTabs"
v-model:active-key="subTabsName"
diff --git a/src/views/mes/pro/feedback/param-state.ts b/src/views/mes/pro/feedback/param-state.ts
new file mode 100644
index 0000000..2b3599a
--- /dev/null
+++ b/src/views/mes/pro/feedback/param-state.ts
@@ -0,0 +1,47 @@
+import { ref } from 'vue';
+
+import { getWorkOrderParamRecordPage } from '#/api/mes/pro/workorderparamrecord';
+
+/** 鎶ュ伐琛ㄥ崟瑕佸~鍐欑殑鍙傛暟椤癸紙鏉ヨ嚜浠诲姟鍙傛暟璁板綍蹇収锛� */
+export interface ParamDetailItem {
+ id: number;
+ paramCode?: string;
+ paramName?: string;
+ paramType?: number;
+ unitMeasureName?: string;
+ paramValue?: string;
+ required?: boolean;
+}
+
+/** 褰撳墠鎶ュ伐琛ㄥ崟鐨勫弬鏁版槑缁嗭紙鍏变韩缁欒〃鍗曚笌浠诲姟閫夋嫨鑱斿姩锛� */
+export const paramDetails = ref<ParamDetailItem[]>([]);
+
+/** 鎸変换鍔″姞杞藉弬鏁版槑缁嗭紙浠诲姟鏃犲弬鏁拌褰曟椂涓虹┖鏁扮粍锛� */
+export async function loadParamDetails(taskId?: number) {
+ paramDetails.value = [];
+ if (!taskId) {
+ return;
+ }
+ const page = await getWorkOrderParamRecordPage({
+ taskId,
+ pageNo: 1,
+ pageSize: 1,
+ });
+ const record = page.list?.[0];
+ paramDetails.value = (record?.details ?? []).map((detail) => ({
+ id: detail.id!,
+ paramCode: detail.paramCode,
+ paramName: detail.paramName,
+ paramType: detail.paramType,
+ unitMeasureName: detail.unitMeasureName,
+ paramValue: detail.paramValue,
+ required: detail.required,
+ }));
+}
+
+/** 鏍¢獙蹇呭~鍙傛暟鏄惁閮藉凡濉啓锛涜繑鍥炴湭濉啓鐨勫弬鏁板悕 */
+export function findMissingRequiredParams(): string[] {
+ return paramDetails.value
+ .filter((detail) => detail.required && !detail.paramValue?.trim())
+ .map((detail) => detail.paramName || detail.paramCode || '');
+}
diff --git a/src/views/mes/pro/mps/index.vue b/src/views/mes/pro/mps/index.vue
index c0dcdbf..f481b13 100644
--- a/src/views/mes/pro/mps/index.vue
+++ b/src/views/mes/pro/mps/index.vue
@@ -66,13 +66,13 @@
}
/** 鍒犻櫎 */
-async function handleDelete(ids: number[]) {
+async function handleDelete(row: MesProMpsApi.Mps) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting'),
duration: 0,
});
try {
- await deleteMps(ids);
+ await deleteMps(row.id!);
message.success($t('ui.actionMessage.deleteSuccess'));
handleRefresh();
} finally {
@@ -310,7 +310,7 @@
ifShow: () => row.status === MpsStatusEnum.DRAFT,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
- confirm: handleDelete.bind(null, [row.id!]),
+ confirm: handleDelete.bind(null, row),
},
},
]"
diff --git a/src/views/mes/pro/route/data.ts b/src/views/mes/pro/route/data.ts
index d59d436..6d8a693 100644
--- a/src/views/mes/pro/route/data.ts
+++ b/src/views/mes/pro/route/data.ts
@@ -18,6 +18,7 @@
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { getTemplatePage } from '#/api/mes/pro/processdesign/template';
import {
MdItemSelect,
MdProductBomSelect,
@@ -216,15 +217,37 @@
rules: 'selectRequired',
},
{
+ fieldName: 'paramTemplateId',
+ label: '宸ヨ壓鍙傛暟妯℃澘',
+ component: 'ApiSelect',
+ componentProps: {
+ api: () => getTemplatePage({ pageNo: 1, pageSize: 100 }),
+ labelField: 'templateName',
+ valueField: 'id',
+ resultField: 'list',
+ allowClear: true,
+ placeholder: '璇烽�夋嫨宸ヨ壓鍙傛暟妯℃澘',
+ },
+ },
+ {
fieldName: 'linkType',
label: '涓庝笅閬撳伐搴忓叧绯�',
component: 'Select',
+ help: () =>
+ h(
+ 'div',
+ {
+ style:
+ 'white-space: pre-line; max-width: 320px; line-height: 1.6;',
+ },
+ '0=寮�濮�-寮�濮�(SS)锛氭湰宸ュ簭寮�濮嬪悗锛屼笅閬撳伐搴忔墠鑳藉紑濮嬶紙鍙噸鍙犲苟琛屽紑宸ワ級\n1=缁撴潫-缁撴潫(FF)锛氫笅閬撳伐搴忓繀椤荤瓑鍒版湰宸ュ簭缁撴潫鎵嶈兘缁撴潫锛堝苟琛屼絾鍚屾瀹屽伐锛塡n2=寮�濮�-缁撴潫(SF)锛氭湰宸ュ簭寮�濮嬪悗锛屼笅閬撳伐搴忔墠鑳界粨鏉焅n3=缁撴潫-寮�濮�(FS)锛氭湰宸ュ簭缁撴潫鍚庯紝涓嬮亾宸ュ簭鎵嶈兘寮�濮嬶紙榛樿鍊硷級',
+ ),
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'),
placeholder: '璇烽�夋嫨',
},
- rules: z.number().default(3),
+ rules: z.number().optional(),
},
{
fieldName: 'colorCode',
diff --git a/src/views/mes/pro/route/modules/process-form.vue b/src/views/mes/pro/route/modules/process-form.vue
index b742c63..6dcab06 100644
--- a/src/views/mes/pro/route/modules/process-form.vue
+++ b/src/views/mes/pro/route/modules/process-form.vue
@@ -31,7 +31,7 @@
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-1',
- labelWidth: 130,
+ labelWidth: 150,
},
layout: 'horizontal',
schema: [],
diff --git a/src/views/mes/pro/task/data.ts b/src/views/mes/pro/task/data.ts
index a24e576..7dae255 100644
--- a/src/views/mes/pro/task/data.ts
+++ b/src/views/mes/pro/task/data.ts
@@ -1,5 +1,9 @@
-锘縤mport type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import { z } from '#/adapter/form';
+import { getSimpleUserListByRoleCode } from '#/api/system/user';
+import { getTemplatePage } from '#/api/mes/pro/processdesign/template';
+import { getUserListByWorkstationId } from '#/api/mes/md/workstation/worker';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
@@ -130,19 +134,43 @@
width: 80,
},
{
+ field: 'status',
+ title: '璁㈠崟鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
+ },
+ },
+ {
+ field: 'productionStatus',
+ title: '鐢熶骇鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS },
+ },
+ },
+ {
field: 'quantity',
title: '璁㈠崟鏁伴噺',
width: 100,
},
- {
- field: 'quantityChanged',
- title: '璋冩暣鏁伴噺',
- width: 100,
- },
+ // {
+ // field: 'quantityChanged',
+ // title: '璋冩暣鏁伴噺',
+ // width: 100,
+ // },
{
field: 'quantityProduced',
title: '宸茬敓浜ф暟閲�',
width: 100,
+ },
+ {
+ field: 'progressPercent',
+ title: '瀹屾垚杩涘害',
+ width: 150,
+ slots: { default: 'progressPercent' },
},
{
field: 'clientCode',
@@ -160,15 +188,7 @@
width: 120,
formatter: 'formatDate',
},
- {
- field: 'status',
- title: '鎺掍骇鐘舵��',
- width: 100,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
- },
- },
+
{
title: '鎿嶄綔',
width: 160,
@@ -335,6 +355,12 @@
width: 120,
},
{
+ field: 'paramTemplateId',
+ title: '宸ヨ壓鍙傛暟妯℃澘',
+ minWidth: 140,
+ formatter: ({ cellValue }) => cellValue ? `妯℃澘 #${cellValue}` : '鏈厤缃�',
+ },
+ {
field: 'quantity',
title: '鎺掍骇鏁伴噺',
width: 100,
@@ -383,6 +409,19 @@
/** 鐢熶骇浠诲姟鏂板/淇敼鐨勮〃鍗� */
export function useTaskFormSchema(formApi?: VbenFormApi, workOrderQuantity?: Ref<number | undefined>): VbenFormSchema[] {
return [
+ {
+ fieldName: 'paramTemplateId',
+ label: '宸ヨ壓鍙傛暟妯℃澘',
+ component: 'ApiSelect',
+ componentProps: {
+ api: () => getTemplatePage({ pageNo: 1, pageSize: 100 }),
+ labelField: 'templateName',
+ valueField: 'id',
+ resultField: 'list',
+ allowClear: true,
+ placeholder: '璇烽�夋嫨宸ヨ壓鍙傛暟妯℃澘',
+ },
+ },
{
fieldName: 'workstationId',
label: '宸ヤ綔绔�',
@@ -458,6 +497,27 @@
},
},
{
+ fieldName: 'assigneeUserIds',
+ label: '鎶ュ伐浜�',
+ component: 'ApiSelect',
+ componentProps: {
+ placeholder: '璇烽�夋嫨鎶ュ伐浜�',
+ api: getUserListByWorkstationId,
+ labelField: 'nickname',
+ valueField: 'id',
+ mode: 'multiple',
+ },
+ dependencies: {
+ triggerFields: ['workstationId'],
+ componentProps: (values) => ({
+ params: { workstationId: values.workstationId },
+ disabled: !values.workstationId,
+ placeholder: values.workstationId ? '璇烽�夋嫨鎶ュ伐浜�' : '璇峰厛閫夋嫨宸ヤ綔绔�',
+ }),
+ },
+ rules: z.array(z.number()).min(1, '璇烽�夋嫨鎶ュ伐浜�').default([]),
+ },
+ {
fieldName: 'remark',
label: '澶囨敞',
component: 'Textarea',
@@ -474,6 +534,18 @@
async function recalcDuration(formApi?: VbenFormApi) {
if (!formApi) return;
const values = await formApi.getValues();
+
+ if (values.workstationId) {
+ try {
+ const users = await getUserListByWorkstationId({ workstationId: values.workstationId });
+ if (!users || users.length === 0) {
+ message.warning('褰撳墠宸ヤ綔绔欐病鏈夐厤缃汉鍔涜祫婧�');
+ }
+ } catch {
+ // ignore
+ }
+ }
+
if (!values.workstationId || !values.quantity) return;
try {
const res = await calculateTaskDuration(values.workstationId, values.quantity);
diff --git a/src/views/mes/pro/task/edit/index.vue b/src/views/mes/pro/task/edit/index.vue
index 5fac308..93474da 100644
--- a/src/views/mes/pro/task/edit/index.vue
+++ b/src/views/mes/pro/task/edit/index.vue
@@ -4,6 +4,7 @@
import { Page } from '@vben/common-ui';
import { Badge, Button, message } from 'ant-design-vue';
+import dayjs from 'dayjs';
import { getGanttTaskList, updateTask } from '#/api/mes/pro/task';
@@ -38,9 +39,9 @@
changes.map((change) =>
updateTask({
duration: change.duration,
- endTime: change.endTime,
+ endTime: change.endTime ? dayjs(change.endTime).format('YYYY-MM-DD HH:mm:ss') : undefined,
id: change.id,
- startTime: change.startTime,
+ startTime: change.startTime ? dayjs(change.startTime).format('YYYY-MM-DD HH:mm:ss') : undefined,
}),
),
);
diff --git a/src/views/mes/pro/task/index.vue b/src/views/mes/pro/task/index.vue
index bd75f40..ba6ff48 100644
--- a/src/views/mes/pro/task/index.vue
+++ b/src/views/mes/pro/task/index.vue
@@ -1,42 +1,40 @@
-锘�<script lang="ts" setup>
+<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+import {getWorkOrderSchedulePage, type MesProWorkOrderApi} from '#/api/mes/pro/workorder';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import {
+ MesProWorkOrderProductionStatusEnum,
MesProWorkOrderStatusEnum,
MesProWorkOrderTypeEnum,
} from '@vben/constants';
-import { Button, Card, message } from 'ant-design-vue';
+import { Button, Card, Progress, Tabs } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getGanttTaskList, previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
+import { getGanttTaskList } from '#/api/mes/pro/task';
import { getWorkOrderPage } from '#/api/mes/pro/workorder';
import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
import { GanttChart } from './components';
import { useGridColumns, useGridFormSchema } from './data';
import ScheduleForm from './modules/schedule-form.vue';
-import SchedulePreview from '../workorder-center/modules/schedule-preview.vue';
const router = useRouter();
const ganttTasks = ref<any[]>([]); // 鐢樼壒鍥鹃瑙堟暟鎹�
const archiveVisible = ref(false);
const archiveProductCode = ref('');
+const activeTab = ref('PENDING'); // 榛樿閫変腑寰呮帓浜�
+
const [ScheduleModal, scheduleModalApi] = useVbenModal({
connectedComponent: ScheduleForm,
destroyOnClose: true,
});
-const [SchedulePreviewModal, schedulePreviewModalApi] = useVbenModal({
- connectedComponent: SchedulePreview,
- destroyOnClose: true,
-});
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
@@ -63,28 +61,6 @@
}
/** 涓�閿帓浜ч瑙� */
-async function handleAutoSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
- try {
- const preview = await previewAutoGenerate({ workOrderId: row.id! });
- schedulePreviewModalApi
- .setData({ workOrder: row, preview })
- .open();
- } catch {
- message.error('鑾峰彇鎺掍骇棰勮澶辫触');
- }
-}
-
-/** 涓�閿帓浜ф墽琛� */
-async function handleAutoSchedule(row: MesProWorkOrderApi.WorkOrder) {
- try {
- const result = await autoGenerateTasks({ workOrderId: row.id! });
- message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
- handleRefresh();
- } catch {
- message.error('鎺掍骇澶辫触');
- }
-}
-
/** 鎵撳紑褰掓。璇︽儏 */
function handleArchive(row: MesProWorkOrderApi.WorkOrder) {
archiveProductCode.value = row.productCode || '';
@@ -94,6 +70,30 @@
/** 鎵撳紑鐢樼壒鍥剧紪杈戦〉闈� */
function handleGanttEdit() {
router.push({ name: 'MesProTaskGanttEdit' });
+}
+
+/** 鍒ゆ柇璁㈠崟鏄惁涓衡�滃凡瀹屾垚绫诲瀷鈥濓細璁㈠崟鐘舵��=宸插畬鎴� 鎴� 鐢熶骇鐘舵��=鐢熶骇瀹屾垚 */
+function isCompletedWorkOrder(row: MesProWorkOrderApi.WorkOrder): boolean {
+ return (
+ row.status === MesProWorkOrderStatusEnum.FINISHED ||
+ row.productionStatus === MesProWorkOrderProductionStatusEnum.FINISHED
+ );
+}
+
+/**
+ * 宸插畬鎴愮被鍨嬭鐨勨�滃畬鎴愭�佲�濆厹搴曞睍绀猴細
+ * 宸茬敓浜ф暟閲忔寜璁㈠崟鏁伴噺灞曠ず锛堣嫢浣庝簬璁㈠崟鏁伴噺锛夛紝浣垮畬鎴愯繘搴︽樉绀轰负 100%銆�
+ * 浠呬綔鐢ㄤ簬鏈〉鍒楄〃灞曠ず锛屼笉淇敼鍚庣鏁版嵁锛屼笉褰卞搷搴撳瓨/鎶ュ伐/瀵煎嚭绛夌湡瀹炲彛寰勩��
+ */
+function normalizeCompletionDisplay(row: MesProWorkOrderApi.WorkOrder): void {
+ if (!isCompletedWorkOrder(row)) {
+ return;
+ }
+ const quantity = row.quantity ?? 0;
+ const produced = row.quantityProduced ?? 0;
+ if (produced < quantity) {
+ row.quantityProduced = quantity;
+ }
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -106,13 +106,27 @@
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
- return await getWorkOrderPage({
+ let scheduleStatus: number | undefined = undefined;
+
+ if (activeTab.value === 'PENDING') {
+ scheduleStatus = 0; // 鏈畬鎴�
+ } else if (activeTab.value === 'FINISHED') {
+ scheduleStatus = 1; // 宸插畬鎴�
+ }
+ // activeTab === 'ALL' 鏃朵笉浼�
+
+ const res = await getWorkOrderSchedulePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
- status: MesProWorkOrderStatusEnum.CONFIRMED,
+ scheduleStatus,
type: MesProWorkOrderTypeEnum.SELF,
...formValues,
});
+ // 宸插畬鎴愮被鍨嬭锛氬凡鐢熶骇鏁伴噺/瀹屾垚杩涘害鍋氣�滃畬鎴愭�佲�濆厹搴曞睍绀猴紙浠呮湰椤靛睍绀猴紝涓嶆敼搴擄級
+ if (res?.list) {
+ res.list.forEach(normalizeCompletionDisplay);
+ }
+ return res;
},
},
},
@@ -140,14 +154,25 @@
<template>
<Page auto-content-height>
<ScheduleModal @success="handleRefresh" />
- <SchedulePreviewModal @success="handleRefresh" />
-
<!-- 鎺掍骇鐢樼壒鍥鹃瑙� -->
<Card title="鎺掍骇鐢樼壒鍥�" class="mb-4">
+ <template #extra>
+ <span class="text-xs text-gray-400">灞曠ず宸茬‘璁よ嚜鍒惰鍗曠殑浠诲姟璁″垝</span>
+ </template>
<GanttChart :height="350" :readonly="true" :tasks="ganttTasks" />
</Card>
- <Grid table-title="寰呮帓浜ц鍗�">
+ <Grid>
+ <template #table-title>
+ <div class="flex flex-col gap-2">
+ <span class="text-base font-medium">鐢熶骇璁㈠崟鍒楄〃</span>
+ <Tabs v-model:activeKey="activeTab" @change="handleRefresh" :tabBarStyle="{ margin: 0, borderBottom: 0 }">
+ <Tabs.TabPane key="ALL" tab="鍏ㄩ儴" />
+ <Tabs.TabPane key="PENDING" tab="鏈畬鎴�" />
+ <Tabs.TabPane key="FINISHED" tab="宸插畬鎴�" />
+ </Tabs>
+ </div>
+ </template>
<template #toolbar-tools>
<TableAction
:actions="[
@@ -165,14 +190,22 @@
{{ row.code }}
</Button>
</template>
+ <template #progressPercent="{ row }">
+ <Progress
+ :percent="Math.min(100, Math.round((row.quantity ? ((row.quantityProduced || 0) / row.quantity) * 100 : 0)))"
+ size="small"
+ />
+ </template>
<template #actions="{ row }">
<TableAction
:actions="[
{
- label: '鎺掍骇',
+ label: '鎺掍骇缁存姢',
type: 'link',
auth: ['mes:pro-task:create'],
- ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED,
+ ifShow:
+ row.status === MesProWorkOrderStatusEnum.CONFIRMED &&
+ !isCompletedWorkOrder(row),
onClick: handleSchedule.bind(null, row),
},
{
diff --git a/src/views/mes/pro/task/modules/schedule-form.vue b/src/views/mes/pro/task/modules/schedule-form.vue
index 8f67bd3..f459bfb 100644
--- a/src/views/mes/pro/task/modules/schedule-form.vue
+++ b/src/views/mes/pro/task/modules/schedule-form.vue
@@ -1,4 +1,4 @@
-锘�<script lang="ts" setup>
+<script lang="ts" setup>
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
@@ -50,16 +50,19 @@
message.warning('褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅');
return;
}
+ // 纭畾宸ヨ壓璺嚎ID锛氬伐鍗曡矾绾� 鈫� 浜у搧璺嚎
+ let tempRouteId = 0;
+ if (workOrder.value?.routeId) {
+ tempRouteId = workOrder.value.routeId;
+ } else {
+ const processes = await getRouteProcessListByProduct(productId);
+ tempRouteId = processes?.[0]?.routeId ?? 0;
+ }
+ currentRouteId.value = tempRouteId;
+
routeProcessList.value = [...woProcesses].toSorted(
(a, b) => (a.sort ?? 0) - (b.sort ?? 0),
);
- // 纭畾宸ヨ壓璺嚎ID锛氬伐鍗曡矾绾� 鈫� 浜у搧璺嚎
- if (workOrder.value?.routeId) {
- currentRouteId.value = workOrder.value.routeId;
- } else {
- const processes = await getRouteProcessListByProduct(productId);
- currentRouteId.value = processes?.[0]?.routeId ?? 0;
- }
}
/** 瀹屾垚璁㈠崟 */
@@ -129,24 +132,25 @@
v-for="rp in routeProcessList"
:key="rp.processId"
:title="rp.processName"
+ :description="rp.paramTemplateId ? `鍙傛暟妯℃澘 #${rp.paramTemplateId}` : '鏈厤缃弬鏁版ā鏉�'"
/>
</Steps>
- <Card
- v-for="(rp, index) in routeProcessList"
- v-show="activeProcessStep === index"
- :key="rp.processId"
- class="mx-4"
- >
- <TaskList
- :color-code="rp.colorCode"
- :disabled="isReadonly"
- :item-id="rp.outputItemId || workOrder.productId"
- :process-id="rp.processId!"
- :route-id="currentRouteId"
- :work-order-id="workOrder.id!"
- :work-order-quantity="workOrder.quantity"
- />
- </Card>
+ <template v-for="(rp, index) in routeProcessList" :key="rp.processId">
+ <Card
+ v-if="activeProcessStep === index"
+ class="mx-4"
+ >
+ <TaskList
+ :color-code="rp.colorCode"
+ :disabled="isReadonly"
+ :item-id="rp.outputItemId || workOrder.productId"
+ :process-id="rp.processId!"
+ :route-id="currentRouteId"
+ :work-order-id="workOrder.id!"
+ :work-order-quantity="workOrder.quantity"
+ />
+ </Card>
+ </template>
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
diff --git a/src/views/mes/pro/task/modules/task-form.vue b/src/views/mes/pro/task/modules/task-form.vue
index fd83dcd..acb2de8 100644
--- a/src/views/mes/pro/task/modules/task-form.vue
+++ b/src/views/mes/pro/task/modules/task-form.vue
@@ -1,4 +1,4 @@
-锘�<script lang="ts" setup>
+<script lang="ts" setup>
import type { MesProTaskApi } from '#/api/mes/pro/task';
import { computed, ref } from 'vue';
@@ -72,8 +72,6 @@
context.value = {};
return;
}
- formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
- // 鍔犺浇鏁版嵁
const data = modalApi.getData<{
colorCode?: string;
id?: number;
@@ -83,30 +81,39 @@
routeId?: number;
workOrderId?: number;
}>();
+
+ let wid: number | undefined;
+
if (data?.id) {
+ // 缂栬緫妯″紡锛氬厛璁剧疆琛ㄥ崟 schema锛屽啀鍥炲~浠诲姟鏁版嵁锛岄伩鍏嶅瓧娈垫湭寤虹珛鏃� setValues 涓㈠け
+ formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
modalApi.lock();
try {
formData.value = await getTask(data.id);
+ wid = formData.value.workOrderId;
// 璁剧疆鍒� values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
- return;
+ } else {
+ context.value = {
+ colorCode: data.colorCode,
+ itemId: data.itemId,
+ processId: data.processId,
+ routeId: data.routeId,
+ workOrderId: data.workOrderId,
+ };
+ wid = data.workOrderId;
+ workOrderQuantity.value = data.quantity;
+ // 鏂板妯″紡锛氬厛璁剧疆琛ㄥ崟 schema锛屽啀璁剧疆鍒濆鍊�
+ formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) });
+ await formApi.setValues({
+ colorCode: data.colorCode || '#00AEF3',
+ duration: 1,
+ quantity: data.quantity,
+ });
}
- context.value = {
- colorCode: data.colorCode,
- itemId: data.itemId,
- processId: data.processId,
- routeId: data.routeId,
- workOrderId: data.workOrderId,
- };
- workOrderQuantity.value = data.quantity;
- await formApi.setValues({
- colorCode: data.colorCode || '#00AEF3',
- duration: 1,
- quantity: data.quantity,
- });
},
});
</script>
diff --git a/src/views/mes/pro/task/modules/task-list.vue b/src/views/mes/pro/task/modules/task-list.vue
index d126366..12053cb 100644
--- a/src/views/mes/pro/task/modules/task-list.vue
+++ b/src/views/mes/pro/task/modules/task-list.vue
@@ -14,6 +14,7 @@
import { useTaskGridColumns } from '../data';
import TaskForm from './task-form.vue';
+// import QuickFeedbackForm from '../../workorder-center/modules/quick-feedback.vue'; // 鎶ュ伐鎿嶄綔鏆傞殣钘�
const props = defineProps<{
colorCode?: string;
@@ -31,6 +32,11 @@
connectedComponent: TaskForm,
destroyOnClose: true,
});
+
+// const [QuickFeedbackModal, quickFeedbackModalApi] = useVbenModal({
+// connectedComponent: QuickFeedbackForm,
+// destroyOnClose: true,
+// });
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
@@ -55,6 +61,18 @@
function handleEdit(row: MesProTaskApi.Task) {
taskFormModalApi.setData({ id: row.id }).open();
}
+
+// /** 蹇�熸姤宸� */
+// function handleQuickFeedback(row: MesProTaskApi.Task) {
+// quickFeedbackModalApi
+// .setData({
+// taskId: row.id,
+// taskCode: row.code,
+// taskQuantity: row.quantity,
+// taskProducedQuantity: row.producedQuantity,
+// })
+// .open();
+// }
/** 鍒犻櫎浠诲姟 */
async function handleDelete(row: MesProTaskApi.Task) {
@@ -107,14 +125,17 @@
// 宸ュ簭姝ラ鍒囨崲鏃堕噸鏂板姞杞藉綋鍓嶅伐搴忕殑浠诲姟
watch(
- () => props.processId,
+ () => [props.workOrderId, props.routeId, props.processId],
() => handleRefresh(),
);
+
+
</script>
<template>
<div>
<TaskFormModal @success="handleRefresh" />
+ <!-- <QuickFeedbackModal @success="handleRefresh" /> -->
<Grid table-title="鐢熶骇浠诲姟">
<template v-if="editable" #toolbar-tools>
<TableAction
@@ -138,6 +159,13 @@
<template #actions="{ row }">
<TableAction
:actions="[
+ // {
+ // label: '鎶ュ伐',
+ // type: 'link',
+ // auth: ['mes:pro-feedback:create'],
+ // ifShow: (row.quantity ?? 0) > (row.producedQuantity ?? 0),
+ // onClick: handleQuickFeedback.bind(null, row),
+ // },
{
label: $t('common.edit'),
type: 'link',
diff --git a/src/views/mes/pro/workorder-center/modules/quick-feedback.vue b/src/views/mes/pro/workorder-center/modules/quick-feedback.vue
index b249fdf..9df47ce 100644
--- a/src/views/mes/pro/workorder-center/modules/quick-feedback.vue
+++ b/src/views/mes/pro/workorder-center/modules/quick-feedback.vue
@@ -9,6 +9,10 @@
import { message } from 'ant-design-vue';
import { quickCreateFeedback } from '#/api/mes/pro/feedback';
+import {
+ loadParamDetails,
+ paramDetails,
+} from '#/views/mes/pro/feedback/param-state';
import ProTaskSelect from '#/views/mes/pro/task/components/select.vue';
defineOptions({ name: 'QuickFeedbackForm' });
@@ -35,10 +39,14 @@
selectedTask.value = undefined;
formData.value = {
feedbackQuantity: undefined,
- qualifiedQuantity: undefined,
- unqualifiedQuantity: undefined,
remark: '',
};
+ // 棰勮浠诲姟鏃跺姞杞藉叾宸ヨ壓鍙傛暟锛涘惁鍒欑瓑寰呴�夋嫨浠诲姟鍚庡啀鍔犺浇
+ if (props.taskId) {
+ loadParamDetails(props.taskId);
+ } else {
+ paramDetails.value = [];
+ }
}
},
});
@@ -48,8 +56,6 @@
const selectedTask = ref<MesProTaskApi.Task>();
const formData = ref({
feedbackQuantity: undefined as number | undefined,
- qualifiedQuantity: undefined as number | undefined,
- unqualifiedQuantity: undefined as number | undefined,
remark: '',
});
const submitting = ref(false);
@@ -70,6 +76,7 @@
function handleTaskChange(task: MesProTaskApi.Task | undefined) {
selectedTask.value = task;
selectedTaskId.value = task?.id;
+ loadParamDetails(task?.id);
}
async function handleSubmit() {
@@ -87,8 +94,10 @@
await quickCreateFeedback({
taskId,
feedbackQuantity: formData.value.feedbackQuantity,
- qualifiedQuantity: formData.value.qualifiedQuantity,
- unqualifiedQuantity: formData.value.unqualifiedQuantity,
+ paramValues: paramDetails.value.map((detail) => ({
+ detailId: detail.id,
+ value: detail.paramValue,
+ })),
remark: formData.value.remark || undefined,
});
message.success('鎶ュ伐鎴愬姛');
@@ -128,24 +137,27 @@
class="!w-full"
/>
</a-form-item>
- <a-form-item label="鍚堟牸鍝佹暟閲�">
- <a-input-number
- v-model:value="formData.qualifiedQuantity"
- :min="0"
- :precision="2"
- placeholder="璇疯緭鍏ュ悎鏍煎搧鏁伴噺"
- class="!w-full"
- />
- </a-form-item>
- <a-form-item label="涓嶈壇鍝佹暟閲�">
- <a-input-number
- v-model:value="formData.unqualifiedQuantity"
- :min="0"
- :precision="2"
- placeholder="璇疯緭鍏ヤ笉鑹搧鏁伴噺"
- class="!w-full"
- />
- </a-form-item>
+ <div v-if="paramDetails.length" class="mb-4">
+ <div class="mb-1 font-medium">宸ヨ壓鍙傛暟</div>
+ <div
+ v-for="detail in paramDetails"
+ :key="detail.id"
+ class="mb-2 flex items-center gap-2"
+ >
+ <span class="w-40 flex-shrink-0 text-right text-sm text-gray-600">
+ <span v-if="detail.required" class="text-red-500">* </span>
+ {{ detail.paramName || detail.paramCode
+ }}{{
+ detail.unitMeasureName ? `锛�${detail.unitMeasureName}锛塦 : ''
+ }}
+ </span>
+ <a-input
+ v-model:value="detail.paramValue"
+ placeholder="璇疯緭鍏ュ弬鏁板��"
+ class="!w-full"
+ />
+ </div>
+ </div>
<a-form-item label="澶囨敞">
<a-textarea
v-model:value="formData.remark"
diff --git a/src/views/mes/pro/workorder/components/select-dialog.vue b/src/views/mes/pro/workorder/components/select-dialog.vue
index 2f000dc..250ddd8 100644
--- a/src/views/mes/pro/workorder/components/select-dialog.vue
+++ b/src/views/mes/pro/workorder/components/select-dialog.vue
@@ -1,4 +1,4 @@
-锘�<script lang="ts" setup>
+<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
@@ -21,10 +21,12 @@
defineProps<{
status?: number;
type?: number;
+ checkMethod?: (row: MesProWorkOrderApi.WorkOrder) => boolean;
}>(),
{
status: undefined,
type: undefined,
+ checkMethod: undefined,
},
);
const emit = defineEmits<{
@@ -133,10 +135,12 @@
highlight: true,
range: true,
reserve: true,
+ checkMethod: props.checkMethod ? ({ row }) => props.checkMethod!(row) : undefined,
},
radioConfig: {
highlight: true,
trigger: 'row',
+ checkMethod: props.checkMethod ? ({ row }) => props.checkMethod!(row) : undefined,
},
proxyConfig: {
ajax: {
diff --git a/src/views/mes/pro/workorder/components/select.vue b/src/views/mes/pro/workorder/components/select.vue
index 6788174..39dd494 100644
--- a/src/views/mes/pro/workorder/components/select.vue
+++ b/src/views/mes/pro/workorder/components/select.vue
@@ -1,4 +1,4 @@
-锘�<script lang="ts" setup>
+<script lang="ts" setup>
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import { computed, ref, useAttrs, watch } from 'vue';
@@ -21,6 +21,7 @@
placeholder?: string;
status?: number;
type?: number;
+ checkMethod?: (row: MesProWorkOrderApi.WorkOrder) => boolean;
}>(),
{
allowClear: true,
@@ -29,6 +30,7 @@
placeholder: '璇烽�夋嫨璁㈠崟',
status: undefined,
type: undefined,
+ checkMethod: undefined,
},
);
@@ -140,6 +142,7 @@
ref="dialogRef"
:status="status"
:type="type"
+ :check-method="checkMethod"
@selected="handleSelected"
/>
</template>
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index af895bd..de3d920 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -1,6 +1,7 @@
-锘縤mport type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+import { type MesProWorkOrderApi, getWorkOrderApproveProcessList } from '#/api/mes/pro/workorder';
+import { getSimpleRoleList } from '#/api/system/role';
import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
@@ -126,6 +127,10 @@
}
},
},
+ help: (values: { orderSourceType?: number }) =>
+ values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER
+ ? '宸查�夊鎴疯鍗曪細闇�琛ュ厖鏉ユ簮鍗曟嵁缂栧彿涓庡鎴凤紝渚夸簬杩芥函璁㈠崟鏉ユ簮銆�'
+ : '鐢ㄤ簬鍖哄垎璁㈠崟涓氬姟褰掑睘锛涢�夈�屽鎴疯鍗曘�嶆椂鎵嶉渶鍏宠仈鏉ユ簮鍗曟嵁涓庡鎴枫��',
rules: 'selectRequired',
},
{
@@ -141,6 +146,7 @@
show: (values) =>
values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER,
},
+ help: '濉啓涓婃父鍗曟嵁锛堝閿�鍞鍗曪級鐨勭紪鍙凤紝鐢ㄤ簬杩芥函璇ヨ鍗曠殑鏉ユ簮銆�',
},
{
fieldName: 'type',
@@ -160,6 +166,11 @@
}
},
},
+ help: (values: { type?: number }) =>
+ values.type === MesProWorkOrderTypeEnum.OUTSOURCE ||
+ values.type === MesProWorkOrderTypeEnum.PURCHASE
+ ? '宸查�夊澶�/閲囪喘鐢熶骇锛氶渶鎸囧畾渚涘簲鍟嗭紝濮斿鎶ュ伐涓庡叆搴撳皢鎸夎渚涘簲鍟嗙粨绠椼��'
+ : '鑷惀鐢熶骇鎸夋湰鍗曞伐搴忔帓浜э紱閫夈�屽澶栧姞宸ャ�嶆垨銆岄噰璐敓浜с�嶆椂闇�棰濆鎸囧畾渚涘簲鍟嗐��',
rules: 'selectRequired',
},
{
@@ -170,6 +181,7 @@
disabled: headerReadonly,
placeholder: '璇烽�夋嫨浜у搧',
},
+ help: '閫夋嫨浜у搧鍚庯紝宸ュ簭灏嗚嚜鍔ㄤ粠鍏跺伐鑹鸿矾绾垮甫鍑哄苟鎸夐渶璋冩暣锛涗骇鍝侀渶鍏堝湪銆屽伐鑹烘祦绋嬨�嶄腑閰嶇疆璺嚎涓庢姇鏂� BOM銆�',
rules: 'selectRequired',
},
{
@@ -198,6 +210,7 @@
show: (values) =>
values.orderSourceType === MesProWorkOrderSourceTypeEnum.ORDER,
},
+ help: '鍏宠仈瀹㈡埛鍚庯紝璁㈠崟鍙湪瀹㈡埛缁村害杩芥函骞跺鎺ュ悗缁粨绠椼��',
},
{
fieldName: 'vendorId',
@@ -213,6 +226,7 @@
values.type === MesProWorkOrderTypeEnum.OUTSOURCE ||
values.type === MesProWorkOrderTypeEnum.PURCHASE,
},
+ help: '濮斿/閲囪喘璁㈠崟闇�鎸囧畾渚涘簲鍟嗭紝濮斿鎶ュ伐涓庡叆搴撳潎浠ヨ渚涘簲鍟嗕负鍑嗐��',
},
{
fieldName: 'batchCode',
@@ -234,8 +248,61 @@
placeholder: '璇烽�夋嫨闇�姹傛棩鏈�',
valueFormat: 'YYYY-MM-DD',
},
+ help: '棰勮浜や粯/瀹屾垚鏃ユ湡锛岀敤浜庢帓浜с�佽繘搴﹁窡韪笌浜ゆ湡棰勮銆�',
rules: 'required',
},
+ // {
+ // fieldName: 'feedbackRole',
+ // label: '鎶ュ伐瑙掕壊',
+ // component: 'ApiSelect',
+ // componentProps: {
+ // api: getSimpleRoleList,
+ // disabled: headerReadonly,
+ // labelField: 'name',
+ // valueField: 'code',
+ // mode: 'multiple',
+ // placeholder: '璇烽�夋嫨鎶ュ伐瑙掕壊',
+ // },
+ // rules: 'selectRequired',
+ // },
+ {
+ fieldName: 'feedbackApprove',
+ label: '鏄惁鎶ュ伐瀹℃壒',
+ component: 'Switch',
+ componentProps: {
+ class: '!w-auto',
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ disabled: headerReadonly,
+ },
+ defaultValue: false,
+ help: '寮�鍚悗锛岀敓浜ф姤宸ユ彁浜ら渶缁忓鎵归�氳繃鎵嶈鍏ヤ骇閲忥紱鍏抽棴鍒欐姤宸ョ洿鎺ョ敓鏁堛��',
+ rules: 'required',
+ },
+ {
+ fieldName: 'processDefinitionKey',
+ label: '瀹℃壒娴佺▼',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getWorkOrderApproveProcessList,
+ disabled: headerReadonly,
+ labelField: 'name',
+ valueField: 'key',
+ placeholder: '璇烽�夋嫨瀹℃壒娴佺▼',
+ },
+ dependencies: {
+ triggerFields: ['feedbackApprove'],
+ show: (values) => !!values.feedbackApprove,
+ rules(values) {
+ if (values.feedbackApprove) {
+ return 'selectRequired';
+ }
+ return undefined;
+ },
+ },
+ help: '閫夋嫨鎶ュ伐鎻愪氦鍚庤蛋鐨勫鎵规祦绋嬶紱娴佺▼闇�鍏堝湪绯荤粺銆屾祦绋嬬鐞嗐�嶄腑閮ㄧ讲锛屼粎銆屾姤宸ュ鎵广�嶇被娴佺▼鍙�夈��',
+ },
+
{
fieldName: 'remark',
label: '澶囨敞',
@@ -413,6 +480,27 @@
},
},
{
+ field: 'productionStatus',
+ title: '鐢熶骇鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.ORDER_PRODUCTION_STATUS },
+ },
+ },
+ {
+ field: 'processProgressList',
+ title: '宸ュ簭鐢熶骇杩涘害',
+ width: 350,
+ slots: { default: 'processProgressList' },
+ },
+ {
+ field: 'completionProgress',
+ title: '瀹屾垚杩涘害',
+ width: 200,
+ slots: { default: 'completionProgress' },
+ },
+ {
field: 'createTime',
title: '鍒涘缓鏃堕棿',
width: 180,
diff --git a/src/views/mes/pro/workorder/index.vue b/src/views/mes/pro/workorder/index.vue
index e3c8248..6c3ef86 100644
--- a/src/views/mes/pro/workorder/index.vue
+++ b/src/views/mes/pro/workorder/index.vue
@@ -6,13 +6,12 @@
import { Page, useVbenModal } from "@vben/common-ui";
import {
- BarcodeBizTypeEnum,
MesProWorkOrderStatusEnum,
MesProWorkOrderTypeEnum,
} from "@vben/constants";
import { downloadFileFromBlobPart } from "@vben/utils";
- import { Button, message } from "ant-design-vue";
+ import { Button, message, Progress, Steps, Popover, Modal } from "ant-design-vue";
import { ACTION_ICON, TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
import {
@@ -23,7 +22,7 @@
} from "#/api/mes/pro/workorder";
import { $t } from "#/locales";
import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
-import { BarcodeDetail } from '#/views/wls/barcode/components';
+// import { BarcodeDetail } from '#/views/wls/barcode/components';
import { useGridColumns, useGridFormSchema } from "./data";
import Form from "./modules/form.vue";
@@ -33,8 +32,9 @@
destroyOnClose: true,
});
- const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>();
-const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
+ const archiveVisible = ref(false);
+ const archiveProductCode = ref('');
+// const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
@@ -66,6 +66,21 @@
formModalApi.setData({ formType: "finish", id: row.id }).open();
}
+ /** 瀹屾垚璁㈠崟鍓嶆牎楠� */
+ function handleCheckFinish(row: MesProWorkOrderApi.WorkOrder) {
+ if (row.productionStatus !== 2) {
+ Modal.confirm({
+ title: '鎿嶄綔纭',
+ content: '璇ヨ鍗曟湭瀹屾垚锛屾槸鍚︾‘璁よ缁х画瀹屾垚锛�',
+ onOk() {
+ handleFinish(row);
+ },
+ });
+ } else {
+ handleFinish(row);
+ }
+ }
+
/** 鍒犻櫎璁㈠崟 */
async function handleDelete(row: MesProWorkOrderApi.WorkOrder) {
const hideLoading = message.loading({
@@ -88,15 +103,21 @@
handleRefresh();
}
- /** 鏌ョ湅璁㈠崟鏉$爜 */
- function handleBarcode(row: MesProWorkOrderApi.WorkOrder) {
- barcodeDetailRef.value?.openByBusiness(
- row.id!,
- BarcodeBizTypeEnum.WORKORDER,
- row.code,
- row.name
- );
+ /** 鎵撳紑褰掓。璇︽儏 */
+ function handleArchive(row: MesProWorkOrderApi.WorkOrder) {
+ archiveProductCode.value = row.productCode || '';
+ archiveVisible.value = true;
}
+
+ /** 鏌ョ湅璁㈠崟鏉$爜 */
+ // function handleBarcode(row: MesProWorkOrderApi.WorkOrder) {
+ // barcodeDetailRef.value?.openByBusiness(
+ // row.id!,
+ // BarcodeBizTypeEnum.WORKORDER,
+ // row.code,
+ // row.name
+ // );
+ // }
/** 瀵煎嚭琛ㄦ牸 */
async function handleExport() {
@@ -140,6 +161,13 @@
},
} as VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>,
});
+
+ /** 鑾峰彇宸ュ簭鑺傜偣鐘舵�佹牱寮忕被 */
+ function getProcessNodeStatus(percent: number) {
+ if (percent >= 100) return 'completed';
+ if (percent > 0) return 'in-progress';
+ return 'pending';
+ }
</script>
<template>
@@ -170,6 +198,29 @@
{{ row.code }}
</Button>
</template>
+ <template #processProgressList="{ row }">
+ <div v-if="row.processProgressList && row.processProgressList.length > 0" class="process-node-list">
+ <div
+ v-for="(item, index) in row.processProgressList"
+ :key="index"
+ class="process-node-item"
+ >
+ <div class="process-node-dot" :class="getProcessNodeStatus(item.progressPercent || item.progress || 0)"></div>
+ <div class="process-node-info">
+ <span class="process-node-name" :title="item.processName ">{{ item.processName }}</span>
+ <span class="process-node-percent">{{ item.progress || 0 }}%</span>
+ </div>
+ <div v-if="index !== row.processProgressList.length - 1" class="process-node-line"></div>
+ </div>
+ </div>
+ <span v-else>-</span>
+ </template>
+ <template #completionProgress="{ row }">
+ <div style="display: flex; align-items: center; gap: 8px; width: 100%;">
+ <Progress :percent="row.completionProgress || 0" :show-info="false" size="small" style="flex: 1; margin: 0;" />
+ <span style="white-space: nowrap; min-width: 40px; text-align: left;">{{ row.completionProgress || 0 }}%</span>
+ </div>
+ </template>
<template #actions="{ row }">
<TableAction :actions="[
{
@@ -193,7 +244,7 @@
},
},
{
- label: $t('common.create'),
+ label: '鏂板瀛愯鍗�',
type: 'link',
auth: ['mes:pro-work-order:create'],
ifShow:
@@ -208,7 +259,7 @@
ifShow:
row.status === MesProWorkOrderStatusEnum.CONFIRMED &&
!!row.quantityProduced,
- onClick: handleFinish.bind(null, row),
+ onClick: handleCheckFinish.bind(null, row),
},
{
label: '鍙栨秷',
@@ -221,21 +272,97 @@
confirm: handleCancel.bind(null, row),
},
},
- {
- label: '鏉$爜',
- type: 'link',
- auth: ['mes:pro-work-order:query'],
- onClick: handleBarcode.bind(null, row),
- },
+ // {
+ // label: '鏉$爜',
+ // type: 'link',
+ // auth: ['mes:pro-work-order:query'],
+ // onClick: handleBarcode.bind(null, row),
+ // },
{
label: '褰掓。',
type: 'link',
- onClick: () => archiveDetailRef.value?.open(row.productCode),
+ onClick: handleArchive.bind(null, row),
},
]" />
</template>
</Grid>
- <BarcodeDetail ref="barcodeDetailRef" />
- <ArchiveDetail ref="archiveDetailRef" />
+ <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
+ <ArchiveDetail :visible="archiveVisible" :product-code="archiveProductCode" @update:visible="archiveVisible = $event" />
</Page>
</template>
+
+<style scoped>
+.process-node-list {
+ display: flex;
+ align-items: center;
+ overflow-x: auto;
+ overflow-y: hidden;
+ max-width: 100%;
+ padding-bottom: 4px;
+}
+
+/* Custom scrollbar to make it sleek */
+.process-node-list::-webkit-scrollbar {
+ height: 4px;
+}
+.process-node-list::-webkit-scrollbar-thumb {
+ background-color: #d9d9d9;
+ border-radius: 4px;
+}
+.process-node-list::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.process-node-item {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.process-node-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: 6px;
+}
+
+.process-node-dot.completed {
+ background-color: #52c41a;
+}
+.process-node-dot.in-progress {
+ background-color: #1890ff;
+}
+.process-node-dot.pending {
+ background-color: #d9d9d9;
+}
+
+.process-node-info {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin-right: 12px;
+ font-size: 12px;
+ line-height: 1.2;
+}
+
+.process-node-name {
+ color: #333;
+ max-width: 80px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.process-node-percent {
+ color: #888;
+ font-size: 11px;
+}
+
+.process-node-line {
+ width: 20px;
+ height: 1px;
+ background-color: #e8e8e8;
+ margin-right: 12px;
+}
+
+</style>
diff --git a/src/views/mes/pro/workorder/modules/form.vue b/src/views/mes/pro/workorder/modules/form.vue
index 5da9d79..36a346e 100644
--- a/src/views/mes/pro/workorder/modules/form.vue
+++ b/src/views/mes/pro/workorder/modules/form.vue
@@ -8,7 +8,6 @@
import { confirm, useVbenModal } from "@vben/common-ui";
import {
- BarcodeBizTypeEnum,
MesProWorkOrderStatusEnum,
} from "@vben/constants";
@@ -24,7 +23,7 @@
} from "#/api/mes/pro/workorder";
import { batchSaveWorkOrderProcess, getWorkOrderProcessListByWorkOrderId } from "#/api/mes/pro/workorder/process";
import { $t } from "#/locales";
- import { BarcodeDetail } from "#/views/wls/barcode/components";
+ // import { BarcodeDetail } from "#/views/wls/barcode/components";
import { useFormSchema } from "../data";
import PredictDelivery from "./ai/predict-delivery.vue";
@@ -39,7 +38,7 @@
const formData = ref<MesProWorkOrderApi.WorkOrder>();
const originalSnapshot = ref("");
const subTabsName = ref("process");
- const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
+ // const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
const processListRef = ref<InstanceType<typeof ProcessList>>();
const currentProductId = ref<number>(); // 褰撳墠閫変腑鐨勪骇鍝� ID锛堢敤浜庡伐搴忓姞杞斤級
const tempProcessList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]); // 涓存椂宸ュ簭鍒楄〃
@@ -128,7 +127,10 @@
if (!valid || !formData.value?.id) return;
modalApi.lock();
try {
- const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
+ const data = (await formApi.getValues()) as any;
+ if (Array.isArray(data.feedbackRole)) {
+ data.feedbackRole = data.feedbackRole.join(',');
+ }
await updateWorkOrder({ ...formData.value, ...data });
if (tempProcessList.value.length > 0) {
const processList = tempProcessList.value.map(item => ({
@@ -147,17 +149,17 @@
}
/** 鏌ョ湅璁㈠崟鏉$爜 */
- function handleBarcode() {
- if (!formData.value?.id) {
- return;
- }
- barcodeDetailRef.value?.openByBusiness(
- formData.value.id,
- BarcodeBizTypeEnum.WORKORDER,
- formData.value.code,
- formData.value.name
- );
- }
+ // function handleBarcode() {
+ // if (!formData.value?.id) {
+ // return;
+ // }
+ // barcodeDetailRef.value?.openByBusiness(
+ // formData.value.id,
+ // BarcodeBizTypeEnum.WORKORDER,
+ // formData.value.code,
+ // formData.value.name
+ // );
+ // }
/** 宸ュ簭鍒楄〃鍙樺寲鍥炶皟 */
function handleProcessListChange(list: MesProWorkOrderProcessApi.WorkOrderProcess[]) {
@@ -181,7 +183,10 @@
if (isEditable.value) {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
- const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
+ const data = (await formApi.getValues()) as any;
+ if (Array.isArray(data.feedbackRole)) {
+ data.feedbackRole = data.feedbackRole.join(',');
+ }
await updateWorkOrder({ ...formData.value, ...data });
originalSnapshot.value = current;
}
@@ -243,7 +248,10 @@
const { valid } = await formApi.validate();
if (!valid) return;
modalApi.lock();
- const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
+ const data = (await formApi.getValues()) as any;
+ if (Array.isArray(data.feedbackRole)) {
+ data.feedbackRole = data.feedbackRole.join(',');
+ }
try {
if (formData.value?.id) {
// 缂栬緫妯″紡
@@ -307,7 +315,11 @@
modalApi.lock();
try {
formData.value = await getWorkOrder(data.id);
- await formApi.setValues(formData.value);
+ const formValues: any = { ...formData.value };
+ if (formValues.feedbackRole && typeof formValues.feedbackRole === 'string') {
+ formValues.feedbackRole = formValues.feedbackRole.split(',');
+ }
+ await formApi.setValues(formValues);
currentProductId.value = formData.value.productId;
} finally {
modalApi.unlock();
@@ -341,6 +353,9 @@
<Tabs.TabPane key="process"
tab="宸ュ簭"
forceRender>
+ <p class="mb-3 text-xs text-gray-400 leading-relaxed">
+ 鏂板妯″紡浼氳嚜鍔ㄤ粠鎵�閫変骇鍝佺殑宸ヨ壓璺嚎甯﹀嚭宸ュ簭锛岀偣鍑昏棣栨墜鏌勫彲鎷栨嫿璋冩暣椤哄簭锛涖�屽叧閿伐搴忋�嶉渶閲嶇偣绠℃帶銆併�岃川妫�宸ュ簭銆嶇敱璐ㄦ浜哄憳妫�楠屻�佸嬀閫夈�屽�掑啿銆嶇殑宸ュ簭鍦ㄦ姤宸ュ悗鎸夋姇鏂欑敤閲忚嚜鍔ㄦ墸鍑忕墿鏂欏簱瀛樸�傛瘡閬撳伐搴忓彲鐐广�屾柊澧炵墿鏂欍�嶉厤缃姇鏂欐槑缁嗐��
+ </p>
<ProcessList
ref="processListRef"
:form-type="formType"
@@ -352,6 +367,9 @@
</Tabs.TabPane>
<Tabs.TabPane key="item"
tab="鐗╂枡闇�姹�">
+ <p class="mb-3 text-xs text-gray-400 leading-relaxed">
+ 鐗╂枡闇�姹傛寜鍚勫伐搴忋�屾姇鏂欐槑缁嗐�嵜楄鍗曟暟閲忚嚜鍔ㄦ眹鎬荤敓鎴愶紝浠呬緵鏌ョ湅锛涘闇�璋冩暣锛岃鍥炲埌銆屽伐搴忋�嶉〉绛剧淮鎶ゆ姇鏂欍��
+ </p>
<ItemList v-if="formData?.id"
:work-order-id="formData.id" />
<div v-else class="text-gray-400 text-center py-8">
@@ -362,11 +380,15 @@
</template>
<template #prepend-footer>
<div class="flex flex-auto flex-col gap-2">
- <div v-if="canUseAi" class="flex items-center gap-2">
- <PredictMaterial :work-order-id="formData!.id" />
- <PredictDuration :work-order-id="formData!.id" />
- <PredictRisk :work-order-id="formData!.id" />
- <PredictDelivery :work-order-id="formData!.id" />
+ <div v-if="canUseAi" class="flex flex-wrap items-center gap-x-2 gap-y-1">
+ <span class="text-xs font-semibold text-gray-800 dark:text-gray-100">AI 鏅鸿兘鍒嗘瀽</span>
+ <div class="flex items-center gap-2">
+ <PredictMaterial :work-order-id="formData!.id" />
+ <PredictDuration :work-order-id="formData!.id" />
+ <PredictRisk :work-order-id="formData!.id" />
+ <PredictDelivery :work-order-id="formData!.id" />
+ </div>
+ <span class="text-xs text-gray-400">鍩轰簬鍘嗗彶鐢熶骇鏁版嵁浼扮畻锛屼粎渚涘弬鑰冿紝涓嶅弬涓庝笟鍔¤绠�</span>
</div>
<div v-if="canUseAi && showMainBtn" class="border-t border-gray-200" />
<div class="flex items-center">
@@ -384,13 +406,13 @@
@click="handleSave">
淇濆瓨淇敼
</Button>
- <Button v-if="formType === 'detail' && formData?.id"
+ <!-- <Button v-if="formType === 'detail' && formData?.id"
@click="handleBarcode">
鏌ョ湅鏉$爜
- </Button>
+ </Button> -->
<Button @click="modalApi.close()">鍙栨秷</Button>
</div>
</template>
</Modal>
- <BarcodeDetail ref="barcodeDetailRef" />
+ <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
</template>
diff --git a/src/views/mes/pro/workorder/modules/process-form.vue b/src/views/mes/pro/workorder/modules/process-form.vue
index bdbdcaa..bbe10c0 100644
--- a/src/views/mes/pro/workorder/modules/process-form.vue
+++ b/src/views/mes/pro/workorder/modules/process-form.vue
@@ -2,7 +2,7 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
-import { computed, ref } from 'vue';
+import { computed, h, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -91,6 +91,15 @@
fieldName: 'linkType',
label: '宸ュ簭鍏崇郴',
component: 'Select',
+ help: () =>
+ h(
+ 'div',
+ {
+ style:
+ 'white-space: pre-line; max-width: 320px; line-height: 1.6;',
+ },
+ '0=寮�濮�-寮�濮�(SS)锛氭湰宸ュ簭寮�濮嬪悗锛屼笅閬撳伐搴忔墠鑳藉紑濮嬶紙鍙噸鍙犲苟琛屽紑宸ワ級\n1=缁撴潫-缁撴潫(FF)锛氫笅閬撳伐搴忓繀椤荤瓑鍒版湰宸ュ簭缁撴潫鎵嶈兘缁撴潫锛堝苟琛屼絾鍚屾瀹屽伐锛塡n2=寮�濮�-缁撴潫(SF)锛氭湰宸ュ簭寮�濮嬪悗锛屼笅閬撳伐搴忔墠鑳界粨鏉焅n3=缁撴潫-寮�濮�(FS)锛氭湰宸ュ簭缁撴潫鍚庯紝涓嬮亾宸ュ簭鎵嶈兘寮�濮嬶紙榛樿鍊硷級',
+ ),
componentProps: {
allowClear: true,
options: [
diff --git a/src/views/mes/pro/workorder/modules/process-list.vue b/src/views/mes/pro/workorder/modules/process-list.vue
index 283424d..687633a 100644
--- a/src/views/mes/pro/workorder/modules/process-list.vue
+++ b/src/views/mes/pro/workorder/modules/process-list.vue
@@ -346,6 +346,9 @@
width="500px"
@ok="handleQuantityConfirm"
>
+ <p class="mb-3 text-xs text-gray-400 leading-relaxed">
+ 鐢ㄦ枡姣斾緥 = 鐢熶骇 1 浠朵骇鍝佹秷鑰楄鐗╂枡鐨勬暟閲忥紱銆屽�掑啿銆嶅伐搴忓皢鍦ㄦ姤宸ュ悗鎹鑷姩鎵e噺搴撳瓨銆�
+ </p>
<div class="space-y-3 py-2">
<div
v-for="(item, idx) in pendingItems"
diff --git a/src/views/mes/process-design/parameter/index.vue b/src/views/mes/process-design/parameter/index.vue
index d022a88..f8ff530 100644
--- a/src/views/mes/process-design/parameter/index.vue
+++ b/src/views/mes/process-design/parameter/index.vue
@@ -17,15 +17,28 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ProcessLinkDialog from './modules/process-link-dialog.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
+const [ProcessLinkModal, processLinkModalApi] = useVbenModal({
+ connectedComponent: ProcessLinkDialog,
+ destroyOnClose: true,
+});
+
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
gridApi.query();
+}
+
+/** 鏌ョ湅璇ュ弬鏁拌鍝簺宸ュ簭缁戝畾 */
+function handleViewProcessLink(row: MesPdParameterApi.Parameter) {
+ processLinkModalApi
+ .setData({ paramId: row.id, paramName: row.paramName })
+ .open();
}
/** 鍒涘缓宸ヨ壓鍙傛暟 */
@@ -98,6 +111,7 @@
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
+ <ProcessLinkModal />
<Grid table-title="宸ヨ壓鍙傛暟鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -126,6 +140,13 @@
<TableAction
:actions="[
{
+ label: '鍏宠仈宸ュ簭',
+ type: 'link',
+ icon: ACTION_ICON.LINK,
+ auth: ['mes:pd:parameter:update'],
+ onClick: handleViewProcessLink.bind(null, row),
+ },
+ {
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
diff --git a/src/views/mes/process-design/parameter/modules/process-link-dialog.vue b/src/views/mes/process-design/parameter/modules/process-link-dialog.vue
new file mode 100644
index 0000000..1a7b09e
--- /dev/null
+++ b/src/views/mes/process-design/parameter/modules/process-link-dialog.vue
@@ -0,0 +1,70 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Table } from 'ant-design-vue';
+
+import { getProcessListByParam } from '#/api/mes/pro/processdesign/processparam';
+
+const emit = defineEmits(['success']);
+
+const paramId = ref<number>();
+const paramName = ref<string>('');
+const loading = ref(false);
+const processList = ref<{ id: number; code?: string; name?: string; attention?: string }[]>([]);
+
+const getTitle = computed(() => `鍏宠仈宸ュ簭 - ${paramName.value || ''}`);
+
+async function loadData() {
+ if (!paramId.value) {
+ return;
+ }
+ loading.value = true;
+ try {
+ processList.value = await getProcessListByParam(paramId.value);
+ } finally {
+ loading.value = false;
+ }
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ await modalApi.close();
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ paramId.value = undefined;
+ processList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ paramId: number; paramName?: string }>();
+ paramId.value = data.paramId;
+ paramName.value = data.paramName || '';
+ await loadData();
+ },
+});
+
+const columns = [
+ { title: '宸ュ簭缂栫爜', dataIndex: 'code', key: 'code' },
+ { title: '宸ュ簭鍚嶇О', dataIndex: 'name', key: 'name' },
+ { title: '宸ヨ壓瑕佹眰', dataIndex: 'attention', key: 'attention' },
+];
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/5">
+ <Table
+ :columns="columns"
+ :data-source="processList"
+ :loading="loading"
+ :pagination="false"
+ row-key="id"
+ size="small"
+ >
+ <template #emptyText>
+ {{ loading ? '鍔犺浇涓��' : '璇ュ弬鏁版殏鏈浠讳綍宸ュ簭缁戝畾' }}
+ </template>
+ </Table>
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/process/index.vue b/src/views/mes/process-design/process/index.vue
index f1ee23f..f24e48f 100644
--- a/src/views/mes/process-design/process/index.vue
+++ b/src/views/mes/process-design/process/index.vue
@@ -17,15 +17,28 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ParamManage from './modules/param-manage.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
+const [ParamManageModal, paramManageModalApi] = useVbenModal({
+ connectedComponent: ParamManage,
+ destroyOnClose: true,
+});
+
/** 鍒锋柊琛ㄦ牸 */
function handleRefresh() {
gridApi.query();
+}
+
+/** 绠$悊宸ュ簭缁戝畾鐨勫伐鑹哄弬鏁� */
+function handleParamManage(row: MesProProcessApi.Process) {
+ paramManageModalApi
+ .setData({ processId: row.id, processName: row.name })
+ .open();
}
/** 鍒涘缓鐢熶骇宸ュ簭 */
@@ -97,6 +110,7 @@
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
+ <ParamManageModal @success="handleRefresh" />
<Grid table-title="鐢熶骇宸ュ簭鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -125,6 +139,13 @@
<TableAction
:actions="[
{
+ label: '宸ヨ壓鍙傛暟',
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-process:update'],
+ onClick: handleParamManage.bind(null, row),
+ },
+ {
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
diff --git a/src/views/mes/process-design/process/modules/param-manage.vue b/src/views/mes/process-design/process/modules/param-manage.vue
new file mode 100644
index 0000000..e0b5157
--- /dev/null
+++ b/src/views/mes/process-design/process/modules/param-manage.vue
@@ -0,0 +1,193 @@
+<script lang="ts" setup>
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+import type { MesProProcessParamApi } from '#/api/mes/pro/processdesign/processparam';
+
+import { computed, ref } from 'vue';
+
+import { DICT_TYPE } from '@vben/constants';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button, Input, InputNumber, message, Switch } from 'ant-design-vue';
+
+import {
+ getProcessParamListByProcess,
+ updateProcessParams,
+} from '#/api/mes/pro/processdesign/processparam';
+import { $t } from '#/locales';
+import { DictTag } from '#/components/dict-tag';
+
+import ParameterSelectDialog from '../../template/modules/parameter-select-dialog.vue';
+
+const emit = defineEmits(['success']);
+
+const processId = ref<number>();
+const processName = ref<string>('');
+const loading = ref(false);
+const boundList = ref<MesProProcessParamApi.ProcessParam[]>([]);
+const parameterSelectDialogRef =
+ ref<InstanceType<typeof ParameterSelectDialog>>();
+
+const getTitle = computed(() => `宸ヨ壓鍙傛暟缁戝畾 - ${processName.value || ''}`);
+
+/** 鍔犺浇宸ュ簭宸茬粦瀹氱殑鍙傛暟 */
+async function loadData() {
+ if (!processId.value) {
+ return;
+ }
+ loading.value = true;
+ try {
+ boundList.value = await getProcessParamListByProcess(processId.value);
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 鎵撳紑宸ヨ壓鍙傛暟閫夋嫨寮圭獥 */
+function handleSelectParameters() {
+ parameterSelectDialogRef.value?.open(
+ boundList.value
+ .map((item) => item.processParamId)
+ .filter((id): id is number => id !== undefined),
+ );
+}
+
+/** 鎺ユ敹閫変腑鐨勫弬鏁帮紝杩藉姞鍒扮粦瀹氬垪琛紙蹇呭~/鎺掑簭璧伴粯璁ゅ�硷級 */
+function onParametersSelected(rows: MesPdParameterApi.Parameter[]) {
+ const existingIds = new Set(
+ boundList.value
+ .map((item) => item.processParamId)
+ .filter((id): id is number => id !== undefined),
+ );
+ const newRows = rows
+ .filter((row) => row.id !== undefined && !existingIds.has(row.id!))
+ .map((row, index) => ({
+ processParamId: row.id,
+ paramCode: row.paramCode,
+ paramName: row.paramName,
+ paramType: row.paramType,
+ unitMeasureName: row.unitMeasureName,
+ required: row.required,
+ sort: boundList.value.length + index + 1,
+ remark: row.remark,
+ }));
+ boundList.value = [...boundList.value, ...newRows];
+}
+
+/** 绉婚櫎缁戝畾 */
+function removeItem(index: number) {
+ boundList.value.splice(index, 1);
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (processId.value === undefined) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ await updateProcessParams(
+ processId.value,
+ boundList.value.map((item, index) => ({
+ processParamId: item.processParamId!,
+ required: item.required,
+ sort: item.sort ?? index + 1,
+ remark: item.remark,
+ })),
+ );
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ emit('success');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ processId.value = undefined;
+ boundList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{ processId: number; processName?: string }>();
+ processId.value = data.processId;
+ processName.value = data.processName || '';
+ await loadData();
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-4/5">
+ <div class="mx-4">
+ <div class="mb-2 flex items-center justify-between">
+ <span class="text-sm font-medium">宸茬粦瀹氬伐鑹哄弬鏁�</span>
+ <Button type="primary" size="small" @click="handleSelectParameters">
+ 娣诲姞鍙傛暟
+ </Button>
+ </div>
+ <div v-if="boundList.length > 0" class="rounded border border-gray-200">
+ <div
+ class="grid grid-cols-12 gap-2 border-b border-gray-200 bg-gray-50 p-2 text-xs font-medium"
+ >
+ <div class="col-span-3">鍙傛暟鍚嶇О</div>
+ <div class="col-span-2">鍙傛暟缂栫爜</div>
+ <div class="col-span-2">鍙傛暟绫诲瀷</div>
+ <div class="col-span-1">鍗曚綅</div>
+ <div class="col-span-1 text-center">鏄惁蹇呭~</div>
+ <div class="col-span-1">鎺掑簭</div>
+ <div class="col-span-1">澶囨敞</div>
+ <div class="col-span-1 text-center">鎿嶄綔</div>
+ </div>
+ <div
+ v-for="(item, index) in boundList"
+ :key="item.processParamId || index"
+ class="grid grid-cols-12 items-center gap-2 border-b border-gray-100 p-2"
+ >
+ <div class="col-span-3 text-sm">{{ item.paramName }}</div>
+ <div class="col-span-2 text-sm text-gray-500">{{ item.paramCode }}</div>
+ <div class="col-span-2">
+ <DictTag
+ v-if="item.paramType !== undefined"
+ :type="DICT_TYPE.MES_PD_PARAM_TYPE"
+ :value="item.paramType"
+ />
+ </div>
+ <div class="col-span-1 text-sm">{{ item.unitMeasureName }}</div>
+ <div class="col-span-1 flex items-center justify-center">
+ <Switch v-model:checked="item.required" size="small" />
+ </div>
+ <div class="col-span-1">
+ <InputNumber
+ v-model:value="item.sort"
+ :min="1"
+ size="small"
+ class="w-full"
+ />
+ </div>
+ <div class="col-span-1">
+ <Input
+ v-model:value="item.remark"
+ placeholder="澶囨敞"
+ size="small"
+ />
+ </div>
+ <div class="col-span-1 flex items-center justify-center">
+ <Button type="text" danger size="small" @click="removeItem(index)">
+ 鍒犻櫎
+ </Button>
+ </div>
+ </div>
+ </div>
+ <div
+ v-else
+ class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400"
+ >
+ {{ loading ? '鍔犺浇涓��' : '鏆傛湭缁戝畾鍙傛暟' }}
+ </div>
+ </div>
+ <ParameterSelectDialog
+ ref="parameterSelectDialogRef"
+ @selected="onParametersSelected"
+ />
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/data.ts b/src/views/mes/process-design/route/data.ts
index 941a2be..3f99c03 100644
--- a/src/views/mes/process-design/route/data.ts
+++ b/src/views/mes/process-design/route/data.ts
@@ -205,12 +205,21 @@
fieldName: "linkType",
label: "涓庝笅閬撳伐搴忓叧绯�",
component: "Select",
+ help: () =>
+ h(
+ "div",
+ {
+ style:
+ "white-space: pre-line; max-width: 320px; line-height: 1.6;",
+ },
+ "0=寮�濮�-寮�濮�(SS)锛氭湰宸ュ簭寮�濮嬪悗锛屼笅閬撳伐搴忔墠鑳藉紑濮嬶紙鍙噸鍙犲苟琛屽紑宸ワ級\n1=缁撴潫-缁撴潫(FF)锛氫笅閬撳伐搴忓繀椤荤瓑鍒版湰宸ュ簭缁撴潫鎵嶈兘缁撴潫锛堝苟琛屼絾鍚屾瀹屽伐锛塡n2=寮�濮�-缁撴潫(SF)锛氭湰宸ュ簭寮�濮嬪悗锛屼笅閬撳伐搴忔墠鑳界粨鏉焅n3=缁撴潫-寮�濮�(FS)锛氭湰宸ュ簭缁撴潫鍚庯紝涓嬮亾宸ュ簭鎵嶈兘寮�濮嬶紙榛樿鍊硷級",
+ ),
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, "number"),
placeholder: "璇烽�夋嫨",
},
- rules: z.number().default(3),
+ rules: z.number().optional(),
},
{
fieldName: "colorCode",
@@ -401,8 +410,11 @@
];
}
-/** 宸ヨ壓璺嚎浜у搧琛ㄥ崟 */
-export function useRouteProductFormSchema(onItemChange?: (item: any) => void): VbenFormSchema[] {
+/** 宸ヨ壓璺嚎浜у搧琛ㄥ崟锛沵ultiple = true锛堟柊澧炴ā寮忥級鏃朵骇鍝佸彲澶氶�夛紝涓�娆℃�х粦瀹氬涓� */
+export function useRouteProductFormSchema(
+ onItemChange?: (item: any) => void,
+ multiple = false,
+): VbenFormSchema[] {
return [
{
fieldName: "id",
@@ -414,17 +426,29 @@
component: "Input",
dependencies: { triggerFields: [""], show: () => false },
},
- // 浜у搧鐗╂枡锛氫娇鐢ㄤ笟鍔¤嚜瀹氫箟閫夋嫨鍣紝change 鍥炲~缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅
- {
- fieldName: "itemId",
- label: "浜у搧",
- component: markRaw(MdItemSelect),
- componentProps: {
- onChange: onItemChange,
- },
- formItemClass: "col-span-2",
- rules: "selectRequired",
- },
+ // 鏂板锛堝閫夛級锛氫竴娆$粦瀹氬涓骇鍝侊紱缂栬緫锛堝崟閫夛級锛歝hange 鍥炲~缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅
+ multiple
+ ? {
+ fieldName: "itemIds",
+ label: "浜у搧锛堝彲澶氶�夛級",
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ multiple: true,
+ placeholder: "璇烽�夋嫨瑕佺粦瀹氱殑浜у搧锛堝彲澶氶�夛級",
+ },
+ formItemClass: "col-span-2",
+ rules: "selectRequired",
+ }
+ : {
+ fieldName: "itemId",
+ label: "浜у搧",
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ onChange: onItemChange,
+ },
+ formItemClass: "col-span-2",
+ rules: "selectRequired",
+ },
{
fieldName: "quantity",
label: "鐢熶骇鏁伴噺",
diff --git a/src/views/mes/process-design/route/modules/form.vue b/src/views/mes/process-design/route/modules/form.vue
index 8c0c230..cf73e0e 100644
--- a/src/views/mes/process-design/route/modules/form.vue
+++ b/src/views/mes/process-design/route/modules/form.vue
@@ -2,24 +2,28 @@
import type { FormType } from '../data';
import type { MesProRouteApi } from '#/api/mes/pro/route';
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import { computed, ref } from 'vue';
-import { useVbenModal } from '@vben/common-ui';
+import { confirm, useVbenModal } from '@vben/common-ui';
-import { message, TabPane, Tabs } from 'ant-design-vue';
+import { Button, Divider, message, Select } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route';
+import {
+ deleteRouteProduct,
+ getRouteProductListByRoute,
+} from '#/api/mes/pro/route/product';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
-import ProcessList from './process-list.vue';
-import ProductList from './product-list.vue';
+import ProductForm from './product-form.vue';
+import Workbench from './workbench.vue';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
-const subTab = ref('process'); // 褰撳墠婵�娲荤殑瀛愯〃 Tab
const formData = ref<MesProRouteApi.Route>();
const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
@@ -32,6 +36,72 @@
: $t('ui.actionTitle.create', ['宸ヨ壓璺嚎']);
});
+// ==================== 浜у搧缁戝畾 ====================
+const products = ref<MesProRouteProductApi.RouteProduct[]>([]); // 鍏宠仈浜у搧鍒楄〃
+const selectedProductItemId = ref<number>(); // 褰撳墠閫変腑鐨勪骇鍝佺墿鏂欑紪鍙�
+const selectedProduct = computed(() =>
+ products.value.find((item) => item.itemId === selectedProductItemId.value),
+);
+const productOptions = computed(() =>
+ products.value.map((item) => ({ label: item.itemName, value: item.itemId })),
+);
+
+async function loadProducts() {
+ const routeId = formData.value?.id;
+ if (!routeId) {
+ products.value = [];
+ return;
+ }
+ products.value = (await getRouteProductListByRoute(routeId)) || [];
+ if (
+ !products.value.some((item) => item.itemId === selectedProductItemId.value)
+ ) {
+ selectedProductItemId.value = products.value[0]?.itemId;
+ }
+}
+
+const [ProductFormModal, productFormModalApi] = useVbenModal({
+ connectedComponent: ProductForm,
+ destroyOnClose: true,
+});
+
+/** 鎵撳紑浜у搧寮圭獥锛堢粦瀹� / 缂栬緫锛� */
+function openProductForm(row?: MesProRouteProductApi.RouteProduct) {
+ if (!formData.value?.id) {
+ return;
+ }
+ productFormModalApi
+ .setData(
+ row
+ ? { id: row.id, routeId: formData.value.id }
+ : {
+ routeId: formData.value.id,
+ boundItemIds: products.value
+ .map((item) => item.itemId)
+ .filter((id): id is number => id != null),
+ },
+ )
+ .open();
+}
+
+/** 绉婚櫎浜у搧锛堜細绾ц仈鍒犻櫎璇ヤ骇鍝佺殑鎶曞叆鐗╂枡閰嶇疆锛� */
+async function handleRemoveProduct() {
+ if (!selectedProduct.value?.id) {
+ return;
+ }
+ try {
+ await confirm(
+ `纭灏嗕骇鍝併��${selectedProduct.value.itemName}銆嶄粠璇ュ伐鑹鸿矾绾跨Щ闄わ紵鍏跺叏閮ㄦ姇鍏ョ墿鏂欓厤缃皢涓�骞跺垹闄ゃ�俙,
+ );
+ } catch {
+ return;
+ }
+ await deleteRouteProduct(selectedProduct.value.id);
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await loadProducts();
+}
+
+// ==================== 鍩烘湰淇℃伅琛ㄥ崟 ====================
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: { class: 'w-full' },
@@ -59,11 +129,12 @@
const data = (await formApi.getValues()) as MesProRouteApi.Route;
try {
if (formType.value === 'create') {
- // 鏂板鎴愬姛鍚庡垏鍒扮紪杈戞ā寮忥紝鏂逛究缁х画缁存姢缁勬垚宸ュ簭銆佸叧鑱斾骇鍝�
+ // 鏂板鎴愬姛鍚庡垏鍒扮紪杈戞ā寮忥紝杩涘叆銆屼骇鍝佷笌宸ュ簭閰嶇疆銆嶅伐浣滃彴
const id = await createRoute(data);
formData.value = { ...data, id };
await formApi.setFieldValue('id', id);
formType.value = 'update';
+ await loadProducts();
} else {
await updateRoute(data);
formData.value = { ...formData.value, ...data };
@@ -82,10 +153,11 @@
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
+ products.value = [];
+ selectedProductItemId.value = undefined;
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
- subTab.value = 'process';
// 鍔犺浇鏁版嵁
const data = modalApi.getData<{ formType: FormType; id?: number }>();
formType.value = data.formType;
@@ -99,6 +171,7 @@
formData.value = await getRoute(data.id);
// 璁剧疆鍒� values
await formApi.setValues(formData.value);
+ await loadProducts();
} finally {
modalApi.unlock();
}
@@ -107,20 +180,49 @@
</script>
<template>
- <Modal :title="getTitle" class="w-4/5">
+ <Modal :title="getTitle" class="w-11/12">
<Form class="mx-4" />
- <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀哄瓙琛� Tab锛屾柊澧炴ā寮忎笅闅愯棌 -->
- <Tabs
- v-if="formType !== 'create' && formData?.id"
- v-model:active-key="subTab"
- class="mx-4 mt-4"
- >
- <TabPane key="process" tab="缁勬垚宸ュ簭">
- <ProcessList :form-type="formType" :route-id="formData.id" />
- </TabPane>
- <TabPane key="product" tab="鍏宠仈浜у搧">
- <ProductList :form-type="formType" :route-id="formData.id" />
- </TabPane>
- </Tabs>
+ <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀恒�屼骇鍝佷笌宸ュ簭閰嶇疆銆嶅伐浣滃彴锛屾柊澧炴ā寮忎笅闅愯棌 -->
+ <template v-if="formData?.id">
+ <div class="mx-4 mt-4">
+ <Divider class="!my-2" orientation="left">
+ <span class="text-sm">浜у搧涓庡伐搴忛厤缃�</span>
+ </Divider>
+ <div class="flex flex-wrap items-center gap-3">
+ <span class="shrink-0 text-sm font-medium">鐢熶骇浜у搧</span>
+ <Select
+ v-model:value="selectedProductItemId"
+ class="!w-64"
+ allow-clear
+ :disabled="isDetail"
+ placeholder="璇烽�夋嫨鎴栫粦瀹氫骇鍝�"
+ :options="productOptions"
+ />
+ <template v-if="!isDetail">
+ <Button type="primary" @click="openProductForm()"> 缁戝畾浜у搧 </Button>
+ <Button
+ :disabled="!selectedProduct"
+ @click="openProductForm(selectedProduct)"
+ >
+ 缂栬緫浜у搧
+ </Button>
+ <Button danger :disabled="!selectedProduct" @click="handleRemoveProduct">
+ 绉婚櫎浜у搧
+ </Button>
+ </template>
+ </div>
+ <p v-if="products.length === 0" class="mt-2 text-xs text-gray-400">
+ 鍏堢粦瀹氳鐢熶骇鐨勭墿鏂欙紝鍐嶄负姣忛亾宸ュ簭閰嶇疆銆屾姇鍏� + 浜у嚭銆�
+ </p>
+ <div class="mt-3">
+ <Workbench
+ :form-type="formType"
+ :product="selectedProduct"
+ :route-id="formData.id"
+ />
+ </div>
+ </div>
+ </template>
</Modal>
-</template>
+ <ProductFormModal @success="loadProducts" />
+</template>
\ No newline at end of file
diff --git a/src/views/mes/process-design/route/modules/process-form.vue b/src/views/mes/process-design/route/modules/process-form.vue
index b742c63..6dcab06 100644
--- a/src/views/mes/process-design/route/modules/process-form.vue
+++ b/src/views/mes/process-design/route/modules/process-form.vue
@@ -31,7 +31,7 @@
commonConfig: {
componentProps: { class: 'w-full' },
formItemClass: 'col-span-1',
- labelWidth: 130,
+ labelWidth: 150,
},
layout: 'horizontal',
schema: [],
diff --git a/src/views/mes/process-design/route/modules/product-form.vue b/src/views/mes/process-design/route/modules/product-form.vue
index a3b0e5d..e20baa6 100644
--- a/src/views/mes/process-design/route/modules/product-form.vue
+++ b/src/views/mes/process-design/route/modules/product-form.vue
@@ -5,7 +5,7 @@
import { useVbenModal } from '@vben/common-ui';
-import { Divider, message } from 'ant-design-vue';
+import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
@@ -16,17 +16,17 @@
import { $t } from '#/locales';
import { useRouteProductFormSchema } from '../data';
-import ProductBomList from './product-bom-list.vue';
const emit = defineEmits(['success']);
const formData = ref<MesProRouteProductApi.RouteProduct>();
+const boundItemIds = ref<number[]>([]); // 宸茬粦瀹氭湰璺嚎鐨勪骇鍝佺墿鏂欑紪鍙凤紙鏂板澶氶�夋椂璺宠繃锛�
const getTitle = computed(() =>
formData.value?.id
? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎浜у搧'])
: $t('ui.actionTitle.create', ['宸ヨ壓璺嚎浜у搧']),
);
-/** MdItemSelect change 鍥炶皟锛氭妸鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅鍥炲~鍒� form */
+/** MdItemSelect 鍗曢�� change 鍥炶皟锛氭妸鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅鍥炲~鍒� form */
async function handleItemChange(item?: any) {
if (!item) {
return;
@@ -58,22 +58,62 @@
return;
}
modalApi.lock();
- // 鎻愪氦琛ㄥ崟
- const data =
- (await formApi.getValues()) as MesProRouteProductApi.RouteProduct;
+ const data = (await formApi.getValues()) as MesProRouteProductApi.RouteProduct & {
+ itemIds?: number[];
+ };
try {
+ // 缂栬緫妯″紡锛氭洿鏂板崟涓骇鍝�
if (formData.value?.id) {
await updateRouteProduct(data);
- // 鐢ㄦ渶鏂拌〃鍗曞�煎悓姝� formData锛岀‘淇濅骇鍝� BOM 瀛愯〃缁戝畾鐨� itemId 涓庤〃鍗曚竴鑷�
- formData.value = { ...formData.value, ...data };
- } else {
- // 鏂板鎴愬姛鍚庡垏鎹㈠埌缂栬緫妯″紡锛屾柟渚跨户缁淮鎶や骇鍝� BOM
- const id = await createRouteProduct(data);
- formData.value = { ...data, id };
- await formApi.setFieldValue('id', id);
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ return;
}
- emit('success');
- message.success($t('ui.actionMessage.operationSuccess'));
+ // 鏂板妯″紡锛氬閫変骇鍝佷竴娆℃�х粦瀹氾紝璺宠繃宸插湪璺嚎涓殑浜у搧
+ const itemIds = Array.isArray(data.itemIds) ? data.itemIds : [];
+ if (itemIds.length === 0) {
+ message.warning('璇疯嚦灏戦�夋嫨涓�涓骇鍝�');
+ return;
+ }
+ let created = 0;
+ let skipped = 0;
+ const failedIds: number[] = [];
+ for (const itemId of itemIds) {
+ if (itemId == null) {
+ continue;
+ }
+ if (boundItemIds.value.includes(itemId)) {
+ skipped++;
+ continue;
+ }
+ try {
+ await createRouteProduct({
+ routeId: data.routeId,
+ itemId,
+ quantity: data.quantity,
+ productionTime: data.productionTime,
+ timeUnitType: data.timeUnitType,
+ remark: data.remark,
+ });
+ created++;
+ } catch {
+ failedIds.push(itemId);
+ }
+ }
+ if (skipped > 0) {
+ message.warning(`宸茶烦杩� ${skipped} 涓凡缁戝畾鐨勪骇鍝乣);
+ }
+ if (failedIds.length > 0) {
+ message.error(
+ `鏈� ${failedIds.length} 涓骇鍝佺粦瀹氬け璐ワ紙鍙兘宸茶鍏跺畠宸ヨ壓璺嚎鍗犵敤锛塦,
+ );
+ }
+ if (created > 0) {
+ message.success(`鎴愬姛缁戝畾 ${created} 涓骇鍝乣);
+ await modalApi.close();
+ emit('success');
+ }
} finally {
modalApi.unlock();
}
@@ -81,24 +121,42 @@
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
+ boundItemIds.value = [];
return;
}
// 鍔犺浇鏁版嵁
const data = modalApi.getData<{
id?: number;
routeId: number;
+ boundItemIds?: number[];
}>();
if (!data) {
return;
}
+ boundItemIds.value = data.boundItemIds || [];
if (!data.id) {
- await formApi.setValues({ routeId: data.routeId });
+ // 鏂板妯″紡锛氬閫変骇鍝佺粦瀹�
+ formData.value = undefined;
+ await formApi.setState({
+ schema: useRouteProductFormSchema(handleItemChange, true),
+ });
+ await formApi.setValues({
+ routeId: data.routeId,
+ itemIds: [],
+ quantity: 1,
+ productionTime: 1,
+ timeUnitType: 'MINUTE',
+ remark: undefined,
+ });
return;
}
+ // 缂栬緫妯″紡锛氬姞杞藉崟涓骇鍝佸洖濉�
modalApi.lock();
try {
formData.value = await getRouteProduct(data.id);
- // 璁剧疆鍒� values
+ await formApi.setState({
+ schema: useRouteProductFormSchema(handleItemChange, false),
+ });
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
@@ -108,18 +166,7 @@
</script>
<template>
- <Modal :title="getTitle" class="w-3/5">
+ <Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
- <!-- 缂栬緫妯″紡涓嬪睍绀轰骇鍝� BOM 瀛愯〃锛屾柊澧炴ā寮忎笅闅愯棌 -->
- <template v-if="formData?.id && formData?.itemId">
- <Divider class="!my-3" orientation="left">浜у搧 BOM 閰嶇疆</Divider>
- <div class="mx-4">
- <ProductBomList
- :route-id="formData.routeId!"
- :product-id="formData.itemId"
- :product-name="formData.itemName"
- />
- </div>
- </template>
</Modal>
-</template>
+</template>
\ No newline at end of file
diff --git a/src/views/mes/process-design/route/modules/workbench.vue b/src/views/mes/process-design/route/modules/workbench.vue
new file mode 100644
index 0000000..7959f3b
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/workbench.vue
@@ -0,0 +1,545 @@
+<script lang="ts" setup>
+import type { MesMdItemApi } from '#/api/mes/md/item';
+import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
+import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
+
+import { computed, ref, watch } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { DICT_TYPE } from '@vben/constants';
+import { getDictLabel } from '@vben/hooks';
+
+import {
+ Button,
+ Card,
+ Empty,
+ Input,
+ InputNumber,
+ message,
+ Popconfirm,
+ Table,
+} from 'ant-design-vue';
+
+import {
+ deleteRouteProcess,
+ getRouteProcessListByRoute,
+ updateRouteProcess,
+} from '#/api/mes/pro/route/process';
+import {
+ createRouteProductBom,
+ deleteRouteProductBom,
+ getRouteProductBomList,
+ updateRouteProductBom,
+} from '#/api/mes/pro/route/productbom';
+import { $t } from '#/locales';
+import { MdItemSelect, MdProductBomSelect } from '#/views/mes/md/item/components';
+
+import type { FormType } from '../data';
+import ProcessForm from './process-form.vue';
+
+defineOptions({ name: 'MesRouteWorkbench' });
+
+const props = withDefaults(
+ defineProps<{
+ routeId: number;
+ formType: FormType;
+ product?: MesProRouteProductApi.RouteProduct;
+ }>(),
+ { product: undefined },
+);
+
+const editable = computed(() => props.formType !== 'detail');
+
+const processes = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ュ簭鍒楄〃
+const selectedProcessId = ref<number>(); // 褰撳墠閫変腑宸ュ簭
+const outputItemIds = ref<number[]>([]); // 閫変腑宸ュ簭鐨勪骇鍑哄崐鎴愬搧
+const bomList = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); // 閫変腑宸ュ簭鍦ㄥ綋鍓嶄骇鍝佷笅鐨勬姇鍏ョ墿鏂�
+const adding = ref(false); // 鏄惁鏄剧ず鏂板鎶曞叆鐗╂枡琛�
+
+const selectedProcess = computed(() =>
+ processes.value.find((item) => item.processId === selectedProcessId.value),
+);
+const productItemId = computed(() => props.product?.itemId);
+const linkTypeLabel = computed(() =>
+ selectedProcess.value?.linkType != null
+ ? getDictLabel(DICT_TYPE.MES_PRO_LINK_TYPE, selectedProcess.value.linkType)
+ : '-',
+);
+
+const [ProcessFormModal, processFormModalApi] = useVbenModal({
+ connectedComponent: ProcessForm,
+ destroyOnClose: true,
+});
+
+/** 鍔犺浇宸ュ簭鍒楄〃锛堜繚鎸侀�変腑锛屽け鏁堟椂鍥為��绗竴涓級 */
+async function loadProcesses() {
+ const list = (await getRouteProcessListByRoute(props.routeId)) || [];
+ processes.value = list;
+ if (list.length === 0) {
+ selectedProcessId.value = undefined;
+ return;
+ }
+ if (!list.some((item) => item.processId === selectedProcessId.value)) {
+ selectedProcessId.value = list[0]!.processId;
+ }
+}
+
+/** 鏍规嵁閫変腑宸ュ簭鍚屾寰呯紪杈戠殑浜у嚭鍗婃垚鍝� */
+function syncOutputItemIds() {
+ outputItemIds.value = selectedProcess.value?.outputItemIds ?? [];
+}
+
+/** 鍔犺浇閫変腑宸ュ簭鍦ㄥ綋鍓嶄骇鍝佷笅鐨勬姇鍏ョ墿鏂� */
+async function loadBomList() {
+ const processId = selectedProcess.value?.processId;
+ if (!processId || !productItemId.value) {
+ bomList.value = [];
+ return;
+ }
+ bomList.value =
+ (await getRouteProductBomList({
+ routeId: props.routeId,
+ processId,
+ productId: productItemId.value,
+ })) || [];
+ // 娓呯悊宸蹭笉鍦ㄥ綋鍓嶆姇鍏ユ竻鍗曚腑鐨勮鍐呭娉ㄨ崏绋匡紝閬垮厤鍒囨崲鍚庢畫鐣欐棫杈撳叆
+ const draftIds = new Set(
+ bomList.value
+ .map((item) => item.id)
+ .filter((id): id is number => id != null),
+ );
+ Object.keys(remarkEditValues.value).forEach((key) => {
+ if (!draftIds.has(Number(key))) {
+ delete remarkEditValues.value[Number(key)];
+ }
+ });
+}
+
+watch(
+ () => props.routeId,
+ () => loadProcesses(),
+ { immediate: true },
+);
+
+watch(
+ () => selectedProcessId.value,
+ () => {
+ syncOutputItemIds();
+ loadBomList();
+ },
+);
+
+watch(
+ () => productItemId.value,
+ () => {
+ outputItemIds.value = [];
+ loadBomList();
+ },
+);
+
+/** 鏂板宸ュ簭 */
+function openProcessCreate() {
+ const maxSort = processes.value.length
+ ? Math.max(...processes.value.map((item) => item.sort || 0))
+ : 0;
+ processFormModalApi
+ .setData({ maxSort, routeId: props.routeId, formType: props.formType })
+ .open();
+}
+
+/** 缂栬緫宸ュ簭淇℃伅 */
+function openProcessEdit(row: MesProRouteProcessApi.RouteProcess) {
+ processFormModalApi
+ .setData({ id: row.id, routeId: props.routeId, formType: props.formType })
+ .open();
+}
+
+/** 鍒犻櫎宸ュ簭 */
+async function handleProcessDelete(row: MesProRouteProcessApi.RouteProcess) {
+ await deleteRouteProcess(row.id!);
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await loadProcesses();
+}
+
+/** 浜у嚭鍗婃垚鍝侊細閫夋嫨鍙樺寲鍗充繚瀛� */
+async function handleOutputChange(
+ items: MesMdItemApi.Item | MesMdItemApi.Item[] | undefined,
+) {
+ if (!selectedProcess.value) {
+ return;
+ }
+ const ids = (
+ Array.isArray(items) ? items : items ? [items] : []
+ )
+ .map((item) => item.id)
+ .filter((id): id is number => id != null);
+ outputItemIds.value = ids;
+ try {
+ await updateRouteProcess({
+ id: selectedProcess.value.id,
+ routeId: selectedProcess.value.routeId,
+ outputItemIds: ids,
+ });
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } catch {
+ // 淇濆瓨澶辫触锛氶噸杞藉伐搴忓垪琛ㄥ洖閫�鏈湴鍊�
+ await loadProcesses();
+ syncOutputItemIds();
+ }
+}
+
+/** 鐢ㄩ噺淇敼锛氬彉鍖栧嵆淇濆瓨锛堟惡甯﹀畬鏁村叧鑱斿瓧娈碉紝閫氳繃鍚庣蹇呭~鏍¢獙锛� */
+async function handleQuantityChange(
+ record: MesProRouteProductBomApi.RouteProductBom,
+ value: number | string | null,
+) {
+ if (value == null || record.id == null) {
+ return;
+ }
+ const quantity = Number(value);
+ if (Number.isNaN(quantity)) {
+ return;
+ }
+ record.quantity = quantity;
+ try {
+ await updateRouteProductBom(toUpdatePayload(record));
+ } catch {
+ await loadBomList();
+ }
+}
+
+const remarkEditValues = ref<Record<number, string>>({}); // 琛屽唴澶囨敞缂栬緫鐨勬殏瀛樺��
+
+/** 澶囨敞杈撳叆锛氭殏瀛樺�硷紝涓嶇珛鍗虫彁浜� */
+function handleRemarkInput(
+ record: MesProRouteProductBomApi.RouteProductBom,
+ value: string,
+) {
+ if (record.id != null) {
+ remarkEditValues.value[record.id] = value;
+ }
+}
+
+/** 璇诲彇琛屽唴澶囨敞鑽夌锛屾棤缂栬緫鏃跺洖閫�鏁版嵁搴撳�� */
+function getRemarkDraft(record: MesProRouteProductBomApi.RouteProductBom) {
+ const draft =
+ record.id != null ? remarkEditValues.value[record.id] : undefined;
+ return draft ?? record.remark ?? '';
+}
+
+/** 澶囨敞淇敼锛氬け鐒�/鍥炶溅鍗充繚瀛� */
+async function handleRemarkChange(
+ record: MesProRouteProductBomApi.RouteProductBom,
+ value: string,
+) {
+ if (record.id == null) {
+ return;
+ }
+ const remark = typeof value === 'string' ? value.trim() : '';
+ // 鍚屾鏆傚瓨鍊硷紝淇濊瘉鍥炶溅鍚庡啀瑙﹀彂鐨� blur 璇诲埌鐨勬槸鎻愪氦鍚庣殑鍊硷紝涓嶄細鐢ㄦ棫鍊煎洖鍐�
+ remarkEditValues.value[record.id] = remark;
+ if (record.remark === remark) {
+ return;
+ }
+ record.remark = remark;
+ try {
+ await updateRouteProductBom(toUpdatePayload(record));
+ } catch {
+ await loadBomList();
+ }
+}
+
+/** 琛屽唴缂栬緫鎻愪氦锛氫粎鎼哄甫鍚庣鏍¢獙鎵�闇�鐨勫叧鑱斿瓧娈� */
+function toUpdatePayload(record: MesProRouteProductBomApi.RouteProductBom) {
+ return {
+ id: record.id,
+ routeId: record.routeId,
+ processId: record.processId,
+ productId: record.productId,
+ itemId: record.itemId,
+ quantity: record.quantity,
+ remark: record.remark,
+ };
+}
+
+/** 閫変腑 BOM 鍚庢壒閲忓叆搴擄紙鏀寔澶氶�夛紝宸插瓨鍦ㄧ殑鐗╂枡鑷姩璺宠繃锛� */
+async function handleBomSelected(
+ boms?: MesMdProductBomApi.ProductBom | MesMdProductBomApi.ProductBom[],
+) {
+ if (!selectedProcess.value || !productItemId.value) {
+ return;
+ }
+ const selectedList = Array.isArray(boms) ? boms : boms ? [boms] : [];
+ if (selectedList.length === 0) {
+ return;
+ }
+ // 璺宠繃宸插湪鎶曞叆娓呭崟涓殑鐗╂枡锛岄伩鍏嶉噸澶嶅叆搴�
+ const existingIds = new Set(
+ bomList.value
+ .map((item) => item.itemId)
+ .filter((id): id is number => id != null),
+ );
+ let created = 0;
+ for (const bom of selectedList) {
+ if (!bom.bomItemId || existingIds.has(bom.bomItemId)) {
+ continue;
+ }
+ await createRouteProductBom({
+ routeId: props.routeId,
+ processId: selectedProcess.value.processId,
+ productId: productItemId.value,
+ itemId: bom.bomItemId,
+ quantity: bom.quantity ?? 1,
+ });
+ created++;
+ }
+ if (created > 0) {
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+ adding.value = false;
+ await loadBomList();
+}
+
+/** 鍒犻櫎鎶曞叆鐗╂枡 */
+async function handleBomDelete(record: MesProRouteProductBomApi.RouteProductBom) {
+ await deleteRouteProductBom(record.id!);
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await loadBomList();
+}
+
+/** 鎶曞叆鐗╂枡琛ㄦ牸鍒� */
+const columns = [
+ { title: '鐗╂枡', key: 'item' as const, width: 220 },
+ { title: '鐢ㄩ噺', key: 'quantity' as const, width: 110 },
+ { title: '鍗曚綅', key: 'unit' as const, dataIndex: 'unitName', width: 70 },
+ { title: '澶囨敞', key: 'remark' as const, dataIndex: 'remark', minWidth: 160 },
+ { title: '鎿嶄綔', key: 'action' as const, width: 70 },
+];
+</script>
+
+<template>
+ <ProcessFormModal @success="loadProcesses" />
+ <div class="flex gap-4">
+ <!-- 宸︿晶锛氬伐搴忓垪琛� -->
+ <div
+ class="w-72 shrink-0 overflow-hidden rounded-lg border border-gray-200 bg-white"
+ >
+ <div
+ class="flex h-10 items-center justify-between border-b border-gray-100 bg-gray-50 px-3"
+ >
+ <span class="text-sm font-medium">宸ュ簭鍒楄〃</span>
+ <Button
+ v-if="editable"
+ size="small"
+ type="link"
+ @click="openProcessCreate"
+ >
+ 鏂板宸ュ簭
+ </Button>
+ </div>
+ <ul class="max-h-[560px] overflow-auto">
+ <li
+ v-for="item in processes"
+ :key="item.processId"
+ class="group flex cursor-pointer items-center gap-2 px-3 py-2.5 text-sm transition-colors"
+ :class="
+ selectedProcess?.processId === item.processId
+ ? 'bg-blue-50 text-blue-600'
+ : 'hover:bg-gray-50'
+ "
+ @click="selectedProcessId = item.processId"
+ >
+ <span class="w-6 shrink-0 text-right text-xs text-gray-400">
+ {{ item.sort }}
+ </span>
+ <span
+ class="h-3 w-3 shrink-0 rounded-full border border-gray-200"
+ :style="{ backgroundColor: item.colorCode }"
+ ></span>
+ <span class="min-w-0 flex-1 truncate">{{ item.processName }}</span>
+ <span v-if="item.keyFlag" class="shrink-0 text-amber-500" title="鍏抽敭宸ュ簭">鈽�</span>
+ <span v-if="item.checkFlag" class="shrink-0 text-green-600" title="璐ㄦ纭">鈼�</span>
+ <span v-if="item.backflushFlag" class="shrink-0 text-cyan-600" title="鍊掑啿">鈫�</span>
+ <Popconfirm
+ v-if="editable"
+ title="纭鍒犻櫎璇ュ伐搴忥紵"
+ ok-text="纭畾"
+ cancel-text="鍙栨秷"
+ @confirm="handleProcessDelete(item)"
+ @click.stop
+ >
+ <Button
+ class="opacity-0 transition-opacity group-hover:opacity-100"
+ size="small"
+ type="text"
+ danger
+ >
+ 鍒�
+ </Button>
+ </Popconfirm>
+ </li>
+ <li v-if="processes.length === 0" class="px-3 py-10 text-center text-xs text-gray-400">
+ 灏氭湭閰嶇疆宸ュ簭锛岀偣鍑诲彸涓婅銆屾柊澧炲伐搴忋�嶅紑濮�
+ </li>
+ </ul>
+ </div>
+
+ <!-- 鍙充晶锛氶�変腑宸ュ簭鐨勯厤缃潰鏉� -->
+ <div class="min-w-0 flex-1 space-y-3">
+ <template v-if="selectedProcess">
+ <!-- 宸ュ簭淇℃伅 -->
+ <Card :bordered="false" class="!border border-gray-200">
+ <div class="flex items-start justify-between gap-3">
+ <div class="min-w-0">
+ <div class="flex items-center gap-2">
+ <span class="text-base font-medium">
+ {{ selectedProcess.processName }}
+ </span>
+ <span class="text-xs text-gray-400">
+ {{ selectedProcess.processCode }}
+ </span>
+ <span v-if="selectedProcess.keyFlag" class="text-amber-500" title="鍏抽敭宸ュ簭">鈽�</span>
+ <span v-if="selectedProcess.checkFlag" class="text-green-600" title="璐ㄦ纭">鈼�</span>
+ <span v-if="selectedProcess.backflushFlag" class="text-cyan-600" title="鍊掑啿">鈫�</span>
+ </div>
+ <div class="mt-1 flex flex-wrap gap-x-5 gap-y-1 text-sm text-gray-500">
+ <span>搴忓彿锛歿{ selectedProcess.sort }}</span>
+ <span>鍑嗗鏃堕棿锛歿{ selectedProcess.prepareTime ?? 0 }} 鍒�</span>
+ <span>绛夊緟鏃堕棿锛歿{ selectedProcess.waitTime ?? 0 }} 鍒�</span>
+ <span v-if="selectedProcess.linkType != null">
+ 涓庝笅閬撳伐搴忥細{{ linkTypeLabel }}
+ </span>
+ </div>
+ <div v-if="selectedProcess.remark" class="mt-1 text-xs text-gray-400">
+ 澶囨敞锛歿{ selectedProcess.remark }}
+ </div>
+ </div>
+ <Button
+ v-if="editable"
+ type="link"
+ @click="openProcessEdit(selectedProcess)"
+ >
+ 缂栬緫淇℃伅
+ </Button>
+ </div>
+ </Card>
+
+ <!-- 浜у嚭 -->
+ <Card :bordered="false" class="!border border-gray-200">
+ <template #title>
+ <span class="text-sm font-medium">浜у嚭</span>
+ </template>
+ <MdItemSelect
+ :model-value="outputItemIds"
+ multiple
+ :disabled="!editable"
+ placeholder="鐣欑┖鍒欓粯璁や负宸ュ崟鎴愬搧"
+ @change="handleOutputChange"
+ />
+ <p class="mt-1 text-xs text-gray-400">
+ 鐣欑┖琛ㄧず璇ュ伐搴忎骇鍑哄伐鍗曟垚鍝侊紱閫夋嫨澶氫釜琛ㄧず涓�涓伐搴忎骇鍑哄绉嶅崐鎴愬搧銆�
+ </p>
+ </Card>
+
+ <!-- 鎶曞叆鐗╂枡 -->
+ <Card :bordered="false" class="!border border-gray-200">
+ <template #title>
+ <span class="text-sm font-medium">
+ 鎶曞叆鐗╂枡{{ productItemId ? `锛�${props.product?.itemName}锛塦 : '' }}
+ </span>
+ </template>
+ <Empty
+ v-if="!productItemId"
+ :image="Empty.PRESENTED_IMAGE_SIMPLE"
+ description="璇峰厛鍦ㄩ《閮ㄧ粦瀹氱敓浜т骇鍝�"
+ />
+ <template v-else>
+ <Table
+ :data-source="bomList"
+ :columns="columns"
+ :pagination="false"
+ size="small"
+ row-key="id"
+ >
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.key === 'item'">
+ <div class="leading-4">
+ <div class="text-sm">{{ record.itemName }}</div>
+ <div class="text-xs text-gray-400">{{ record.itemCode }}</div>
+ </div>
+ </template>
+ <template v-else-if="column.key === 'quantity'">
+ <InputNumber
+ v-if="editable"
+ :value="record.quantity"
+ :min="0"
+ :precision="2"
+ size="small"
+ class="!w-24"
+ @change="(value) => handleQuantityChange(record, value)"
+ />
+ <span v-else>{{ record.quantity }}</span>
+ </template>
+ <template v-else-if="column.key === 'remark'">
+ <span v-if="!editable">{{ record.remark }}</span>
+ <Input
+ v-else
+ :value="getRemarkDraft(record)"
+ :maxlength="255"
+ size="small"
+ placeholder="澶囨敞"
+ @update:value="(value: string) => handleRemarkInput(record, value)"
+ @blur="() => handleRemarkChange(record, getRemarkDraft(record))"
+ @pressEnter="() => handleRemarkChange(record, getRemarkDraft(record))"
+ />
+ </template>
+ <template v-else-if="column.key === 'action'">
+ <Popconfirm
+ v-if="editable"
+ title="纭鍒犻櫎璇ユ姇鍏ョ墿鏂欙紵"
+ ok-text="纭畾"
+ cancel-text="鍙栨秷"
+ @confirm="handleBomDelete(record)"
+ >
+ <Button type="link" size="small" danger> 鍒犻櫎 </Button>
+ </Popconfirm>
+ </template>
+ </template>
+ </Table>
+ <div class="mt-2">
+ <template v-if="editable && adding">
+ <div class="flex items-center gap-2">
+ <MdProductBomSelect
+ :item-id="productItemId"
+ multiple
+ placeholder="閫夋嫨璇ヤ骇鍝佺殑 BOM 鐗╂枡锛堝彲澶氶�夛級"
+ @change="handleBomSelected"
+ />
+ <Button size="small" @click="adding = false">鍙栨秷</Button>
+ </div>
+ </template>
+ <div v-else-if="editable" class="flex items-center gap-3">
+ <Button size="small" type="dashed" block @click="adding = true">
+ 锛� 鏂板鎶曞叆鐗╂枡
+ </Button>
+ <span v-if="bomList.length === 0" class="text-xs text-orange-500">
+ 璇ュ伐搴忓皻鏈厤缃姇鍏ョ墿鏂欙紱鏍囦负銆屽�掑啿銆嶇殑宸ュ簭鎶ュ伐鏃朵細鎸夋澶勭敤閲忚嚜鍔ㄦ墸鏂�
+ </span>
+ </div>
+ </div>
+ </template>
+ </Card>
+ </template>
+
+ <!-- 鏃犲伐搴忕┖鎬� -->
+ <div
+ v-else
+ class="flex h-56 items-center justify-center rounded-lg border border-dashed border-gray-300 text-sm text-gray-400"
+ >
+ 璇峰厛鍦ㄥ乏渚ф柊澧炲苟閫変腑涓�閬撳伐搴�
+ </div>
+ </div>
+ </div>
+</template>
\ No newline at end of file
diff --git a/src/views/mes/qc/batchtrace/data.ts b/src/views/mes/qc/batchtrace/data.ts
index 52e9620..3013c2e 100644
--- a/src/views/mes/qc/batchtrace/data.ts
+++ b/src/views/mes/qc/batchtrace/data.ts
@@ -5,6 +5,8 @@
import { markRaw } from 'vue';
+import { DICT_TYPE } from '@vben/constants';
+
import { MdClientSelect } from '#/views/mes/md/client/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
@@ -227,3 +229,176 @@
},
];
}
+
+/** 閲囪喘鏉ユ簮鍒楄〃鐨勫瓧娈� */
+export function usePurchaseSourceColumns(): VxeTableGridOptions<MesWmBatchApi.PurchaseSource>['columns'] {
+ return [
+ {
+ field: 'receiptCode',
+ title: '閲囪喘鍏ュ簱鍗曞彿',
+ minWidth: 160,
+ },
+ {
+ field: 'receiptDate',
+ title: '鍏ュ簱鏃ユ湡',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'purchaseOrderCode',
+ title: '閲囪喘璁㈠崟缂栧彿',
+ minWidth: 160,
+ },
+ {
+ field: 'vendorCode',
+ title: '渚涘簲鍟嗙紪鐮�',
+ minWidth: 140,
+ },
+ {
+ field: 'vendorName',
+ title: '渚涘簲鍟嗗悕绉�',
+ minWidth: 140,
+ },
+ {
+ field: 'itemCode',
+ title: '鐗╂枡缂栫爜',
+ minWidth: 140,
+ },
+ {
+ field: 'itemName',
+ title: '鐗╂枡鍚嶇О',
+ minWidth: 160,
+ },
+ {
+ field: 'iqcCode',
+ title: 'IQC 妫�楠屽崟鍙�',
+ minWidth: 160,
+ },
+ {
+ field: 'checkResult',
+ title: 'IQC 妫�楠岀粨璁�',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_QC_CHECK_RESULT },
+ },
+ },
+ {
+ field: 'receivedQuantity',
+ title: '鍏ュ簱鏁伴噺',
+ width: 100,
+ },
+ ];
+}
+
+/** 閿�鍞幓鍚戝垪琛ㄧ殑瀛楁 */
+export function useSalesTargetColumns(): VxeTableGridOptions<MesWmBatchApi.SalesTarget>['columns'] {
+ return [
+ {
+ field: 'salesCode',
+ title: '閿�鍞嚭搴撳崟鍙�',
+ minWidth: 160,
+ },
+ {
+ field: 'salesDate',
+ title: '鍑哄簱鏃ユ湡',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'salesOrderCode',
+ title: '閿�鍞鍗曠紪鍙�',
+ minWidth: 160,
+ },
+ {
+ field: 'clientName',
+ title: '瀹㈡埛鍚嶇О',
+ minWidth: 140,
+ },
+ {
+ field: 'itemCode',
+ title: '鐗╂枡缂栫爜',
+ minWidth: 140,
+ },
+ {
+ field: 'itemName',
+ title: '鐗╂枡鍚嶇О',
+ minWidth: 160,
+ },
+ {
+ field: 'oqcCode',
+ title: 'OQC 妫�楠屽崟鍙�',
+ minWidth: 160,
+ },
+ {
+ field: 'checkResult',
+ title: 'OQC 妫�楠岀粨璁�',
+ width: 110,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_QC_CHECK_RESULT },
+ },
+ },
+ {
+ field: 'quantity',
+ title: '鍑哄簱鏁伴噺',
+ width: 100,
+ },
+ {
+ field: 'qualityStatus',
+ title: '璐ㄩ噺鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
+ },
+ },
+ ];
+}
+
+/** 宸ュ簭娴佽浆鍒楄〃鐨勫瓧娈� */
+export function useProcessTraceColumns(): VxeTableGridOptions<MesWmBatchApi.ProcessTrace>['columns'] {
+ return [
+ {
+ field: 'eventType',
+ title: '浜嬩欢绫诲瀷',
+ width: 90,
+ slots: { default: 'eventType' },
+ },
+ {
+ field: 'workOrderCode',
+ title: '鐢熶骇宸ュ崟鍙�',
+ minWidth: 160,
+ },
+ {
+ field: 'processCode',
+ title: '宸ュ簭缂栫爜',
+ minWidth: 120,
+ },
+ {
+ field: 'processName',
+ title: '宸ュ簭鍚嶇О',
+ minWidth: 140,
+ },
+ {
+ field: 'quantity',
+ title: '鏁伴噺',
+ width: 100,
+ },
+ {
+ field: 'qualityStatus',
+ title: '璐ㄩ噺鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
+ },
+ },
+ {
+ field: 'eventTime',
+ title: '浜嬩欢鏃堕棿',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ ];
+}
diff --git a/src/views/mes/qc/batchtrace/index.vue b/src/views/mes/qc/batchtrace/index.vue
index b403dec..1762b35 100644
--- a/src/views/mes/qc/batchtrace/index.vue
+++ b/src/views/mes/qc/batchtrace/index.vue
@@ -4,8 +4,10 @@
import { Page, useVbenModal } from '@vben/common-ui';
+import { Input, message } from 'ant-design-vue';
+
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getBatchPage } from '#/api/mes/wm/batch';
+import { getBatchBySn, getBatchPage } from '#/api/mes/wm/batch';
import { useGridColumns, useGridFormSchema } from './data';
import TraceDetail from './modules/trace-detail.vue';
@@ -18,6 +20,19 @@
/** 鎵撳紑鎵规杩芥函璇︽儏 */
function handleTrace(row: MesWmBatchApi.Batch) {
detailModalApi.setData(row).open();
+}
+
+/** 鏍规嵁 SN 鍙嶆煡鎵规骞舵墦寮�璇︽儏 */
+async function handleSearchBySn(sn: string) {
+ if (!sn) {
+ return;
+ }
+ const batch = await getBatchBySn(sn.trim());
+ if (batch) {
+ handleTrace(batch);
+ } else {
+ message.warning(`鏈壘鍒� SN銆�${sn}銆嶅搴旂殑鎵规`);
+ }
}
const [Grid] = useVbenVxeGrid({
@@ -54,6 +69,15 @@
<template>
<Page auto-content-height><DetailModal />
+ <div class="mb-3 w-80">
+ <Input.Search
+ placeholder="杈撳叆 SN 搴忓垪鍙峰弽鏌ユ壒娆�"
+ enter-button="鍙嶆煡"
+ allow-clear
+ @search="handleSearchBySn"
+ />
+ </div>
+
<Grid table-title="鎵规杩芥函鍒楄〃">
<template #actions="{ row }">
<TableAction
diff --git a/src/views/mes/qc/batchtrace/modules/process-trace-list.vue b/src/views/mes/qc/batchtrace/modules/process-trace-list.vue
new file mode 100644
index 0000000..2c3725e
--- /dev/null
+++ b/src/views/mes/qc/batchtrace/modules/process-trace-list.vue
@@ -0,0 +1,66 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesWmBatchApi } from '#/api/mes/wm/batch';
+
+import { watch } from 'vue';
+
+import { Tag } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getProcessTraceList } from '#/api/mes/wm/batch';
+
+import { useProcessTraceColumns } from '../data';
+
+const props = defineProps<{
+ batchCode?: string;
+}>();
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useProcessTraceColumns(),
+ minHeight: 240,
+ pagerConfig: {
+ enabled: false,
+ },
+ rowConfig: {
+ isHover: true,
+ },
+ showOverflow: true,
+ toolbarConfig: {
+ enabled: false,
+ },
+ } as VxeTableGridOptions<MesWmBatchApi.ProcessTrace>,
+});
+
+/** 鍔犺浇宸ュ簭娴佽浆鍒楄〃 */
+async function getList() {
+ if (!props.batchCode) {
+ await gridApi.grid?.loadData([]);
+ return;
+ }
+ gridApi.setLoading(true);
+ try {
+ const list = await getProcessTraceList(props.batchCode);
+ await gridApi.grid?.loadData(list);
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+watch(
+ () => props.batchCode,
+ () => {
+ getList();
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <Grid class="w-full">
+ <template #eventType="{ row }">
+ <Tag v-if="row.eventType === 'OUT'" color="success">浜у嚭</Tag>
+ <Tag v-else color="processing">鎶曞叆</Tag>
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/qc/batchtrace/modules/purchase-source-list.vue b/src/views/mes/qc/batchtrace/modules/purchase-source-list.vue
new file mode 100644
index 0000000..472a6a4
--- /dev/null
+++ b/src/views/mes/qc/batchtrace/modules/purchase-source-list.vue
@@ -0,0 +1,59 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesWmBatchApi } from '#/api/mes/wm/batch';
+
+import { watch } from 'vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getPurchaseSourceList } from '#/api/mes/wm/batch';
+
+import { usePurchaseSourceColumns } from '../data';
+
+const props = defineProps<{
+ batchCode?: string;
+}>();
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: usePurchaseSourceColumns(),
+ minHeight: 240,
+ pagerConfig: {
+ enabled: false,
+ },
+ rowConfig: {
+ isHover: true,
+ },
+ showOverflow: true,
+ toolbarConfig: {
+ enabled: false,
+ },
+ } as VxeTableGridOptions<MesWmBatchApi.PurchaseSource>,
+});
+
+/** 鍔犺浇閲囪喘鏉ユ簮鍒楄〃 */
+async function getList() {
+ if (!props.batchCode) {
+ await gridApi.grid?.loadData([]);
+ return;
+ }
+ gridApi.setLoading(true);
+ try {
+ const list = await getPurchaseSourceList(props.batchCode);
+ await gridApi.grid?.loadData(list);
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+watch(
+ () => props.batchCode,
+ () => {
+ getList();
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <Grid class="w-full" />
+</template>
diff --git a/src/views/mes/qc/batchtrace/modules/sales-target-list.vue b/src/views/mes/qc/batchtrace/modules/sales-target-list.vue
new file mode 100644
index 0000000..0963502
--- /dev/null
+++ b/src/views/mes/qc/batchtrace/modules/sales-target-list.vue
@@ -0,0 +1,59 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesWmBatchApi } from '#/api/mes/wm/batch';
+
+import { watch } from 'vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getSalesTargetList } from '#/api/mes/wm/batch';
+
+import { useSalesTargetColumns } from '../data';
+
+const props = defineProps<{
+ batchCode?: string;
+}>();
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useSalesTargetColumns(),
+ minHeight: 240,
+ pagerConfig: {
+ enabled: false,
+ },
+ rowConfig: {
+ isHover: true,
+ },
+ showOverflow: true,
+ toolbarConfig: {
+ enabled: false,
+ },
+ } as VxeTableGridOptions<MesWmBatchApi.SalesTarget>,
+});
+
+/** 鍔犺浇閿�鍞幓鍚戝垪琛� */
+async function getList() {
+ if (!props.batchCode) {
+ await gridApi.grid?.loadData([]);
+ return;
+ }
+ gridApi.setLoading(true);
+ try {
+ const list = await getSalesTargetList(props.batchCode);
+ await gridApi.grid?.loadData(list);
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+watch(
+ () => props.batchCode,
+ () => {
+ getList();
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <Grid class="w-full" />
+</template>
diff --git a/src/views/mes/qc/batchtrace/modules/trace-detail.vue b/src/views/mes/qc/batchtrace/modules/trace-detail.vue
index 7fcba7e..8d01d5b 100644
--- a/src/views/mes/qc/batchtrace/modules/trace-detail.vue
+++ b/src/views/mes/qc/batchtrace/modules/trace-detail.vue
@@ -5,15 +5,24 @@
import { useVbenModal } from '@vben/common-ui';
-import { Tabs } from 'ant-design-vue';
+import { Tabs, Tooltip } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
+import ProcessTraceList from './process-trace-list.vue';
+import PurchaseSourceList from './purchase-source-list.vue';
+import SalesTargetList from './sales-target-list.vue';
import TraceList from './trace-list.vue';
const detailData = ref<MesWmBatchApi.Batch>(); // 褰撳墠鎵规璇︽儏
-const subTabsName = ref<'backward' | 'forward'>('forward'); // 褰撳墠婵�娲荤殑杩芥函鏂瑰悜
+const subTabsName = ref<
+ | 'backward'
+ | 'forward'
+ | 'purchase'
+ | 'sales'
+ | 'process'
+>('forward'); // 褰撳墠婵�娲荤殑杩芥函椤电
const [Descriptions] = useDescription({
bordered: true,
@@ -47,12 +56,46 @@
class="mx-4 mt-4"
type="card"
>
- <Tabs.TabPane key="forward" tab="鍚戝墠杩芥函">
+ <Tabs.TabPane key="forward">
+ <template #tab>
+ <Tooltip title="椤虹潃鐢熶骇娴佸線涓嬫父鏌ワ細璇ユ壒娆¤鍝簺宸ュ崟娑堣�楀悗锛屼骇鍑烘垚浜嗗摢浜涙垚鍝佹壒娆�">
+ <span>鍚戝墠杩芥函</span>
+ </Tooltip>
+ </template>
<TraceList :batch-code="detailData.code" direction="forward" />
</Tabs.TabPane>
- <Tabs.TabPane key="backward" tab="鍚戝悗杩芥函">
+ <Tabs.TabPane key="backward">
+ <template #tab>
+ <Tooltip title="閫嗙潃鐢熶骇娴佸線涓婃父鏌ワ細鐢熶骇璇ユ壒娆℃椂锛屾秷鑰椾簡鍝簺鍘熸枡鎵规">
+ <span>鍚戝悗杩芥函</span>
+ </Tooltip>
+ </template>
<TraceList :batch-code="detailData.code" direction="backward" />
</Tabs.TabPane>
+ <Tabs.TabPane key="purchase">
+ <template #tab>
+ <Tooltip title="鍘熸枡渚ф潵婧愶細璇ユ壒娆″搴旂殑閲囪喘鍏ュ簱鍗曚笌 IQC 鏉ユ枡妫�楠岃褰�">
+ <span>閲囪喘鏉ユ簮</span>
+ </Tooltip>
+ </template>
+ <PurchaseSourceList :batch-code="detailData.code" />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="sales">
+ <template #tab>
+ <Tooltip title="鎴愬搧渚у幓鍚戯細璇ユ壒娆″搴旂殑閿�鍞嚭搴撳崟涓� OQC 鍑哄巶妫�楠岃褰�">
+ <span>閿�鍞幓鍚�</span>
+ </Tooltip>
+ </template>
+ <SalesTargetList :batch-code="detailData.code" />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="process">
+ <template #tab>
+ <Tooltip title="鍚勫伐搴忕殑鎶曞叆/浜у嚭浜嬩欢锛氳鎵规鍦ㄥ摢浜涘伐搴忚鎶曞叆鎴栦骇鍑猴紝鍙婃暟閲忓拰璐ㄩ噺鐘舵��">
+ <span>宸ュ簭娴佽浆</span>
+ </Tooltip>
+ </template>
+ <ProcessTraceList :batch-code="detailData.code" />
+ </Tabs.TabPane>
</Tabs>
</Modal>
</template>
diff --git a/src/views/mes/qc/components/export-range-modal.vue b/src/views/mes/qc/components/export-range-modal.vue
new file mode 100644
index 0000000..19fedfe
--- /dev/null
+++ b/src/views/mes/qc/components/export-range-modal.vue
@@ -0,0 +1,89 @@
+<script lang="ts" setup>
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { DatePicker, message } from 'ant-design-vue';
+
+import { getRangePickerDefaultProps } from '#/utils';
+
+/** 瀵煎嚭鍙傛暟锛堝悗绔� PageReqVO 浠ユ暟缁勮В鏋� inspectDate 璧锋锛� */
+interface ExportParams {
+ inspectDate?: [string, string];
+}
+
+interface ExportModalData {
+ /** 寮圭獥鏍囬 */
+ title?: string;
+ /** 妯″潡瀵煎嚭鍑芥暟锛歳equestClient.download(...) 杩斿洖 Blob */
+ exportApi: (params: ExportParams) => Promise<Blob>;
+ /** 瀵煎嚭鏂囦欢鍚� */
+ fileName: string;
+}
+
+defineOptions({ name: 'QcExportRangeModal' });
+
+const rangePickerProps = getRangePickerDefaultProps();
+
+const modalData = ref<ExportModalData>();
+const range = ref<[string, string] | undefined>(undefined);
+const exporting = ref(false);
+const title = ref('瀵煎嚭');
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (exporting.value) {
+ return;
+ }
+ const data = modalData.value;
+ if (!data) {
+ return;
+ }
+ exporting.value = true;
+ modalApi.lock();
+ try {
+ const params: ExportParams =
+ range.value && range.value.length === 2
+ ? { inspectDate: range.value }
+ : {};
+ const blob = await data.exportApi(params);
+ downloadFileFromBlobPart({ fileName: data.fileName, source: blob });
+ message.success('瀵煎嚭鎴愬姛');
+ await modalApi.close();
+ } finally {
+ exporting.value = false;
+ modalApi.unlock();
+ }
+ },
+ onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ modalData.value = undefined;
+ range.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<ExportModalData>();
+ modalData.value = data;
+ range.value = undefined;
+ title.value = data?.title ?? '瀵煎嚭';
+ modalApi.setState({ confirmText: '瀵煎嚭' });
+ },
+});
+</script>
+
+<template>
+ <Modal :title="title" class="w-2/5" :mask-closable="false">
+ <div class="px-1 py-3">
+ <div class="mb-2 text-sm font-medium text-gray-700">妫�楠屾椂闂�</div>
+ <DatePicker.RangePicker
+ v-model:value="range"
+ v-bind="rangePickerProps"
+ allow-clear
+ class="w-full"
+ />
+ <div class="mt-2 text-xs text-gray-400">
+ 涓嶉�夋椂闂村垯瀵煎嚭鍏ㄩ儴鏁版嵁锛涘鍑烘寜銆屾娴嬫棩鏈熴�嶈繃婊�
+ </div>
+ </div>
+ </Modal>
+</template>
diff --git a/src/views/mes/qc/ipqc/data.ts b/src/views/mes/qc/ipqc/data.ts
index b4979f3..6fa2834 100644
--- a/src/views/mes/qc/ipqc/data.ts
+++ b/src/views/mes/qc/ipqc/data.ts
@@ -16,7 +16,9 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProTaskSelect } from '#/views/mes/pro/task/components';
@@ -202,6 +204,39 @@
statuses: [MesProTaskStatusEnum.PREPARE],
workOrderId: values.workOrderId,
workstationId: values.workstationId,
+ }),
+ },
+ },
+ {
+ fieldName: 'templateId',
+ label: '璐ㄦ鏂规',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ placeholder: '璇烽�夋嫨璐ㄦ鏂规',
+ },
+ rules: 'selectRequired',
+ dependencies: {
+ triggerFields: ['workOrderId', 'taskId'],
+ componentProps: (values) => ({
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ disabled: formType !== 'create' || !values.workOrderId,
+ params: values.workOrderId
+ ? {
+ qcType: 2,
+ workOrderId: values.workOrderId,
+ taskId: values.taskId,
+ }
+ : {},
+ placeholder: values.workOrderId
+ ? '璇烽�夋嫨璐ㄦ鏂规'
+ : '璇峰厛閫夋嫨鐢熶骇璁㈠崟',
}),
},
},
@@ -404,6 +439,14 @@
placeholder: '璇烽�夋嫨妫�娴嬬粨鏋�',
},
},
+ {
+ fieldName: 'inspectDate',
+ label: '妫�娴嬫棩鏈�',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ },
+ },
];
}
diff --git a/src/views/mes/qc/ipqc/index.vue b/src/views/mes/qc/ipqc/index.vue
index 82bf057..6864768 100644
--- a/src/views/mes/qc/ipqc/index.vue
+++ b/src/views/mes/qc/ipqc/index.vue
@@ -4,7 +4,6 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
-import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -12,12 +11,18 @@
import { exportProductWord } from '#/api/mes/qc/indicatorresult';
import { deleteIpqc, exportIpqc, getIpqcPage } from '#/api/mes/qc/ipqc';
import { $t } from '#/locales';
+import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
+ connectedComponent: QcExportRangeModal,
destroyOnClose: true,
});
@@ -56,10 +61,15 @@
}
}
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportIpqc(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '杩囩▼妫�楠屽崟.xls', source: data });
+/** 瀵煎嚭琛ㄦ牸锛堝脊绐楅�夋楠屾椂闂磋寖鍥达級 */
+function handleExport() {
+ exportRangeModalApi
+ .setData({
+ title: '鎸夋楠屾椂闂村鍑鸿繃绋嬫楠屽崟',
+ exportApi: exportIpqc,
+ fileName: '杩囩▼妫�楠屽崟.xls',
+ })
+ .open();
}
/** 涓嬭浇璐ㄦ鎶ュ憡锛堜骇鍝佹楠屽崟 Word锛屾墦鍗扮敤 Word 鎵撳紑锛� */
@@ -108,8 +118,9 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
-
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ExportRangeModal />
<Grid table-title="杩囩▼妫�楠屽崟鍒楄〃">
<template #toolbar-tools>
<TableAction
diff --git a/src/views/mes/qc/iqc/data.ts b/src/views/mes/qc/iqc/data.ts
index fa7bfa6..42d8d6d 100644
--- a/src/views/mes/qc/iqc/data.ts
+++ b/src/views/mes/qc/iqc/data.ts
@@ -11,6 +11,7 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
@@ -135,6 +136,33 @@
},
},
{
+ fieldName: 'templateId',
+ label: '璐ㄦ鏂规',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ placeholder: '璇烽�夋嫨璐ㄦ鏂规',
+ },
+ rules: 'selectRequired',
+ dependencies: {
+ triggerFields: ['itemId'],
+ componentProps: (values) => ({
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ disabled: formType !== 'create' || !values.itemId,
+ params: values.itemId
+ ? { itemId: values.itemId, qcType: 1 }
+ : {},
+ placeholder: values.itemId ? '璇烽�夋嫨璐ㄦ鏂规' : '璇峰厛閫夋嫨浜у搧鐗╂枡',
+ }),
+ },
+ },
+ {
fieldName: 'vendorId',
label: '渚涘簲鍟�',
component: 'ApiSelect',
diff --git a/src/views/mes/qc/iqc/index.vue b/src/views/mes/qc/iqc/index.vue
index 75a2d25..f13e656 100644
--- a/src/views/mes/qc/iqc/index.vue
+++ b/src/views/mes/qc/iqc/index.vue
@@ -7,7 +7,6 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
-import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -15,6 +14,7 @@
import { exportProductWord } from '#/api/mes/qc/indicatorresult';
import { deleteIqc, exportIqc, getIqcPage } from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
+import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
@@ -25,6 +25,11 @@
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
+ connectedComponent: QcExportRangeModal,
destroyOnClose: true,
});
@@ -63,10 +68,15 @@
}
}
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportIqc(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '鏉ユ枡妫�楠屽崟.xls', source: data });
+/** 瀵煎嚭琛ㄦ牸锛堝脊绐楅�夋楠屾椂闂磋寖鍥达級 */
+function handleExport() {
+ exportRangeModalApi
+ .setData({
+ title: '鎸夋楠屾椂闂村鍑烘潵鏂欐楠屽崟',
+ exportApi: exportIqc,
+ fileName: '鏉ユ枡妫�楠屽崟.xls',
+ })
+ .open();
}
/** 涓嬭浇璐ㄦ鎶ュ憡锛堜骇鍝佹楠屽崟 Word锛屾墦鍗扮敤 Word 鎵撳紑锛� */
@@ -122,8 +132,9 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
-
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ExportRangeModal />
<Grid table-title="鏉ユ枡妫�楠屽崟鍒楄〃">
<template #toolbar-tools>
<TableAction
diff --git a/src/views/mes/qc/oqc/data.ts b/src/views/mes/qc/oqc/data.ts
index 1143391..72fa17e 100644
--- a/src/views/mes/qc/oqc/data.ts
+++ b/src/views/mes/qc/oqc/data.ts
@@ -11,7 +11,9 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
@@ -129,6 +131,33 @@
componentProps: (values) => ({
disabled: !!values.sourceDocId,
placeholder: '璇烽�夋嫨浜у搧鐗╂枡',
+ }),
+ },
+ },
+ {
+ fieldName: 'templateId',
+ label: '璐ㄦ鏂规',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ placeholder: '璇烽�夋嫨璐ㄦ鏂规',
+ },
+ rules: 'selectRequired',
+ dependencies: {
+ triggerFields: ['itemId'],
+ componentProps: (values) => ({
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ disabled: formType !== 'create' || !values.itemId,
+ params: values.itemId
+ ? { itemId: values.itemId, qcType: 3 }
+ : {},
+ placeholder: values.itemId ? '璇烽�夋嫨璐ㄦ鏂规' : '璇峰厛閫夋嫨浜у搧鐗╂枡',
}),
},
},
@@ -340,6 +369,14 @@
placeholder: '璇烽�夋嫨妫�娴嬬粨鏋�',
},
},
+ {
+ fieldName: 'inspectDate',
+ label: '妫�娴嬫棩鏈�',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ },
+ },
];
}
diff --git a/src/views/mes/qc/oqc/index.vue b/src/views/mes/qc/oqc/index.vue
index 8647b27..a1f3965 100644
--- a/src/views/mes/qc/oqc/index.vue
+++ b/src/views/mes/qc/oqc/index.vue
@@ -4,7 +4,6 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
-import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -12,12 +11,18 @@
import { exportProductWord } from '#/api/mes/qc/indicatorresult';
import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
+import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
+ connectedComponent: QcExportRangeModal,
destroyOnClose: true,
});
@@ -56,10 +61,15 @@
}
}
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportOqc(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '鍑鸿揣妫�楠屽崟.xls', source: data });
+/** 瀵煎嚭琛ㄦ牸锛堝脊绐楅�夋楠屾椂闂磋寖鍥达級 */
+function handleExport() {
+ exportRangeModalApi
+ .setData({
+ title: '鎸夋楠屾椂闂村鍑鸿揣妫�楠屽崟',
+ exportApi: exportOqc,
+ fileName: '鍑鸿揣妫�楠屽崟.xls',
+ })
+ .open();
}
/** 涓嬭浇璐ㄦ鎶ュ憡锛堜骇鍝佹楠屽崟 Word锛屾墦鍗扮敤 Word 鎵撳紑锛� */
@@ -108,8 +118,9 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
-
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ExportRangeModal />
<Grid table-title="鍑鸿揣妫�楠屽崟鍒楄〃">
<template #toolbar-tools>
<TableAction
diff --git a/src/views/mes/qc/rqc/data.ts b/src/views/mes/qc/rqc/data.ts
index 0026430..7901951 100644
--- a/src/views/mes/qc/rqc/data.ts
+++ b/src/views/mes/qc/rqc/data.ts
@@ -15,7 +15,9 @@
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { getTemplateItemOptionList } from '#/api/mes/qc/template/item';
import { getSimpleUserList } from '#/api/system/user';
+import { getRangePickerDefaultProps } from '#/utils';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
/** 琛ㄥ崟绫诲瀷 */
@@ -154,6 +156,33 @@
componentProps: (values) => ({
disabled: !!values.sourceDocId,
placeholder: '璇烽�夋嫨浜у搧鐗╂枡',
+ }),
+ },
+ },
+ {
+ fieldName: 'templateId',
+ label: '璐ㄦ鏂规',
+ component: 'ApiSelect',
+ componentProps: {
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ placeholder: '璇烽�夋嫨璐ㄦ鏂规',
+ },
+ rules: 'selectRequired',
+ dependencies: {
+ triggerFields: ['itemId'],
+ componentProps: (values) => ({
+ api: getTemplateItemOptionList,
+ autoSelect: 'first',
+ labelField: 'templateName',
+ valueField: 'templateId',
+ disabled: formType !== 'create' || !values.itemId,
+ params: values.itemId
+ ? { itemId: values.itemId, qcType: 4 }
+ : {},
+ placeholder: values.itemId ? '璇烽�夋嫨璐ㄦ鏂规' : '璇峰厛閫夋嫨浜у搧鐗╂枡',
}),
},
},
@@ -330,6 +359,14 @@
valueField: 'id',
},
},
+ {
+ fieldName: 'inspectDate',
+ label: '妫�娴嬫棩鏈�',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ },
+ },
];
}
diff --git a/src/views/mes/qc/rqc/index.vue b/src/views/mes/qc/rqc/index.vue
index 08efd64..d901f6f 100644
--- a/src/views/mes/qc/rqc/index.vue
+++ b/src/views/mes/qc/rqc/index.vue
@@ -4,7 +4,6 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { MesQcStatusEnum } from '@vben/constants';
-import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
@@ -12,12 +11,18 @@
import { exportProductWord } from '#/api/mes/qc/indicatorresult';
import { deleteRqc, exportRqc, getRqcPage } from '#/api/mes/qc/rqc';
import { $t } from '#/locales';
+import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
+ connectedComponent: QcExportRangeModal,
destroyOnClose: true,
});
@@ -56,10 +61,15 @@
}
}
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportRqc(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '閫�璐ф楠屽崟.xls', source: data });
+/** 瀵煎嚭琛ㄦ牸锛堝脊绐楅�夋楠屾椂闂磋寖鍥达級 */
+function handleExport() {
+ exportRangeModalApi
+ .setData({
+ title: '鎸夋楠屾椂闂村閫�璐ф楠屽崟',
+ exportApi: exportRqc,
+ fileName: '閫�璐ф楠屽崟.xls',
+ })
+ .open();
}
/** 涓嬭浇璐ㄦ鎶ュ憡锛堜骇鍝佹楠屽崟 Word锛屾墦鍗扮敤 Word 鎵撳紑锛� */
@@ -108,8 +118,9 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
-
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <ExportRangeModal />
<Grid table-title="閫�璐ф楠屽崟鍒楄〃">
<template #toolbar-tools>
<TableAction
diff --git a/src/views/mes/qc/template/data.ts b/src/views/mes/qc/template/data.ts
index 1f5add0..2403139 100644
--- a/src/views/mes/qc/template/data.ts
+++ b/src/views/mes/qc/template/data.ts
@@ -17,7 +17,7 @@
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
-import { getItemPage } from '#/api/mdm/item';
+import { getItemSimpleList } from '#/api/mdm/item';
import { MdUnitMeasureSelect } from '#/views/mes/md/unitmeasure/components';
import { QcIndicatorSelect } from '#/views/mes/qc/indicator/components';
@@ -426,8 +426,7 @@
placeholder: '璇烽�夋嫨浜у搧鐗╂枡',
allowClear: true,
showSearch: true,
- api: getItemPage,
- resultField: 'list',
+ api: getItemSimpleList,
labelField: 'name',
valueField: 'id',
optionRender: (option: any) => {
diff --git a/src/views/mes/workbench/components/TaskListPanel.vue b/src/views/mes/workbench/components/TaskListPanel.vue
index 6530d39..90141ef 100644
--- a/src/views/mes/workbench/components/TaskListPanel.vue
+++ b/src/views/mes/workbench/components/TaskListPanel.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
-import type { PendingTask } from '../../types';
+
import { computed, ref } from 'vue';
@@ -15,7 +15,13 @@
const emit = defineEmits<{
select: [task: PendingTask];
+ refresh: [];
}>();
+
+import { pauseTask, resumeTask } from '#/api/mes/pro/task';
+import { Form, FormItem, Input, Modal, message, Button } from 'ant-design-vue';
+import { confirm } from '@vben/common-ui';
+import type {PendingTask} from "#/views/mes/workbench/types";
const searchKeyword = ref('');
@@ -41,6 +47,50 @@
if (rate >= 100) return '#52c41a';
if (rate >= 60) return '#1890ff';
return '#faad14';
+}
+
+const pauseModalVisible = ref(false);
+const pauseFormRef = ref();
+const pauseFormState = ref({ id: 0, reason: '', remark: '' });
+const pauseLoading = ref(false);
+
+const pauseRules = {
+ reason: [{ required: true, message: '璇疯緭鍏ュ仠宸ュ師鍥�', trigger: 'blur' }],
+};
+
+function handlePause(item: PendingTask, event: Event) {
+ event.stopPropagation();
+ pauseFormState.value = { id: item.id!, reason: '', remark: '' };
+ pauseModalVisible.value = true;
+}
+
+async function submitPause() {
+ await pauseFormRef.value?.validate();
+ pauseLoading.value = true;
+ try {
+ await pauseTask(pauseFormState.value);
+ message.success('鍋滃伐鎴愬姛');
+ pauseModalVisible.value = false;
+ emit('refresh');
+ } finally {
+ pauseLoading.value = false;
+ }
+}
+
+async function handleResume(item: PendingTask, event: Event) {
+ event.stopPropagation();
+ try {
+ await confirm('纭瑕佸宸ヨ浠诲姟鍚楋紵');
+ } catch {
+ return;
+ }
+ try {
+ await resumeTask({ id: item.id! });
+ message.success('澶嶅伐鎴愬姛');
+ emit('refresh');
+ } catch (error: any) {
+ // Error is handled by request interceptor
+ }
}
</script>
@@ -99,6 +149,27 @@
>
{{ item.feedbackRate ?? 0 }}%
</span>
+ <div class="task-card__actions">
+ <Button
+ v-if="item.status === 9"
+ type="primary"
+ size="small"
+ class="task-card__action-btn resume-btn"
+ @click="handleResume(item, $event)"
+ >
+ 澶嶅伐
+ </Button>
+ <Button
+ v-else
+ type="primary"
+ danger
+ size="small"
+ class="task-card__action-btn pause-btn"
+ @click="handlePause(item, $event)"
+ >
+ 鍋滃伐
+ </Button>
+ </div>
</div>
<div class="task-card__progress-row">
@@ -144,6 +215,42 @@
</div>
</div>
</div>
+
+ <Modal
+ v-model:open="pauseModalVisible"
+ title="鐢熶骇浠诲姟鍋滃伐"
+ @ok="submitPause"
+ :confirm-loading="pauseLoading"
+ destroy-on-close
+ >
+ <Form
+ ref="pauseFormRef"
+ :model="pauseFormState"
+ :rules="pauseRules"
+ :label-col="{ span: 4 }"
+ :wrapper-col="{ span: 20 }"
+ style="margin-top: 20px"
+ >
+ <FormItem label="鍋滃伐鍘熷洜" name="reason">
+ <Input.TextArea
+ v-model:value="pauseFormState.reason"
+ placeholder="璇疯緭鍏ュ仠宸ュ師鍥狅紙濡傦細璁惧鏁呴殰銆佺己鏂欑瓑锛�"
+ :rows="3"
+ :maxlength="100"
+ show-count
+ />
+ </FormItem>
+ <FormItem label="澶囨敞" name="remark">
+ <Input.TextArea
+ v-model:value="pauseFormState.remark"
+ placeholder="璇疯緭鍏ュ娉ㄨ鏄�"
+ :rows="2"
+ :maxlength="200"
+ show-count
+ />
+ </FormItem>
+ </Form>
+ </Modal>
</section>
</template>
@@ -290,6 +397,32 @@
font-variant-numeric: tabular-nums;
}
+.task-card__actions {
+ display: flex;
+ align-items: center;
+ margin-left: auto;
+}
+
+.task-card__action-btn {
+ font-size: 13px;
+ border-radius: 4px;
+}
+
+.pause-btn {
+ box-shadow: 0 2px 4px rgb(255 77 79 / 20%);
+}
+
+.resume-btn {
+ background-color: #52c41a;
+ border-color: #52c41a;
+ box-shadow: 0 2px 4px rgb(82 196 26 / 20%);
+}
+.resume-btn:hover,
+.resume-btn:focus {
+ background-color: #73d13d;
+ border-color: #73d13d;
+}
+
.task-card__progress-row {
display: flex;
align-items: baseline;
diff --git a/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue b/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
index 8620c70..2f7092e 100644
--- a/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
+++ b/src/views/mes/workbench/components/tabs/FeedbackHistoryTab.vue
@@ -3,12 +3,14 @@
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
import {
DICT_TYPE,
MesProFeedbackStatusEnum,
} from '@vben/constants';
import { useVbenModal } from '@vben/common-ui';
+import { useAccess } from '@vben/access';
import { Button, message, Popconfirm, Space } from 'ant-design-vue';
@@ -30,6 +32,7 @@
const props = defineProps<{
taskId?: number;
+ task?: Record<string, any>;
/** 澶栭儴瑙﹀彂鍒锋柊 */
refreshKey?: number;
}>();
@@ -41,8 +44,20 @@
destroyOnClose: true,
});
+const { hasAccessByCodes } = useAccess();
+const router = useRouter();
+
const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
-const approvingRow = ref<MesProFeedbackApi.Feedback>();
+const batchActionType = ref<'submit' | 'approve'>('approve');
+const actionRow = ref<MesProFeedbackApi.Feedback>();
+
+/** 鎵ц鎻愪氦锛堝惈鎵规灞炴�э級 */
+async function doSubmit(row: MesProFeedbackApi.Feedback, batchFields?: Record<string, unknown>) {
+ await submitFeedback({ id: row.id!, ...batchFields });
+ message.success('鎶ュ伐鍗曞凡鎻愪氦');
+ refreshGrid();
+ emit('reported');
+}
/** 鎵ц瀹℃壒锛堝惈鎵规灞炴�э級 */
async function doApprove(row: MesProFeedbackApi.Feedback, batchFields?: Record<string, unknown>) {
@@ -74,10 +89,23 @@
/** 鎻愪氦锛堜粎鑽夌锛� */
async function handleSubmit(row: MesProFeedbackApi.Feedback) {
- await submitFeedback(row.id!);
- message.success('鎶ュ伐鍗曞凡鎻愪氦');
- refreshGrid();
- emit('reported');
+ if (props.task?.feedbackApprove) {
+ await doSubmit(row);
+ return;
+ }
+
+ try {
+ const checkResult = await getBatchRequirements(row.id!);
+ if (checkResult?.needBatchInput) {
+ batchActionType.value = 'submit';
+ actionRow.value = row;
+ batchDialogRef.value?.open({ checkResult });
+ } else {
+ await doSubmit(row);
+ }
+ } catch {
+ await doSubmit(row);
+ }
}
/** 瀹℃壒閫氳繃锛堜粎瀹℃壒涓級 */
@@ -85,7 +113,8 @@
try {
const checkResult = await getBatchRequirements(row.id!);
if (checkResult?.needBatchInput) {
- approvingRow.value = row;
+ batchActionType.value = 'approve';
+ actionRow.value = row;
batchDialogRef.value?.open({ checkResult });
} else {
await doApprove(row);
@@ -97,8 +126,12 @@
/** 鎵规寮圭獥纭鍥炶皟 */
function handleBatchConfirm(batchData: Record<string, unknown>) {
- if (approvingRow.value) {
- doApprove(approvingRow.value, batchData);
+ if (actionRow.value) {
+ if (batchActionType.value === 'approve') {
+ doApprove(actionRow.value, batchData);
+ } else if (batchActionType.value === 'submit') {
+ doSubmit(actionRow.value, batchData);
+ }
}
}
@@ -108,6 +141,15 @@
message.success('鎶ュ伐鍗曞凡椹冲洖');
refreshGrid();
emit('reported');
+}
+
+/** 鏌ョ湅瀹℃壒璇︽儏 */
+function handleViewProcess(row: MesProFeedbackApi.Feedback) {
+ if (!row.processInstanceId) {
+ message.warning('璇ユ姤宸ュ皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } });
}
/** 鐘舵�佸父閲忎緵妯℃澘浣跨敤 */
@@ -274,9 +316,18 @@
鍒犻櫎
</Button>
</Popconfirm>
- <!-- 瀹℃壒涓細瀹℃壒閫氳繃銆侀┏鍥� -->
- <Popconfirm
+ <!-- 瀹℃壒涓細鏌ョ湅瀹℃壒 -->
+ <Button
v-if="row.status === STATUS_APPROVING"
+ type="link"
+ size="small"
+ @click="handleViewProcess(row)"
+ >
+ 鏌ョ湅瀹℃壒
+ </Button>
+ <!--
+ <Popconfirm
+ v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])"
title="纭瀹℃壒閫氳繃璇ユ姤宸ュ崟锛�"
@confirm="handleApprove(row)"
>
@@ -285,7 +336,7 @@
</Button>
</Popconfirm>
<Popconfirm
- v-if="row.status === STATUS_APPROVING"
+ v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])"
title="纭椹冲洖璇ユ姤宸ュ崟锛�"
@confirm="handleReject(row)"
>
@@ -293,6 +344,7 @@
椹冲洖
</Button>
</Popconfirm>
+ -->
</Space>
</template>
</Grid>
diff --git a/src/views/mes/workbench/components/tabs/FeedbackTab.vue b/src/views/mes/workbench/components/tabs/FeedbackTab.vue
index 8439b83..86b6e25 100644
--- a/src/views/mes/workbench/components/tabs/FeedbackTab.vue
+++ b/src/views/mes/workbench/components/tabs/FeedbackTab.vue
@@ -1,5 +1,5 @@
<script lang="ts" setup>
-import type { PendingTask } from '../../../types';
+import type { PendingTask } from '../../types';
import type { FormType } from '../../../pro/feedback/data';
import { computed, ref, watch } from 'vue';
@@ -12,13 +12,13 @@
} from '@vben/constants';
import { useUserStore } from '@vben/stores';
-import { Button, message, Modal, Popconfirm } from 'ant-design-vue';
+import { Button, Input, message, Modal, Popconfirm } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import {
approveFeedback,
- createFeedback,
+ createFeedback, getBatchRequirements,
getFeedback,
getFeedbackPage,
rejectFeedback,
@@ -29,7 +29,14 @@
import { $t } from '#/locales';
import { useFormSchema } from '../../../pro/feedback/data';
-
+import {
+ loadParamDetails,
+ paramDetails,
+} from '../../../pro/feedback/param-state';
+import BatchInputDialog from '../../../pro/feedback/modules/batch-input-dialog.vue';
+const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>();
+const batchActionType = ref<'' | 'submit' | 'approve'>('');
+const pendingSubmitMode = ref<'saveAndSubmit' | 'submit'>('submit');
defineOptions({ name: 'WorkbenchFeedbackTab' });
const props = defineProps<{
@@ -70,26 +77,29 @@
wrapperClass: 'grid-cols-3',
});
-/** 鏍规嵁 checkFlag 瀵归綈鏁伴噺 */
+/** 鎻愪氦鍓嶅榻愭暟閲忥細鎶ュ伐鍙~鍐欐姤宸ユ暟閲忥紝鍚堟牸/涓嶈壇/搴熷搧鏁伴噺鐢辫川妫�纭畾锛屾姤宸ョ幆鑺備笉閲囬泦 */
function alignQuantity(data: any) {
- if (data.checkFlag) {
- data.uncheckQuantity = data.feedbackQuantity;
- data.qualifiedQuantity = 0;
- data.unqualifiedQuantity = 0;
- data.laborScrapQuantity = 0;
- data.materialScrapQuantity = 0;
- data.otherScrapQuantity = 0;
- } else {
- data.feedbackQuantity =
- (data.qualifiedQuantity || 0) + (data.unqualifiedQuantity || 0);
- data.uncheckQuantity = 0;
- }
+ data.uncheckQuantity = data.checkFlag ? data.feedbackQuantity : 0;
+ delete data.qualifiedQuantity;
+ delete data.unqualifiedQuantity;
+ delete data.laborScrapQuantity;
+ delete data.materialScrapQuantity;
+ delete data.otherScrapQuantity;
}
async function getAlignedData() {
const data = (await formApi.getValues()) as any;
+ syncParamValues(data);
alignQuantity(data);
return data;
+}
+
+/** 鍚屾宸ヨ壓鍙傛暟鍊煎埌鎶ュ伐鏁版嵁锛堜笌鎻愪氦鏁版嵁涓�鍚屽彂閫佺粰鍚庣锛� */
+function syncParamValues(data: any) {
+ data.paramValues = paramDetails.value.map((detail) => ({
+ detailId: detail.id,
+ value: detail.paramValue,
+ }));
}
/** 鍔犺浇 checkFlag */
@@ -114,6 +124,8 @@
const code = await generateAutoCode(MesAutoCodeRuleCode.PRO_FEEDBACK_CODE);
const checkFlag = await resolveCheckFlag(task.routeId, task.processId);
+ // 鍔犺浇浠诲姟缁戝畾鐨勫伐鑹哄弬鏁帮紙蹇呭~鍙傛暟鏍¢獙 + 鍙傛暟鍊煎洖鏄撅級
+ await loadParamDetails(task.id);
await formApi.setValues({
code,
@@ -130,10 +142,11 @@
itemName: task.itemName,
itemSpecification: task.itemSpecification,
unitMeasureName: task.unitMeasureName,
+ reportableQuantity: task.reportableQuantity,
scheduledQuantity: task.quantity,
+ feedbackApprove: task.feedbackApprove,
checkFlag,
});
-
originalSnapshot.value = JSON.stringify(await getAlignedData());
}
@@ -168,6 +181,7 @@
if (props.task) {
await resetForTask(props.task);
}
+
emit('reported');
}
@@ -203,8 +217,12 @@
return;
}
+ pendingSubmitMode.value = 'saveAndSubmit';
+
// 4. 璋冪敤鎻愪氦鎺ュ彛
- await submitFeedback(record.id!);
+ const isDone = await handleOnSubmit(feedbackId.value!,props.task?.feedbackApprove);
+ if (!isDone) return;
+
feedbackId.value = record.id;
feedbackStatus.value = record.status;
@@ -232,13 +250,60 @@
await updateFeedback(data);
originalSnapshot.value = JSON.stringify(data);
}
- await submitFeedback(feedbackId.value!);
- message.success('鎶ヨ鍗曞凡鎻愪氦');
+
+ pendingSubmitMode.value = 'submit';
+ const isDone = await handleOnSubmit(feedbackId.value!,props.task?.feedbackApprove);
+ if (!isDone) return;
+
+ message.success('鎶ュ伐鍗曞凡鎻愪氦');
// 閲嶇疆琛ㄥ崟涓烘柊寤烘ā寮�
if (props.task) {
await resetForTask(props.task);
}
emit('reported');
+}
+
+
+/** 鎻愪氦鏂规硶 */
+async function handleOnSubmit(id:any,feedbackRate:any): Promise<boolean> {
+ if (feedbackRate&&feedbackRate==true){
+ await submitFeedback({ id: feedbackId.value! });
+ return true;
+ }
+
+ const checkResult = await getBatchRequirements(id);
+ if (checkResult?.needBatchInput) {
+ batchActionType.value = 'submit';
+ batchDialogRef.value?.open({ checkResult });
+ return false;
+ } else {
+ await submitFeedback({ id: feedbackId.value! });
+ return true;
+ }
+}
+
+/** 鎵规纭鍥炶皟 */
+async function handleBatchConfirm(batchData: Record<string, unknown>) {
+ if (batchActionType.value === 'submit') {
+ await submitFeedback({ id: feedbackId.value!, ...batchData });
+
+ if (pendingSubmitMode.value === 'saveAndSubmit') {
+ const data = await getAlignedData();
+ const code = data.code as string;
+ Modal.success({
+ title: '淇濆瓨骞舵彁浜ゆ垚鍔�',
+ content: `鎶ュ伐鍗� ${code} 宸叉彁浜,
+ centered: true,
+ });
+ } else {
+ message.success('鎶ュ伐鍗曞凡鎻愪氦');
+ }
+
+ if (props.task) {
+ await resetForTask(props.task);
+ }
+ emit('reported');
+ }
}
/** 瀹℃壒閫氳繃 */
@@ -273,13 +338,15 @@
formType.value = 'update';
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
const checkFlag = await resolveCheckFlag(data.routeId, data.processId);
+ await loadParamDetails(data.taskId);
await formApi.setValues({ ...data, checkFlag });
originalSnapshot.value = JSON.stringify(await getAlignedData());
}
-// 鐩戝惉浠诲姟鍙樻洿锛堜粎褰撲换鍔� ID 鐪熸鍙樺寲鏃舵墠閲嶇疆琛ㄥ崟锛�
+// 鐩戝惉浠诲姟鍙樻洿锛氫换鍔″璞″彉鍖栵紙閫夋嫨浠诲姟 / 鍒楄〃鍒锋柊鍚庡悓姝ョ殑鏈�鏂版暟鎹級鏃堕噸缃〃鍗曪紝
+// 纭繚"鍙姤宸ユ暟閲�"绛変换鍔℃淳鐢熷瓧娈靛缁堝睍绀烘渶鏂板��
watch(
- () => props.task?.id,
+ () => props.task,
() => {
if (props.task) {
resetForTask(props.task);
@@ -294,6 +361,7 @@
<template>
<div class="feedback-tab">
+ <BatchInputDialog ref="batchDialogRef" @confirm="handleBatchConfirm" />
<div v-if="!task" class="feedback-tab__empty">
璇蜂粠宸︿晶宸ュ崟鍒楄〃閫夋嫨涓�鏉′换鍔�
</div>
@@ -308,6 +376,34 @@
</div>
<Form class="feedback-tab__form" />
+
+ <!-- 宸ヨ壓鍙傛暟锛堜换鍔$粦瀹氬弬鏁版椂灞曠ず锛屽繀濉弬鏁版湭濉細琚悗绔嫤鎴級 -->
+ <div v-if="paramDetails.length" class="feedback-tab__params">
+ <div class="feedback-tab__params-title">宸ヨ壓鍙傛暟</div>
+ <div class="feedback-tab__params-grid">
+ <div
+ v-for="detail in paramDetails"
+ :key="detail.id"
+ class="feedback-tab__params-item"
+ >
+ <span class="feedback-tab__params-label">
+ <span v-if="detail.required" class="feedback-tab__params-required"
+ >* </span
+ >
+ {{ detail.paramName || detail.paramCode
+ }}{{
+ detail.unitMeasureName ? `锛�${detail.unitMeasureName}锛塦 : ''
+ }}
+ </span>
+ <Input
+ v-model:value="detail.paramValue"
+ :disabled="!isEditable"
+ placeholder="璇疯緭鍏ュ弬鏁板��"
+ class="feedback-tab__params-input"
+ />
+ </div>
+ </div>
+ </div>
<div class="feedback-tab__actions">
<Button
@@ -400,4 +496,48 @@
border-top: 1px solid #f0f0f0;
margin-top: 16px;
}
+
+.feedback-tab__params {
+ margin-top: 16px;
+ padding: 14px 16px;
+ border: 1px solid #f0f0f0;
+ border-radius: 8px;
+ background: #fafbfc;
+}
+
+.feedback-tab__params-title {
+ margin-bottom: 12px;
+ font-size: 15px;
+ font-weight: 600;
+ color: #1a1a1a;
+}
+
+.feedback-tab__params-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px 20px;
+}
+
+.feedback-tab__params-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.feedback-tab__params-label {
+ flex-shrink: 0;
+ width: 176px;
+ text-align: right;
+ font-size: 14px;
+ color: #595959;
+}
+
+.feedback-tab__params-required {
+ color: #ff4d4f;
+}
+
+.feedback-tab__params-input {
+ flex: 1;
+ min-width: 0;
+}
</style>
diff --git a/src/views/mes/workbench/components/tabs/ProductIssueLineFormModal.vue b/src/views/mes/workbench/components/tabs/ProductIssueLineFormModal.vue
index 78f182f..89f0827 100644
--- a/src/views/mes/workbench/components/tabs/ProductIssueLineFormModal.vue
+++ b/src/views/mes/workbench/components/tabs/ProductIssueLineFormModal.vue
@@ -11,6 +11,7 @@
import { useVbenForm } from '#/adapter/form';
import {
createProductIssueLine,
+ getBomItemList,
getProductIssueLine,
updateProductIssueLine,
} from '#/api/mes/wm/productissue/line';
@@ -19,6 +20,37 @@
const emit = defineEmits(['success']);
const formData = ref<MesWmProductIssueLineApi.ProductIssueLine>();
const issueId = ref<number>();
+/** 褰撳墠宸ュ崟 BOM 鍙鐗╂枡 ID 鐧藉悕鍗曪紙涓虹┖鏃堕�夋嫨鍣ㄥ睍绀哄叏閲忕墿鏂欙級 */
+const bomItemIds = ref<number[]>([]);
+
+/**
+ * 鑾峰彇褰撳墠棰嗘枡鍗曞搴斿伐鍗� BOM 鐨勫彲棰嗙墿鏂� ID 鍒楄〃
+ * 浠呮柊澧�/缂栬緫鐗╂枡鏃舵煡璇㈠綋鍓嶈鍗曟墍闇�鐗╂枡锛屽叾浠栫墿鏂欎笉灞曠ず
+ */
+async function loadBomItemIds() {
+ if (!issueId.value) {
+ bomItemIds.value = [];
+ return;
+ }
+ try {
+ const bomItems = await getBomItemList(issueId.value);
+ bomItemIds.value = (bomItems ?? [])
+ .map((item) => item.itemId)
+ .filter((id): id is number => id != null);
+ } catch {
+ bomItemIds.value = [];
+ }
+}
+
+/** 鏇存柊鐗╂枡閫夋嫨鍣ㄧ殑鍙�夎寖鍥达紝浠呭睍绀哄綋鍓嶅伐鍗� BOM 鐗╂枡 */
+async function applyBomItemScope() {
+ await formApi.updateSchema([
+ {
+ fieldName: 'itemId',
+ componentProps: { productIds: bomItemIds.value },
+ },
+ ]);
+}
const getTitle = computed(() => {
return formData.value?.id ? '缂栬緫鐗╂枡' : '娣诲姞鐗╂枡';
@@ -87,17 +119,31 @@
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
+ bomItemIds.value = [];
+ // 鍏抽棴鏃舵仮澶嶉�夋嫨鍣ㄤ负涓嶉檺瀹氳寖鍥�
+ await applyBomItemScope();
return;
}
const data = modalApi.getData<{ id?: number; issueId: number }>();
issueId.value = data.issueId;
+ // 鍔犺浇褰撳墠宸ュ崟 BOM 鍙鐗╂枡鑼冨洿
+ await loadBomItemIds();
if (!data.id) {
await formApi.resetForm();
+ await applyBomItemScope();
return;
}
modalApi.lock();
try {
formData.value = await getProductIssueLine(data.id);
+ // 缂栬緫鍦烘櫙锛氳嫢褰撳墠鐗╂枡涓嶅湪 BOM 鑼冨洿鍐咃紙鍘嗗彶鏁版嵁锛夛紝骞跺叆鐧藉悕鍗曚繚璇佸彲鍥炴樉鍙��
+ if (
+ formData.value?.itemId != null &&
+ !bomItemIds.value.includes(formData.value.itemId)
+ ) {
+ bomItemIds.value = [...bomItemIds.value, formData.value.itemId];
+ }
+ await applyBomItemScope();
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
diff --git a/src/views/mes/workbench/components/tabs/TaskDetailTab.vue b/src/views/mes/workbench/components/tabs/TaskDetailTab.vue
index 68e1a68..fb2069d 100644
--- a/src/views/mes/workbench/components/tabs/TaskDetailTab.vue
+++ b/src/views/mes/workbench/components/tabs/TaskDetailTab.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
-import type { PendingTask } from '../../../types';
+import type { PendingTask } from '../../types';
-import { Descriptions, Progress, Tag } from 'ant-design-vue';
+import { Descriptions, Progress, Tag, Timeline, TimelineItem } from 'ant-design-vue';
defineOptions({ name: 'WorkbenchTaskDetailTab' });
@@ -91,6 +91,48 @@
</Descriptions.Item>
<Descriptions.Item label="澶囨敞" :span="2">{{ task.remark ?? '鈥�' }}</Descriptions.Item>
</Descriptions>
+
+ <!-- 鍋滃伐璁板綍 -->
+ <div v-if="task.pauseRecords && task.pauseRecords.length > 0" class="detail-pause-records">
+ <div class="detail-pause-records__title">鍋滃伐鍘嗗彶璁板綍</div>
+ <Timeline class="pause-timeline">
+ <TimelineItem
+ v-for="record in task.pauseRecords"
+ :key="record.id"
+ :color="record.resumeTime ? 'green' : 'red'"
+ >
+ <div class="pause-timeline-item">
+ <div class="pause-timeline-time">
+ <span class="time-label">{{ record.pauseTime }}</span>
+ <span class="time-separator">鑷�</span>
+ <span class="time-label" :class="{ 'is-ongoing': !record.resumeTime }">
+ {{ record.resumeTime || '姝e湪鍋滃伐' }}
+ </span>
+ </div>
+ <div class="pause-timeline-content">
+ <div class="pause-info-row">
+ <span class="info-label">鎿嶄綔浜哄憳锛�</span>
+ <span class="info-value">
+ {{ record.pauseUserName }} (鍋滃伐)
+ <template v-if="record.resumeTime">
+ <span style="margin: 0 4px; color: #d9d9d9">|</span>
+ {{ record.resumeUserName }} (澶嶅伐)
+ </template>
+ </span>
+ </div>
+ <div class="pause-info-row">
+ <span class="info-label">鍋滃伐鍘熷洜锛�</span>
+ <span class="info-value">{{ record.reason }}</span>
+ </div>
+ <div class="pause-info-row" v-if="record.remark">
+ <span class="info-label">澶囨敞锛�</span>
+ <span class="info-value">{{ record.remark }}</span>
+ </div>
+ </div>
+ </div>
+ </TimelineItem>
+ </Timeline>
+ </div>
</template>
</div>
</template>
@@ -187,4 +229,86 @@
font-size: 14px;
}
}
+
+.detail-pause-records {
+ margin-top: 24px;
+}
+
+.detail-pause-records__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #1a1a1a;
+ margin-bottom: 20px;
+}
+
+.pause-timeline {
+ padding-top: 6px;
+ padding-left: 4px;
+}
+
+.pause-timeline-item {
+ background: #fafafa;
+ border: 1px solid #f0f0f0;
+ border-radius: 8px;
+ padding: 14px 18px;
+ margin-top: -6px;
+ box-shadow: 0 1px 2px rgb(0 0 0 / 2%);
+ transition: all 0.3s ease;
+
+ &:hover {
+ box-shadow: 0 4px 12px rgb(0 0 0 / 5%);
+ border-color: #e6d8d8;
+ }
+}
+
+.pause-timeline-time {
+ font-size: 13px;
+ color: #595959;
+ font-weight: 500;
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.time-label {
+ background: #e6f7ff;
+ color: #1890ff;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-variant-numeric: tabular-nums;
+}
+
+.time-label.is-ongoing {
+ background: #fff1f0;
+ color: #f5222d;
+}
+
+.time-separator {
+ color: #bfbfbf;
+ font-size: 12px;
+}
+
+.pause-timeline-content {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.pause-info-row {
+ font-size: 13px;
+ display: flex;
+ line-height: 1.5;
+}
+
+.pause-info-row .info-label {
+ color: #8c8c8c;
+ width: 70px;
+ flex-shrink: 0;
+}
+
+.pause-info-row .info-value {
+ color: #262626;
+ flex: 1;
+}
</style>
diff --git a/src/views/mes/workbench/index.vue b/src/views/mes/workbench/index.vue
index 15c283a..a9d23c4 100644
--- a/src/views/mes/workbench/index.vue
+++ b/src/views/mes/workbench/index.vue
@@ -17,6 +17,7 @@
} from '#/api/mes/pro/workrecord';
import { getPendingFeedbackPage } from '#/api/mes/pro/task';
import { router } from '#/router';
+import { useAccess } from '@vben/access';
import TaskListPanel from './components/TaskListPanel.vue';
import WorkstationPanel from './components/WorkstationPanel.vue';
@@ -33,16 +34,36 @@
// --- Tab 閰嶇疆 ---
const tabItems: WorkbenchTabItem[] = [
{ key: 'detail', label: '宸ュ崟璇︽儏', icon: 'lucide:file-text' },
- { key: 'feedback', label: '鎶ュ伐', icon: 'lucide:edit-3' },
- { key: 'history', label: '鎶ュ伐璁板綍', icon: 'lucide:history' },
+ { key: 'feedback', label: '鎶ュ伐', icon: 'lucide:edit-3', auth: ['mes:pro-feedback:create'] },
+ { key: 'history', label: '鎶ュ伐璁板綍', icon: 'lucide:history' ,auth: ['mes:pro-feedback:query']},
{ key: 'record-param', label: '鐢熶骇璁板綍鍙傛暟', icon: 'lucide:file-cog' },
{ key: 'product-issue', label: '棰嗘枡', icon: 'lucide:clipboard-list' },
{ key: 'return-issue', label: '閫�鏂�', icon: 'lucide:rotate-ccw' },
];
+const { hasAccessByCodes } = useAccess();
+
+const availableTabs = computed(() => {
+ return tabItems.filter((tab) => {
+ if (!tab.auth || tab.auth.length === 0) return true;
+ return hasAccessByCodes(tab.auth);
+ });
+});
+
const activeTab = ref<WorkbenchTabKey>('detail');
const activeTabLabel = computed(
- () => tabItems.find((t) => t.key === activeTab.value)?.label ?? '',
+ () => availableTabs.value.find((t) => t.key === activeTab.value)?.label ?? '',
+);
+
+// 褰撳彲鐢� tab 鍙樺寲鏃讹紝濡傛灉褰撳墠閫変腑鐨� tab 涓嶅彲鐢紝鍒欒嚜鍔ㄩ�夋嫨绗竴涓彲鐢ㄧ殑 tab
+watch(
+ availableTabs,
+ (tabs) => {
+ if (tabs.length > 0 && !tabs.find((t) => t.key === activeTab.value)) {
+ activeTab.value = tabs[0]!.key;
+ }
+ },
+ { immediate: true },
);
// --- 宸ヤ綔绔� ---
@@ -176,7 +197,7 @@
<nav class="workbench-nav">
<button
- v-for="tab in tabItems"
+ v-for="tab in availableTabs"
:key="tab.key"
type="button"
class="workbench-nav__item"
@@ -215,6 +236,7 @@
:selected-id="selectedTask?.id"
:loading="taskLoading"
@select="handleSelectTask"
+ @refresh="loadTaskList"
/>
</aside>
@@ -245,18 +267,21 @@
<FeedbackHistoryTab
v-else-if="activeTab === 'history'"
:task-id="selectedTask?.id"
+ :task="selectedTask"
:refresh-key="reportRefreshKey"
@reported="onReported"
/>
<RecordParamTab
v-else-if="activeTab === 'record-param'"
:task-id="selectedTask?.id"
+ :task="selectedTask"
:work-order-id="selectedTask?.id"
:workstation-id="workstationId"
/>
<ProductIssueTab
v-else-if="activeTab === 'product-issue'"
:task-id="selectedTask?.id"
+ :task="selectedTask"
:task-code="selectedTask?.code"
:task-name="selectedTask?.name"
:work-order-id="selectedTask?.workOrderId"
@@ -266,6 +291,7 @@
<ReturnIssueTab
v-else-if="activeTab === 'return-issue'"
:task-id="selectedTask?.id"
+ :task="selectedTask"
:task-code="selectedTask?.code"
:task-name="selectedTask?.name"
:work-order-id="selectedTask?.workOrderId"
diff --git a/src/views/mes/workbench/types.ts b/src/views/mes/workbench/types.ts
index 8f74ad3..af0dd1c 100644
--- a/src/views/mes/workbench/types.ts
+++ b/src/views/mes/workbench/types.ts
@@ -8,6 +8,7 @@
key: WorkbenchTabKey;
label: string;
icon: string;
+ auth?: string[];
}
/** 寰呮姤宸ヤ换鍔★紙浠� API 绫诲瀷閲嶅鍑猴紝渚夸簬缁勪欢寮曠敤锛� */
diff --git a/src/views/srm/enums.ts b/src/views/srm/enums.ts
index 6077634..3c2cb0a 100644
--- a/src/views/srm/enums.ts
+++ b/src/views/srm/enums.ts
@@ -62,36 +62,35 @@
labels: { '1': '鐢熶骇鍨�', '2': '璐告槗鍨�', '3': '鏈嶅姟鍨�', '4': '鍏朵粬' },
};
-// ========== 鍑嗗叆鐢宠 鈥� 瀵归綈 SupplierApplyStatusEnum (0/10/20/30/40/50) ==========
+// ========== 鍑嗗叆鐢宠 鈥� 瀵归綈 SupplierApplyStatusEnum (0/10/30/40/50)
+// 鎺ュ叆鍗忓悓鍔炲叕鍚庣畝鍖栦负 5 鎬侊細寰呮彁浜� / 瀹℃壒涓� / 宸插彇娑� / 宸查�氳繃 / 宸查┏鍥�
+// 鍘熷緟閲囪喘瀹℃牳(10)/寰呰川閲忓鏍�(20)/寰呰储鍔″鏍�(30)宸插簾寮� ==========
export const APPLY_STATUS = {
DRAFT: 0,
- PENDING_PURCHASE: 10,
- PENDING_QUALITY: 20,
- PENDING_FINANCE: 30,
+ PROCESS: 10,
+ CANCEL: 30,
APPROVED: 40,
REJECTED: 50,
} as const;
export const APPLY_STATUS_OPTIONS: EnumOption[] = [
{ label: '寰呮彁浜�', value: 0 },
- { label: '寰呴噰璐鏍�', value: 10 },
- { label: '寰呰川閲忓鏍�', value: 20 },
- { label: '寰呰储鍔″鏍�', value: 30 },
+ { label: '瀹℃壒涓�', value: 10 },
+ { label: '宸插彇娑�', value: 30 },
{ label: '宸查�氳繃', value: 40 },
- { label: '宸叉嫆缁�', value: 50 },
+ { label: '宸查┏鍥�', value: 50 },
];
export const APPLY_STATUS_CELLTAG: { data: CellTagItem[]; labels: Record<number, string> } = {
data: [
{ value: 0, color: 'gray' },
{ value: 10, color: 'blue' },
- { value: 20, color: 'orange' },
{ value: 30, color: 'purple' },
{ value: 40, color: 'green' },
{ value: 50, color: 'red' },
],
- labels: { 0: '寰呮彁浜�', 10: '寰呴噰璐鏍�', 20: '寰呰川閲忓鏍�', 30: '寰呰储鍔″鏍�', 40: '宸查�氳繃', 50: '宸叉嫆缁�' },
+ labels: { 0: '寰呮彁浜�', 10: '瀹℃壒涓�', 30: '宸插彇娑�', 40: '宸查�氳繃', 50: '宸查┏鍥�' },
};
// ========== 璧勮川璇佷功 鈥� 瀵归綈 CertificateTypeEnum (1=钀ヤ笟鎵х収 2=璁よ瘉璇佷功 3=妫�娴嬫姤鍛� 4=鍏朵粬) ==========
diff --git a/src/views/srm/statistics/index.vue b/src/views/srm/statistics/index.vue
index 439ae78..5f9652a 100644
--- a/src/views/srm/statistics/index.vue
+++ b/src/views/srm/statistics/index.vue
@@ -96,7 +96,14 @@
</div>
<div class="rounded-lg bg-purple-50 p-6 text-center shadow-sm">
<div class="text-3xl font-bold text-purple-600">{{ stats.savedAmount?.toLocaleString() ?? 0 }}</div>
- <div class="mt-2 text-gray-500">鑺傜害閲戦(鍏�)</div>
+ <div class="mt-2 flex items-center justify-center gap-1 text-gray-500">
+ 鑺傜害閲戦(鍏�)
+ <a-tooltip title="鑺傜害閲戦 = 鎷涙爣鎬婚噾棰�(棰勭畻閲戦) 鈭� 瀹氭爣鎬婚噾棰�(涓爣閲戦)">
+ <span
+ class="inline-flex h-3.5 w-3.5 cursor-help items-center justify-center rounded-full bg-gray-400 text-[10px] leading-none text-white"
+ >?</span>
+ </a-tooltip>
+ </div>
</div>
<div class="rounded-lg bg-teal-50 p-6 text-center shadow-sm">
<div class="text-3xl font-bold text-teal-600">{{ stats.winRate ?? 0 }}%</div>
diff --git a/src/views/srm/supplier/data.ts b/src/views/srm/supplier/data.ts
index 58dc484..f323861 100644
--- a/src/views/srm/supplier/data.ts
+++ b/src/views/srm/supplier/data.ts
@@ -176,3 +176,27 @@
},
];
}
+
+/** 瀵煎叆寮圭獥鐨勮〃鍗� */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '渚涘簲鍟嗘。妗�',
+ component: 'Upload',
+ rules: 'required',
+ help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '鏄惁鏇存柊',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ help: '鍕鹃�夊悗鎸夆�滀緵搴斿晢缂栫爜鈥濆尮閰嶅凡瀛樺湪妗f骞惰鐩栨洿鏂帮紱涓嶅嬀閫夋椂閲嶅缂栫爜璁″叆澶辫触鏄庣粏',
+ },
+ ];
+}
diff --git a/src/views/srm/supplier/index.vue b/src/views/srm/supplier/index.vue
index ff73e1a..6fe70eb 100644
--- a/src/views/srm/supplier/index.vue
+++ b/src/views/srm/supplier/index.vue
@@ -3,18 +3,21 @@
import type { SrmSupplierApi } from '#/api/srm/supplier';
import { Page, useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteSupplier,
+ exportSupplierExcel,
getSupplierPage,
} from '#/api/srm/supplier';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import SupplierForm from './modules/form.vue';
+import SupplierImportForm from './modules/import-form.vue';
defineOptions({ name: 'SrmSupplier' });
@@ -28,6 +31,18 @@
function handleEdit(row: SrmSupplierApi.SupplierVO) {
formModalApi.setData(row).open();
+}
+
+/** 瀵煎叆渚涘簲鍟� */
+function handleImport() {
+ importModalApi.open();
+}
+
+/** 瀵煎嚭渚涘簲鍟嗭紙鎸夊綋鍓嶆悳绱㈡潯浠讹級 */
+async function handleExport() {
+ const formValues = await gridApi.formApi.getValues();
+ const data = await exportSupplierExcel(formValues);
+ downloadFileFromBlobPart({ fileName: '渚涘簲鍟嗘暟鎹�.xlsx', source: data });
}
async function handleDelete(row: SrmSupplierApi.SupplierVO) {
@@ -46,6 +61,11 @@
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: SupplierForm,
+ destroyOnClose: true,
+});
+
+const [SupplierImportModal, importModalApi] = useVbenModal({
+ connectedComponent: SupplierImportForm,
destroyOnClose: true,
});
@@ -77,6 +97,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <SupplierImportModal @success="handleRefresh" />
<Grid table-title="渚涘簲鍟嗗垪琛�">
<template #toolbar-tools>
<TableAction
@@ -88,6 +109,20 @@
auth: ['srm:supplier:create'],
onClick: handleCreate,
},
+ {
+ label: $t('ui.actionTitle.import'),
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['srm:supplier:import'],
+ onClick: handleImport,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['srm:supplier:export'],
+ onClick: handleExport,
+ },
]"
/>
</template>
diff --git a/src/views/srm/supplier/modules/import-form.vue b/src/views/srm/supplier/modules/import-form.vue
new file mode 100644
index 0000000..3ca2f54
--- /dev/null
+++ b/src/views/srm/supplier/modules/import-form.vue
@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Upload } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ getSupplierImportTemplate,
+ importSupplierExcel,
+} from '#/api/srm/supplier';
+
+import { useImportFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ formItemClass: 'col-span-2',
+ labelWidth: 110,
+ },
+ layout: 'horizontal',
+ schema: useImportFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ const data = await formApi.getValues();
+ modalApi.lock();
+ try {
+ const res = (await importSupplierExcel({
+ file: data.file,
+ updateSupport: data.updateSupport,
+ })) as any;
+ // 鍏煎鍝嶅簲缁撴瀯锛氫笟鍔℃暟鎹洿鎺ヨ繑鍥炴垨鍖呭湪 { code, data } 涓�
+ const result = res?.data ?? res ?? {};
+ const createList = result.createList ?? [];
+ const updateList = result.updateList ?? [];
+ const failureList = result.failureList ?? {};
+ const failureCount = Object.keys(failureList).length;
+ let tip = `瀵煎叆瀹屾垚锛氭柊澧� ${createList.length} 鏉°�佹洿鏂� ${updateList.length} 鏉°�佸け璐� ${failureCount} 鏉°�俙;
+ if (failureCount > 0) {
+ const sample = Object.entries(failureList)
+ .slice(0, 5)
+ .map(([name, reason]) => `${name}锛�${reason}`)
+ .join('锛�');
+ tip += `澶辫触鍘熷洜绀轰緥锛�${sample}`;
+ }
+ if (failureCount > 0) {
+ message.warning(tip);
+ } else {
+ message.success(tip);
+ }
+ // 鍏抽棴骞跺埛鏂�
+ await modalApi.close();
+ emit('success');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+/** 涓婁紶鍓� */
+function beforeUpload(file: FileType) {
+ formApi.setFieldValue('file', file);
+ return false;
+}
+
+/** 涓嬭浇妯℃澘 */
+async function handleDownload() {
+ const data = await getSupplierImportTemplate();
+ downloadFileFromBlobPart({ fileName: '渚涘簲鍟嗗鍏ユā鏉�.xlsx', source: data });
+}
+</script>
+
+<template>
+ <Modal title="渚涘簲鍟嗘。妗堝鍏�" class="w-1/3">
+ <Form class="mx-4">
+ <template #file>
+ <div class="w-full">
+ <Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload">
+ <Button type="primary"> 閫夋嫨 Excel 鏂囦欢 </Button>
+ </Upload>
+ </div>
+ </template>
+ </Form>
+ <template #prepend-footer>
+ <div class="flex flex-auto items-center">
+ <Button @click="handleDownload"> 涓嬭浇瀵煎叆妯℃澘 </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/srm/supplierApply/data.ts b/src/views/srm/supplierApply/data.ts
index 6b99c4c..1d681b0 100644
--- a/src/views/srm/supplierApply/data.ts
+++ b/src/views/srm/supplierApply/data.ts
@@ -10,6 +10,9 @@
import { APPLY_STATUS_CELLTAG, APPLY_STATUS_OPTIONS } from '../enums';
+/** 琛ㄥ崟绫诲瀷锛氭柊澧� / 缂栬緫 / 璇︽儏锛堝彧璇伙級 */
+export type FormType = 'create' | 'edit' | 'detail';
+
/** 鏂板/淇敼鐨勮〃鍗� */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
return [
diff --git a/src/views/srm/supplierApply/detail/index.vue b/src/views/srm/supplierApply/detail/index.vue
new file mode 100644
index 0000000..bdccd5c
--- /dev/null
+++ b/src/views/srm/supplierApply/detail/index.vue
@@ -0,0 +1,96 @@
+<script lang="ts" setup>
+import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h, onMounted, ref } from 'vue';
+
+import { ContentWrap } from '@vben/common-ui';
+import { formatDateTime } from '@vben/utils';
+
+import { Spin, Tag } from 'ant-design-vue';
+
+import { getApply } from '#/api/srm/supplierApply';
+import { useDescription } from '#/components/description';
+
+import { APPLY_STATUS_CELLTAG } from '../../enums';
+
+defineOptions({ name: 'SrmSupplierApplyDetail' });
+
+const props = defineProps<{ id: string }>();
+
+const loading = ref(false);
+const formData = ref<SrmSupplierApplyApi.ApplyVO>();
+
+/** 鏂囨湰瀛楁绌哄�煎崰浣� */
+function textRender(val: unknown) {
+ return val === 0 || val ? String(val) : '-';
+}
+
+/** 鐢宠鐘舵�佹爣绛炬覆鏌� */
+function statusRender(val: unknown) {
+ const num = Number(val);
+ const item = APPLY_STATUS_CELLTAG.data.find((i) => Number(i.value) === num);
+ const label = APPLY_STATUS_CELLTAG.labels[num] || '-';
+ return h(Tag, { color: item?.color }, { default: () => label });
+}
+
+const [Descriptions] = useDescription({
+ bordered: true,
+ column: 2,
+ schema: [
+ { label: '鐢宠缂栧彿', field: 'applyNo', render: textRender },
+ { label: '鐢宠鐘舵��', field: 'applyStatus', render: statusRender },
+ { label: '鐢宠浜�', field: 'applyUserName', render: textRender },
+ {
+ label: '鐢宠鏃堕棿',
+ field: 'applyTime',
+ render: (val) => formatDateTime(val) as string,
+ },
+ { label: '渚涘簲鍟嗗悕绉�', field: 'supplierName', render: textRender },
+ { label: '渚涘簲鍟嗙被鍨�', field: 'supplierType', render: textRender },
+ { label: '浼佷笟鎬ц川', field: 'companyNature', render: textRender },
+ {
+ label: '娉ㄥ唽璧勯噾(涓囧厓)',
+ field: 'registeredCapital',
+ render: textRender,
+ },
+ { label: '钀ヤ笟鎵х収缂栧彿', field: 'businessLicenseNo', render: textRender },
+ { label: '绋庡彿', field: 'taxNo', render: textRender },
+ { label: '寮�鎴烽摱琛�', field: 'bankName', render: textRender },
+ { label: '閾惰璐﹀彿', field: 'bankAccount', render: textRender },
+ { label: '鑱旂郴浜�', field: 'contactName', render: textRender },
+ { label: '鑱旂郴鐢佃瘽', field: 'contactMobile', render: textRender },
+ { label: '閭', field: 'contactEmail', render: textRender },
+ { label: '鍏徃鍦板潃', field: 'address', render: textRender },
+ {
+ label: '澶囨敞',
+ field: 'remark',
+ render: textRender,
+ span: 2,
+ },
+ ] as DescriptionItemSchema[],
+});
+
+onMounted(async () => {
+ try {
+ loading.value = true;
+ formData.value = await getApply(Number(props.id));
+ } finally {
+ loading.value = false;
+ }
+});
+</script>
+
+<template>
+ <ContentWrap class="m-2">
+ <Spin :spinning="loading" tip="鍔犺浇涓�...">
+ <div class="rounded-lg border border-border bg-background p-4">
+ <div class="mb-4 flex items-center gap-2">
+ <span class="inline-block h-4 w-1 rounded bg-primary" />
+ <span class="text-base font-semibold">渚涘簲鍟嗗噯鍏ョ敵璇�</span>
+ </div>
+ <Descriptions :data="formData" />
+ </div>
+ </Spin>
+ </ContentWrap>
+</template>
diff --git a/src/views/srm/supplierApply/index.vue b/src/views/srm/supplierApply/index.vue
index 47b8a8f..34663a8 100644
--- a/src/views/srm/supplierApply/index.vue
+++ b/src/views/srm/supplierApply/index.vue
@@ -2,118 +2,38 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
-import { computed, ref } from 'vue';
+import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
-import { useVbenForm } from '#/adapter/form';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteApply,
+ getApproveProcessList,
getApplyPage,
- submitApply,
- reviewPurchase,
- reviewQuality,
- reviewFinance,
} from '#/api/srm/supplierApply';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import ApplyForm from './modules/form.vue';
+import ProcessSelectModal from './modules/process-select-modal.vue';
import { APPLY_STATUS } from '../enums';
defineOptions({ name: 'SrmSupplierApply' });
-// ---------- reviewModal ----------
-const reviewFormData = ref({
- applyId: 0,
- reviewType: '' as 'purchase' | 'quality' | 'finance',
- passed: true,
- comment: '',
-});
-const reviewTitle = computed(() => {
- const labels: Record<string, string> = {
- purchase: '閲囪喘瀹℃牳',
- quality: '璐ㄩ噺瀹℃牳',
- finance: '璐㈠姟瀹℃牳',
- };
- return labels[reviewFormData.value.reviewType] || '瀹℃牳';
+const router = useRouter();
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: ApplyForm,
+ destroyOnClose: true,
});
-const [ReviewForm, reviewFormApi] = useVbenForm({
- commonConfig: {
- componentProps: { class: 'w-full' },
- labelWidth: 100,
- },
- wrapperClass: 'grid-cols-1',
- layout: 'horizontal',
- schema: [
- {
- fieldName: 'passed',
- label: '瀹℃牳缁撴灉',
- component: 'RadioGroup',
- rules: 'required',
- componentProps: {
- options: [
- { label: '閫氳繃', value: true },
- { label: '鎷掔粷', value: false },
- ],
- buttonStyle: 'solid',
- optionType: 'button',
- },
- },
- {
- fieldName: 'comment',
- label: '瀹℃牳鎰忚',
- component: 'Textarea',
- componentProps: { placeholder: '璇疯緭鍏ュ鏍告剰瑙�', rows: 3 },
- },
- ],
- showDefaultActions: false,
+const [ProcessModal, processModalApi] = useVbenModal({
+ connectedComponent: ProcessSelectModal,
+ destroyOnClose: true,
});
-
-const [ReviewModal, reviewModalApi] = useVbenModal({
- async onConfirm() {
- const { valid } = await reviewFormApi.validate();
- if (!valid) {
- return;
- }
- reviewModalApi.lock();
- const data = (await reviewFormApi.getValues()) as { passed: boolean; comment?: string };
- try {
- const payload: SrmSupplierApplyApi.ReviewVO = {
- id: reviewFormData.value.applyId,
- passed: data.passed,
- comment: data.comment,
- };
- if (reviewFormData.value.reviewType === 'purchase') {
- await reviewPurchase(payload);
- } else if (reviewFormData.value.reviewType === 'quality') {
- await reviewQuality(payload);
- } else if (reviewFormData.value.reviewType === 'finance') {
- await reviewFinance(payload);
- }
- await reviewModalApi.close();
- handleRefresh();
- message.success($t('ui.actionMessage.operationSuccess'));
- } finally {
- reviewModalApi.unlock();
- }
- },
-});
-
-function openReviewModal(row: SrmSupplierApplyApi.ApplyVO, reviewType: 'purchase' | 'quality' | 'finance') {
- reviewFormData.value = {
- applyId: row.id!,
- reviewType,
- passed: true,
- comment: '',
- };
- reviewFormApi.resetForm();
- reviewModalApi.open();
-}
// ---------- grid ----------
function handleRefresh() {
@@ -121,11 +41,16 @@
}
function handleCreate() {
- formModalApi.setData(null).open();
+ formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: SrmSupplierApplyApi.ApplyVO) {
- formModalApi.setData(row).open();
+ formModalApi.setData({ formType: 'edit', id: row.id }).open();
+}
+
+/** 鏌ョ湅璇︽儏锛堝彧璇伙級 */
+function handleDetail(row: SrmSupplierApplyApi.ApplyVO) {
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
}
async function handleDelete(row: SrmSupplierApplyApi.ApplyVO) {
@@ -142,21 +67,38 @@
}
}
+/** 鎻愪氦瀹℃壒 - 鍏堣幏鍙栧崗鍚屽姙鍏鎵规祦绋嬪垪琛� */
async function handleSubmit(row: SrmSupplierApplyApi.ApplyVO) {
- const hideLoading = message.loading({ content: '鎻愪氦涓�...', duration: 0 });
+ const hideLoading = message.loading({
+ content: '鑾峰彇瀹℃壒娴佺▼...',
+ duration: 0,
+ });
try {
- await submitApply(row.id!);
- message.success($t('ui.actionMessage.operationSuccess'));
- handleRefresh();
- } finally {
+ const processList = await getApproveProcessList();
+ hideLoading();
+ if (!processList || processList.length === 0) {
+ message.warning('鏆傛棤鍙敤瀹℃壒娴佺▼锛岃鍏堝湪鍗忓悓鍔炲叕閰嶇疆瀹℃壒娴佺▼');
+ return;
+ }
+ // 鎵撳紑娴佺▼閫夋嫨寮圭獥
+ processModalApi.setData({ id: row.id, processList }).open();
+ } catch {
hideLoading();
}
}
-const [FormModal, formModalApi] = useVbenModal({
- connectedComponent: ApplyForm,
- destroyOnClose: true,
-});
+/** 鏌ョ湅瀹℃壒杩涘害锛堣烦杞崗鍚屽姙鍏祦绋嬪疄渚嬭鎯咃紝鍙鏌ョ湅锛� */
+function handleViewProcess(row: SrmSupplierApplyApi.ApplyVO) {
+ if (!row.processInstanceId) {
+ message.warning('璇ョ敵璇峰皻鏈彁浜ゅ鎵�');
+ return;
+ }
+ // viewOnly=1锛氶殣钘忓鎵规搷浣滄寜閽紝浠呮煡鐪嬪鎵硅繘搴�
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: { id: row.processInstanceId, viewOnly: '1' },
+ });
+}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
@@ -186,9 +128,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
- <ReviewModal :title="reviewTitle" class="w-1/3">
- <ReviewForm class="mx-4" />
- </ReviewModal>
+ <ProcessModal @success="handleRefresh" />
<Grid table-title="鍑嗗叆鐢宠鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -207,43 +147,34 @@
<TableAction
:actions="[
{
- label: $t('common.edit'),
+ label: $t('common.detail'),
type: 'link',
- icon: ACTION_ICON.EDIT,
- auth: ['srm:supplier-apply:update'],
- onClick: handleEdit.bind(null, row),
+ icon: ACTION_ICON.VIEW,
+ auth: ['srm:supplier-apply:query'],
+ onClick: handleDetail.bind(null, row),
},
{
label: '鎻愪氦',
type: 'link',
- icon: ACTION_ICON.CHECK,
+ icon: ACTION_ICON.AUDIT,
auth: ['srm:supplier-apply:update'],
- ifShow: row.applyStatus === APPLY_STATUS.DRAFT,
+ ifShow: () => row.applyStatus === APPLY_STATUS.DRAFT || row.applyStatus === APPLY_STATUS.REJECTED,
onClick: handleSubmit.bind(null, row),
},
{
- label: '閲囪喘瀹℃牳',
+ label: '鏌ョ湅瀹℃壒',
type: 'link',
- icon: ACTION_ICON.CHECK,
- auth: ['srm:supplier-apply:update'],
- ifShow: row.applyStatus === APPLY_STATUS.PENDING_PURCHASE,
- onClick: openReviewModal.bind(null, row, 'purchase'),
+ icon: ACTION_ICON.VIEW,
+ ifShow: () => !!row.processInstanceId,
+ onClick: handleViewProcess.bind(null, row),
},
{
- label: '璐ㄩ噺瀹℃牳',
+ label: $t('common.edit'),
type: 'link',
- icon: ACTION_ICON.CHECK,
+ icon: ACTION_ICON.EDIT,
auth: ['srm:supplier-apply:update'],
- ifShow: row.applyStatus === APPLY_STATUS.PENDING_QUALITY,
- onClick: openReviewModal.bind(null, row, 'quality'),
- },
- {
- label: '璐㈠姟瀹℃牳',
- type: 'link',
- icon: ACTION_ICON.CHECK,
- auth: ['srm:supplier-apply:update'],
- ifShow: row.applyStatus === APPLY_STATUS.PENDING_FINANCE,
- onClick: openReviewModal.bind(null, row, 'finance'),
+ ifShow: () => row.applyStatus === APPLY_STATUS.DRAFT || row.applyStatus === APPLY_STATUS.REJECTED,
+ onClick: handleEdit.bind(null, row),
},
{
label: $t('common.delete'),
@@ -251,6 +182,7 @@
danger: true,
icon: ACTION_ICON.DELETE,
auth: ['srm:supplier-apply:delete'],
+ ifShow: () => row.applyStatus === APPLY_STATUS.DRAFT || row.applyStatus === APPLY_STATUS.REJECTED,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.supplierName]),
confirm: handleDelete.bind(null, row),
diff --git a/src/views/srm/supplierApply/modules/form.vue b/src/views/srm/supplierApply/modules/form.vue
index 22305a4..cf382d4 100644
--- a/src/views/srm/supplierApply/modules/form.vue
+++ b/src/views/srm/supplierApply/modules/form.vue
@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
+import type { FormType } from '../data';
import { computed, ref } from 'vue';
@@ -19,10 +20,16 @@
const emit = defineEmits(['success']);
const formData = ref<SrmSupplierApplyApi.ApplyVO>();
+const formType = ref<FormType>('create');
+
const getTitle = computed(() => {
- return formData.value?.id
- ? $t('ui.actionTitle.edit', ['鍑嗗叆鐢宠'])
- : $t('ui.actionTitle.create', ['鍑嗗叆鐢宠']);
+ if (formType.value === 'create') {
+ return $t('ui.actionTitle.create', ['鍑嗗叆鐢宠']);
+ } else if (formType.value === 'edit') {
+ return $t('ui.actionTitle.edit', ['鍑嗗叆鐢宠']);
+ } else {
+ return '鍑嗗叆鐢宠璇︽儏';
+ }
});
const [Form, formApi] = useVbenForm({
@@ -60,7 +67,10 @@
formData.value = undefined;
return;
}
- const data = modalApi.getData<SrmSupplierApplyApi.ApplyVO>();
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data?.formType ?? 'create';
+ formApi.setDisabled(formType.value === 'detail');
+ formApi.updateSchema(useFormSchema(formApi));
if (!data || !data.id) {
return;
}
@@ -78,7 +88,11 @@
</script>
<template>
- <Modal :title="getTitle" class="w-1/2">
+ <Modal
+ :title="getTitle"
+ class="w-1/2"
+ :show-confirm-button="formType !== 'detail'"
+ >
<Form class="mx-4" />
</Modal>
</template>
diff --git a/src/views/srm/supplierApply/modules/process-select-modal.vue b/src/views/srm/supplierApply/modules/process-select-modal.vue
new file mode 100644
index 0000000..e85cf5f
--- /dev/null
+++ b/src/views/srm/supplierApply/modules/process-select-modal.vue
@@ -0,0 +1,74 @@
+<script lang="ts" setup>
+import type { SrmSupplierApplyApi } from '#/api/srm/supplierApply';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Select } from 'ant-design-vue';
+
+import { submitApply } from '#/api/srm/supplierApply';
+
+const emit = defineEmits(['success']);
+
+const applyId = ref<number>();
+const processList = ref<SrmSupplierApplyApi.ProcessDefinition[]>([]);
+const selectedProcessKey = ref<string>('');
+
+const processOptions = computed(() => {
+ return processList.value.map((p) => ({
+ label: p.name,
+ value: p.key,
+ }));
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!selectedProcessKey.value) {
+ message.warning('璇烽�夋嫨瀹℃壒娴佺▼');
+ return;
+ }
+ modalApi.lock();
+ try {
+ await submitApply(applyId.value!, selectedProcessKey.value);
+ await modalApi.close();
+ emit('success');
+ message.success('鎻愪氦鎴愬姛');
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ applyId.value = undefined;
+ selectedProcessKey.value = '';
+ processList.value = [];
+ return;
+ }
+ const data = modalApi.getData<{
+ id: number;
+ processList: SrmSupplierApplyApi.ProcessDefinition[];
+ }>();
+ applyId.value = data?.id;
+ processList.value = data?.processList || [];
+ // 榛樿閫夋嫨绗竴涓祦绋�
+ if (processList.value.length > 0) {
+ selectedProcessKey.value = processList.value[0]!.key;
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal title="閫夋嫨瀹℃壒娴佺▼" class="w-1/3">
+ <div class="mb-4">
+ <span class="mr-2">瀹℃壒娴佺▼锛�</span>
+ <Select
+ v-model:value="selectedProcessKey"
+ :options="processOptions"
+ style="width: 220px"
+ placeholder="璇烽�夋嫨瀹℃壒娴佺▼"
+ />
+ </div>
+ </Modal>
+</template>
diff --git a/src/views/srm/supplierCertificate/data.ts b/src/views/srm/supplierCertificate/data.ts
index 1472c53..030a9df 100644
--- a/src/views/srm/supplierCertificate/data.ts
+++ b/src/views/srm/supplierCertificate/data.ts
@@ -83,6 +83,19 @@
rules: 'required',
},
{
+ fieldName: 'blobIds',
+ label: '璇佷功闄勪欢',
+ component: 'FileUpload',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ accept: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
+ helpText: '鏀寔jpg銆乯peg銆乸ng銆乸df銆乨oc銆乨ocx鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃50MB',
+ },
+ formItemClass: 'md:col-span-2',
+ },
+ {
fieldName: 'remark',
label: '澶囨敞',
component: 'Textarea',
@@ -144,6 +157,13 @@
{ field: 'validEndDate', title: '鍒版湡鏃ユ湡', minWidth: 120 },
{ field: 'createTime', title: '鍒涘缓鏃堕棿', minWidth: 160 },
{
+ field: 'attachmentList',
+ title: '闄勪欢',
+ minWidth: 100,
+ fixed: 'right',
+ slots: { default: 'attachment' },
+ },
+ {
title: '鎿嶄綔',
width: 130,
fixed: 'right',
diff --git a/src/views/srm/supplierCertificate/index.vue b/src/views/srm/supplierCertificate/index.vue
index 29ea03f..a26aa75 100644
--- a/src/views/srm/supplierCertificate/index.vue
+++ b/src/views/srm/supplierCertificate/index.vue
@@ -4,7 +4,7 @@
import { Page, useVbenModal } from '@vben/common-ui';
-import { message } from 'ant-design-vue';
+import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -112,6 +112,21 @@
]"
/>
</template>
+ <template #attachment="{ row }">
+ <div v-if="row.attachmentList && row.attachmentList.length" class="flex flex-col gap-1">
+ <a
+ v-for="file in row.attachmentList"
+ :key="file.id"
+ :href="file.url"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="text-blue-500 hover:underline"
+ >
+ {{ file.name || '闄勪欢-' + file.id }}
+ </a>
+ </div>
+ <Tag v-else color="default">--</Tag>
+ </template>
<template #actions="{ row }">
<TableAction
:actions="[
diff --git a/src/views/srm/supplierCertificate/modules/form.vue b/src/views/srm/supplierCertificate/modules/form.vue
index d0bc05b..041e5e0 100644
--- a/src/views/srm/supplierCertificate/modules/form.vue
+++ b/src/views/srm/supplierCertificate/modules/form.vue
@@ -65,6 +65,17 @@
modalApi.lock();
try {
formData.value = await getCertificate(data.id);
+ if (formData.value?.attachmentList?.length) {
+ // 杞负涓婁紶缁勪欢鐨勬枃浠跺璞★紱鎻愪氦鏃� formApi 浼氭寜 valueKey 鎻愬彇闄勪欢 ID
+ (formData.value as Record<string, unknown>).blobIds =
+ formData.value.attachmentList.map((item) => ({
+ uid: String(item.id),
+ name: item.name || '',
+ url: item.url || '',
+ status: 'done',
+ id: item.id,
+ }));
+ }
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
diff --git a/src/views/srm/tender/data.ts b/src/views/srm/tender/data.ts
index a3b1005..451aab9 100644
--- a/src/views/srm/tender/data.ts
+++ b/src/views/srm/tender/data.ts
@@ -180,6 +180,19 @@
componentProps: { placeholder: '璇疯緭鍏ヨ祫璐ㄨ姹�', rows: 3 },
formItemClass: 'col-span-2',
},
+ {
+ fieldName: 'blobIds',
+ label: '璧勮川瑕佹眰闄勪欢',
+ component: 'FileUpload',
+ componentProps: {
+ valueKey: 'id',
+ maxNumber: 10,
+ multiple: true,
+ accept: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
+ helpText: '鏀寔jpg銆乯peg銆乸ng銆乸df銆乨oc銆乨ocx鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃50MB',
+ },
+ formItemClass: 'col-span-2',
+ },
];
}
diff --git a/src/views/srm/tender/detail.vue b/src/views/srm/tender/detail.vue
index 7b2f1de..fda47ec 100644
--- a/src/views/srm/tender/detail.vue
+++ b/src/views/srm/tender/detail.vue
@@ -6,20 +6,20 @@
import { Page } from '@vben/common-ui';
-import { message, Modal } from 'ant-design-vue';
+import { message } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import {
getTenderProject,
getMaterialList, createMaterial, updateMaterial, deleteMaterial,
- getBidListByProject, createBid, withdrawBid,
+ getBidListByProject, createBid, withdrawBid, updateBidTotal,
getQuoteList, createQuote, updateQuote,
createBidOpen, getBidOpen,
getEvaluationList, createBidEvaluation, updateBidEvaluation, calculateRanking,
createAward, deleteAward, getAward, getAwardByProject, approveAward, generatePurchaseOrder,
confirmTenderProject,
} from '#/api/srm/tender';
-import { getSupplierSimpleList } from '#/api/srm/supplier';
+import { getSupplierList } from '#/api/srm/supplier';
import { getItemSimpleList } from '#/api/mdm/item';
import type { MdmItemApi } from '#/api/mdm/item';
import {
@@ -51,7 +51,7 @@
onMounted(async () => {
const [items, suppliers] = await Promise.all([
getItemSimpleList(),
- getSupplierSimpleList(),
+ getSupplierList(),
]);
mdmItemList.value = items;
supplierList.value = suppliers;
@@ -150,6 +150,8 @@
const quoteFormVisible = ref(false);
const quoteForm = ref<SrmTenderApi.QuoteVO>({ bidId: undefined!, tenderMaterialId: undefined! });
const selectedBidId = ref<number>();
+/** 鎶曟爣鎬婚噾棰濊崏绋匡細杩涘叆鎶ヤ环椤垫椂甯﹀嚭宸蹭繚瀛樻�讳环锛屽彲鑷姩甯﹀嚭鎶ヤ环鍚堣鍚庝汉宸ュ井璋� */
+const bidTotalDraft = ref<number>();
const availableMaterials = computed(() => {
const quotedIds = new Set(
@@ -169,29 +171,11 @@
selectedBidId.value = bidId;
quoteLoading.value = true;
try { quoteList.value = await getQuoteList(bidId); } finally { quoteLoading.value = false; }
+ // 甯﹀嚭璇ユ姇鏍囧凡淇濆瓨鐨勬姇鏍囨�婚噾棰濓紙涓虹┖鍒欒鐢ㄦ埛褰曞叆/鑷姩甯﹀嚭锛�
+ const bid = bidList.value.find((b) => b.id === bidId);
+ bidTotalDraft.value = bid?.bidTotalAmount ?? undefined;
}
async function handleSaveQuote() {
- // 鏍¢獙锛氭姤浠烽噾棰濅笌鎶曟爣閲戦涓�鑷存��
- const bid = bidList.value.find((b) => b.id === quoteForm.value.bidId);
- if (bid?.bidTotalAmount != null) {
- const otherQuotesTotal = quoteList.value
- .filter((q) => q.id !== quoteForm.value.id)
- .reduce((sum, q) => sum + (q.quotePrice ?? 0), 0);
- const newTotal = otherQuotesTotal + (quoteForm.value.quotePrice ?? 0);
- if (newTotal !== bid.bidTotalAmount) {
- await new Promise<void>((resolve, reject) => {
- Modal.confirm({
- title: '閲戦涓嶄竴鑷存彁绀�',
- content: `鎶ヤ环鎬婚噾棰濓紙${newTotal.toLocaleString()}锛変笌鎶曟爣閲戦锛�${bid.bidTotalAmount!.toLocaleString()}锛変笉涓�鑷达紝鏄惁缁х画鎻愪氦锛焋,
- okText: '缁х画鎻愪氦',
- cancelText: '鍙栨秷',
- onOk: () => { resolve(); },
- onCancel: () => { reject(new Error('cancel')); },
- });
- });
- }
- }
-
if (quoteForm.value.id) {
await updateQuote(quoteForm.value);
} else {
@@ -208,6 +192,62 @@
quoteForm.value = { ...row };
quoteFormVisible.value = true;
}
+
+/** 鎷涙爣鐗╂枡 Map锛氭姤浠疯鎸� tenderMaterialId 鍙嶆煡鐗╂枡鏁伴噺 */
+const materialMap = computed(() => {
+ const map = new Map<number, SrmTenderApi.TenderMaterialVO>();
+ materialList.value.forEach((m) => {
+ if (m.id != null) map.set(m.id, m);
+ });
+ return map;
+});
+
+/** 鎶ヤ环鏄庣粏瀵瑰簲鐨勬嫑鏍囩墿鏂欐暟閲� */
+function materialQuantityOf(quote: SrmTenderApi.QuoteVO): number {
+ return materialMap.value.get(quote.tenderMaterialId!)?.quantity ?? 0;
+}
+
+/** 鎶ヤ环琛岄噾棰� = 鎶ヤ环鍗曚环 脳 鐗╂枡鏁伴噺 */
+function quoteAmountOf(quote: SrmTenderApi.QuoteVO): number {
+ return (quote.quotePrice ?? 0) * materialQuantityOf(quote);
+}
+
+/** 閫変腑鎶曟爣涓嬪凡褰曟姤浠风殑鍚堣閲戦 */
+const selectedQuoteTotalAmount = computed(() =>
+ quoteList.value.reduce((sum, q) => sum + quoteAmountOf(q), 0),
+);
+
+/** 缁存姢锛堟暣鍗曪級鎶曟爣鎬婚噾棰濓細鍙嚜鍔ㄥ甫鍑烘姤浠峰悎璁″悗浜哄伐寰皟锛堢敤浜庢暣鍗曡鍒╃瓑锛� */
+async function handleSaveBidTotal() {
+ if (!selectedBidId.value) return;
+ if (bidTotalDraft.value == null) {
+ message.warning('璇峰~鍐欐姇鏍囨�婚噾棰�');
+ return;
+ }
+ await updateBidTotal(selectedBidId.value, bidTotalDraft.value);
+ message.success('鎶曟爣鎬婚噾棰濆凡淇濆瓨');
+ loadBids();
+}
+
+/** 宸蹭繚瀛樼殑鎶曟爣鎬婚噾棰� 涓� 鎶ヤ环鍚堣 鐨勫樊寮傛彁绀猴紙鍙彁绀猴紝涓嶆嫤鎴級 */
+const bidTotalCompareHint = computed<{ text: string; tone: 'success' | 'warning' } | null>(() => {
+ const sum = selectedQuoteTotalAmount.value;
+ if (sum <= 0) {
+ return null; // 灏氭湭褰曟姤浠凤紝鏆備笉姣旇緝
+ }
+ const stored = selectedBid.value?.bidTotalAmount;
+ if (stored == null) {
+ return { text: '璇ユ姇鏍囧皻鏈淮鎶ゆ�婚噾棰濓細鍙偣銆岃嚜鍔ㄥ甫鍑烘姤浠峰悎璁°�嶅~鍏ュ悗淇濆瓨', tone: 'warning' };
+ }
+ const diff = Math.abs(stored - sum);
+ if (diff < 0.01) {
+ return { text: `鎶曟爣鎬婚噾棰� 楼${stored.toLocaleString()} 涓庢姤浠峰悎璁′竴鑷碻, tone: 'success' };
+ }
+ return {
+ text: `鎶曟爣鎬婚噾棰� 楼${stored.toLocaleString()} 涓庢姤浠峰悎璁� 楼${sum.toLocaleString()} 涓嶄竴鑷达紙宸 楼${diff.toLocaleString()}锛夈�傚闇�鏁村崟璁╁埄/璋冧环锛岀洿鎺ュ湪鎶曟爣鎬婚噾棰濆淇敼骞朵繚瀛樸�俙,
+ tone: 'warning',
+ };
+});
// ========== 寮�鏍� ==========
const bidOpen = ref<any>(null);
@@ -363,6 +403,7 @@
const typeLabel = computed(() => getLabel(TENDER_TYPE_CELLTAG.labels, project.value?.tenderType));
const isDraft = computed(() => project.value?.tenderStatus === TENDER_STATUS.DRAFT);
const isPublished = computed(() => project.value?.tenderStatus === TENDER_STATUS.PUBLISHED);
+const isBidding = computed(() => project.value?.tenderStatus === TENDER_STATUS.BIDDING);
const isPublishedOrBidding = computed(() => {
const s = project.value?.tenderStatus;
return s === TENDER_STATUS.PUBLISHED || s === TENDER_STATUS.BIDDING;
@@ -438,7 +479,7 @@
const bidColumns = [
{ title: '鎶曟爣缂栧彿', dataIndex: 'bidNo', width: 140 },
{ title: '渚涘簲鍟�', dataIndex: 'supplierName', width: 200 },
- { title: '鎶曟爣閲戦', dataIndex: 'bidTotalAmount', width: 120 },
+ { title: '鎶曟爣鎬婚噾棰�', dataIndex: 'bidTotalAmount', width: 120 },
{ title: '鎶曟爣鏃堕棿', dataIndex: 'bidTime', width: 160 },
{ title: '鐘舵��', dataIndex: 'bidStatus', width: 100, customRender: ({ text }: any) => BID_STATUS_MAP[text] || text },
{ title: '鎿嶄綔', key: 'actions', width: 240 },
@@ -446,7 +487,9 @@
const quoteColumns = [
{ title: '鐗╂枡', dataIndex: 'materialName', width: 120 },
- { title: '鎶ヤ环浠锋牸', dataIndex: 'quotePrice', width: 100 },
+ { title: '鎶ヤ环鍗曚环', dataIndex: 'quotePrice', width: 100 },
+ { title: '鏁伴噺', dataIndex: 'quantity', width: 70 },
+ { title: '鎶ヤ环閲戦', dataIndex: 'quoteAmount', width: 110 },
{ title: '绋庣巼(%)', dataIndex: 'taxRate', width: 80 },
{ title: '浜ゆ湡(澶�)', dataIndex: 'deliveryCycle', width: 80 },
{ title: '浠樻鏉′欢', dataIndex: 'paymentTerms', width: 120 },
@@ -537,14 +580,32 @@
</div>
<!-- 鎷涙爣璇存槑 / 璧勮川瑕佹眰 -->
- <template v-if="project.tenderDesc || project.qualificationRequirements">
+ <template
+ v-if="project.tenderDesc || project.qualificationRequirements || (project.attachmentList && project.attachmentList.length)"
+ >
<a-divider class="!my-2" />
<a-collapse :bordered="false" ghost expand-icon-position="end">
<a-collapse-panel v-if="project.tenderDesc" key="desc" header="鎷涙爣璇存槑">
<p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.tenderDesc }}</p>
</a-collapse-panel>
- <a-collapse-panel v-if="project.qualificationRequirements" key="req" header="璧勮川瑕佹眰">
- <p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.qualificationRequirements }}</p>
+ <a-collapse-panel
+ v-if="project.qualificationRequirements || (project.attachmentList && project.attachmentList.length)"
+ key="req"
+ header="璧勮川瑕佹眰"
+ >
+ <p v-if="project.qualificationRequirements" class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.qualificationRequirements }}</p>
+ <div v-if="project.attachmentList && project.attachmentList.length" class="mt-2 flex flex-col gap-1">
+ <a
+ v-for="file in project.attachmentList"
+ :key="file.id"
+ :href="file.url"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="text-blue-500 text-xs hover:underline"
+ >
+ {{ file.name || '闄勪欢-' + file.id }}
+ </a>
+ </div>
</a-collapse-panel>
</a-collapse>
</template>
@@ -594,7 +655,7 @@
</template>
<div class="flex items-center justify-between mb-3">
<span class="text-xs text-gray-400">鍏� <b class="text-gray-600">{{ bidCount }}</b> 涓姇鏍�</span>
- <a-button v-if="isPublishedOrBidding || isDraft || isPriceComparing" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
+ <a-button v-if="isBidding || isDraft || isPriceComparing" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
鏂板鎶曟爣
</a-button>
@@ -637,7 +698,29 @@
<span class="text-gray-400">渚涘簲鍟嗭細<b class="text-gray-700">{{ selectedBid.supplierName }}</b></span>
<a-button size="small" type="link" class="ml-auto" @click="activeTab = 'bid'">鍒囨崲鎶曟爣</a-button>
</div>
- <div class="text-right mb-3">
+ <!-- 鎶曟爣鎬婚噾棰濓紙鏁村崟鎬讳环锛夛細鍙笌鎶ヤ环鍚堣鑷姩甯﹀嚭骞朵汉宸ヨ皟鏁� -->
+ <div v-if="(isPublishedOrBidding || isDraft || isPriceComparing) && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" class="flex flex-wrap items-center gap-x-3 gap-y-2 mb-2 px-3 py-2 bg-blue-50/70 rounded text-xs">
+ <span class="text-gray-600 font-medium">鎶曟爣鎬婚噾棰�</span>
+ <a-input-number v-model:value="bidTotalDraft" :min="0" :precision="2" class="!w-40" placeholder="鏁村崟鎬讳环">
+ <template #addonAfter>鍏�</template>
+ </a-input-number>
+ <a-button size="small" type="link" :disabled="selectedQuoteTotalAmount <= 0" @click="bidTotalDraft = selectedQuoteTotalAmount">
+ 鑷姩甯﹀嚭鎶ヤ环鍚堣 楼{{ selectedQuoteTotalAmount.toLocaleString() }}
+ </a-button>
+ <a-button size="small" type="primary" @click="handleSaveBidTotal">淇濆瓨鎬讳环</a-button>
+ </div>
+ <a-alert
+ v-if="bidTotalCompareHint"
+ :type="bidTotalCompareHint.tone"
+ :show-icon="true"
+ class="mb-3"
+ :message="bidTotalCompareHint.text"
+ />
+ <div class="flex items-center justify-between mb-3">
+ <span class="text-xs text-gray-400">
+ 宸叉姤浠� <b class="text-gray-600">{{ quoteList.length }}</b> / <b class="text-gray-600">{{ materialList.length }}</b> 椤圭墿鏂欙紝鎶ヤ环鍚堣
+ <b class="text-blue-500">{{ selectedQuoteTotalAmount.toLocaleString() }}</b> 鍏�
+ </span>
<a-button v-if="(isPublishedOrBidding || isDraft || isPriceComparing) && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
鏂板鎶ヤ环
@@ -647,6 +730,12 @@
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'quotePrice'">
<span class="font-medium text-blue-600">{{ record.quotePrice?.toLocaleString() }}</span>
+ </template>
+ <template v-if="column.dataIndex === 'quantity'">
+ {{ materialQuantityOf(record) || '-' }}
+ </template>
+ <template v-if="column.dataIndex === 'quoteAmount'">
+ <span class="font-medium text-blue-600">{{ quoteAmountOf(record).toLocaleString() }}</span>
</template>
<template v-if="column.dataIndex === 'taxRate'">
{{ record.taxRate != null ? `${record.taxRate}%` : '-' }}
@@ -873,7 +962,9 @@
</a-select>
</a-form-item>
<a-form-item label="鎶曟爣鎬婚噾棰�">
- <a-input-number v-model:value="bidForm.bidTotalAmount" :min="0" class="!w-full" placeholder="鎶曟爣鎬婚噾棰�"><template #addonAfter>鍏�</template></a-input-number>
+ <div class="text-xs text-gray-400 leading-5">
+ 鍙殏涓嶅~锛氭姤浠锋槑缁嗗綍瀹屽悗锛屽湪銆屾姤浠风鐞嗐�嶉〉鎸夋姤浠峰悎璁¤嚜鍔ㄥ甫鍑恒�佸啀浜哄伐寰皟淇濆瓨銆�
+ </div>
</a-form-item>
</a-form>
</a-modal>
@@ -888,7 +979,7 @@
</a-form-item>
<a-row :gutter="12">
<a-col :span="8">
- <a-form-item label="鎶ヤ环浠锋牸" required><a-input-number v-model:value="quoteForm.quotePrice" :min="0" class="!w-full" placeholder="鎶ヤ环"><template #addonAfter>鍏�</template></a-input-number></a-form-item>
+ <a-form-item label="鎶ヤ环鍗曚环" required><a-input-number v-model:value="quoteForm.quotePrice" :min="0" class="!w-full" placeholder="鎶ヤ环"><template #addonAfter>鍏�</template></a-input-number></a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="绋庣巼(%)"><a-input-number v-model:value="quoteForm.taxRate" :min="0" :max="100" class="!w-full" placeholder="濡� 13" /></a-form-item>
@@ -998,4 +1089,4 @@
</a-modal>
</template>
</Page>
-</template>
+</template>
\ No newline at end of file
diff --git a/src/views/srm/tender/modules/form.vue b/src/views/srm/tender/modules/form.vue
index bcdc8d2..652d017 100644
--- a/src/views/srm/tender/modules/form.vue
+++ b/src/views/srm/tender/modules/form.vue
@@ -58,6 +58,7 @@
id: data.id!,
tenderDesc: data.tenderDesc,
qualificationRequirements: data.qualificationRequirements,
+ blobIds: data.blobIds,
});
} else {
await (formData.value?.id
@@ -92,6 +93,17 @@
modalApi.lock();
try {
formData.value = await getTenderProject(data.id);
+ if (formData.value?.attachmentList?.length) {
+ // 杞负涓婁紶缁勪欢鐨勬枃浠跺璞★紱鎻愪氦鏃� formApi 浼氭寜 valueKey 鎻愬彇闄勪欢 ID
+ (formData.value as Record<string, unknown>).blobIds =
+ formData.value.attachmentList.map((item) => ({
+ uid: String(item.id),
+ name: item.name || '',
+ url: item.url || '',
+ status: 'done',
+ id: item.id,
+ }));
+ }
const partial = formData.value.tenderStatus !== undefined
&& formData.value.tenderStatus !== TENDER_STATUS.DRAFT;
await formApi.setValues({ ...formData.value, _partial: partial || undefined });
diff --git a/src/views/system/dept/data.ts b/src/views/system/dept/data.ts
index d111330..9fe39e2 100644
--- a/src/views/system/dept/data.ts
+++ b/src/views/system/dept/data.ts
@@ -114,6 +114,30 @@
];
}
+/** 閮ㄩ棬瀵煎叆鐨勮〃鍗� */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '閮ㄩ棬鏁版嵁',
+ component: 'Upload',
+ rules: 'required',
+ help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '鏄惁瑕嗙洊',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勯儴闂ㄦ暟鎹�',
+ },
+ ];
+}
+
/** 鍒楄〃鐨勫瓧娈� */
export function useGridColumns(): VxeTableGridOptions<SystemDeptApi.Dept>['columns'] {
return [
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
index 64f998e..1646f7b 100644
--- a/src/views/system/dept/index.vue
+++ b/src/views/system/dept/index.vue
@@ -15,9 +15,15 @@
import { useGridColumns } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -36,6 +42,11 @@
/** 鍒涘缓閮ㄩ棬 */
function handleCreate() {
formModalApi.setData(null).open();
+}
+
+/** 瀵煎叆閮ㄩ棬 */
+function handleImport() {
+ importModalApi.open();
}
/** 娣诲姞涓嬬骇閮ㄩ棬 */
@@ -129,6 +140,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<Grid table-title="閮ㄩ棬鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -141,6 +153,13 @@
onClick: handleCreate,
},
{
+ label: $t('ui.actionTitle.import', ['閮ㄩ棬']),
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['system:dept:import'],
+ onClick: handleImport,
+ },
+ {
label: isExpanded ? '鏀剁缉' : '灞曞紑',
type: 'primary',
onClick: handleExpand,
diff --git a/src/views/system/dept/modules/import-form.vue b/src/views/system/dept/modules/import-form.vue
new file mode 100644
index 0000000..08eee2b
--- /dev/null
+++ b/src/views/system/dept/modules/import-form.vue
@@ -0,0 +1,104 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+
+import { h } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, notification, Upload } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { importDept, importDeptTemplate } from '#/api/system/dept';
+import { $t } from '#/locales';
+
+import { useImportFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ formItemClass: 'col-span-2',
+ labelWidth: 120,
+ },
+ layout: 'horizontal',
+ schema: useImportFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = await formApi.getValues();
+ try {
+ const response = (await importDept(data.file, data.updateSupport)) as any;
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+
+ const resultData = response?.data || response || {};
+ const failureNames = resultData.failureDeptNames || {};
+
+ if (Object.keys(failureNames).length > 0) {
+ const errorNodes = Object.entries(failureNames).map(([name, reason]) => {
+ return h('div', `${name}: ${reason}`);
+ });
+
+ notification.warning({
+ message: '瀵煎叆瀹屾垚锛岄儴鍒嗛儴闂ㄥ鍏ュけ璐�',
+ description: () => h('div', errorNodes),
+ duration: null,
+ });
+ } else {
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+/** 涓婁紶鍓� */
+function beforeUpload(file: FileType) {
+ formApi.setFieldValue('file', file);
+ return false;
+}
+
+/** 涓嬭浇妯$増 */
+async function handleDownload() {
+ const data = await importDeptTemplate();
+ downloadFileFromBlobPart({ fileName: '閮ㄩ棬瀵煎叆妯℃澘.xls', source: data });
+}
+</script>
+
+<template>
+ <Modal title="瀵煎叆閮ㄩ棬" class="w-1/3">
+ <Form class="mx-4">
+ <template #file>
+ <div class="w-full">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :before-upload="beforeUpload"
+ >
+ <Button type="primary"> 閫夋嫨 Excel 鏂囦欢 </Button>
+ </Upload>
+ </div>
+ </template>
+ </Form>
+ <div class="mx-4 mt-2 mb-4 text-gray-500 text-xs">
+ 鎻愮ず锛氭ā鏉块噷鐨勨�滀笂绾ч儴闂ㄢ�濊濉啓鍏ㄨ矾寰勶紙濡�:鎬诲叕鍙�/鍖椾含鍒嗗叕鍙革級
+ <div>涓婄骇閮ㄩ棬绌虹潃鍗充负椤剁骇閮ㄩ棬銆�</div>
+ </div>
+ <template #prepend-footer>
+ <div class="flex flex-auto items-center">
+ <Button @click="handleDownload"> 涓嬭浇瀵煎叆妯℃澘 </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/system/post/data.ts b/src/views/system/post/data.ts
index 3c66ebc..ca71395 100644
--- a/src/views/system/post/data.ts
+++ b/src/views/system/post/data.ts
@@ -68,6 +68,30 @@
];
}
+/** 宀椾綅瀵煎叆鐨勮〃鍗� */
+export function useImportFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'file',
+ label: '宀椾綅鏁版嵁',
+ component: 'Upload',
+ rules: 'required',
+ help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
+ },
+ {
+ fieldName: 'updateSupport',
+ label: '鏄惁瑕嗙洊',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勫矖浣嶆暟鎹�',
+ },
+ ];
+}
+
/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
export function useGridFormSchema(): VbenFormSchema[] {
return [
diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue
index c59418e..a8039dc 100644
--- a/src/views/system/post/index.vue
+++ b/src/views/system/post/index.vue
@@ -20,9 +20,15 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import ImportForm from './modules/import-form.vue';
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: ImportForm,
destroyOnClose: true,
});
@@ -40,6 +46,11 @@
/** 鍒涘缓宀椾綅 */
function handleCreate() {
formModalApi.setData(null).open();
+}
+
+/** 瀵煎叆宀椾綅 */
+function handleImport() {
+ importModalApi.open();
}
/** 缂栬緫宀椾綅 */
@@ -126,6 +137,7 @@
<template>
<Page auto-content-height>
<FormModal @success="handleRefresh" />
+ <ImportModal @success="handleRefresh" />
<Grid table-title="宀椾綅鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -145,6 +157,13 @@
onClick: handleExport,
},
{
+ label: $t('ui.actionTitle.import'),
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['system:post:import'],
+ onClick: handleImport,
+ },
+ {
label: $t('ui.actionTitle.deleteBatch'),
type: 'primary',
danger: true,
diff --git a/src/views/system/post/modules/import-form.vue b/src/views/system/post/modules/import-form.vue
new file mode 100644
index 0000000..ba6b9ca
--- /dev/null
+++ b/src/views/system/post/modules/import-form.vue
@@ -0,0 +1,82 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Upload } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { importPost, importPostTemplate } from '#/api/system/post';
+import { $t } from '#/locales';
+
+import { useImportFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ formItemClass: 'col-span-2',
+ labelWidth: 120,
+ },
+ layout: 'horizontal',
+ schema: useImportFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = await formApi.getValues();
+ try {
+ await importPost(data.file, data.updateSupport);
+ // 鍏抽棴骞舵彁绀�
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+/** 涓婁紶鍓� */
+function beforeUpload(file: FileType) {
+ formApi.setFieldValue('file', file);
+ return false;
+}
+
+/** 涓嬭浇妯$増 */
+async function handleDownload() {
+ const data = await importPostTemplate();
+ downloadFileFromBlobPart({ fileName: '宀椾綅瀵煎叆妯℃澘.xls', source: data });
+}
+</script>
+
+<template>
+ <Modal title="瀵煎叆宀椾綅" class="w-1/3">
+ <Form class="mx-4">
+ <template #file>
+ <div class="w-full">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :before-upload="beforeUpload"
+ >
+ <Button type="primary"> 閫夋嫨 Excel 鏂囦欢 </Button>
+ </Upload>
+ </div>
+ </template>
+ </Form>
+ <template #prepend-footer>
+ <div class="flex flex-auto items-center">
+ <Button @click="handleDownload"> 涓嬭浇瀵煎叆妯℃澘 </Button>
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/system/user/data.ts b/src/views/system/user/data.ts
index fb9ea8f..8506c40 100644
--- a/src/views/system/user/data.ts
+++ b/src/views/system/user/data.ts
@@ -72,6 +72,7 @@
fieldName: 'postIds',
label: '宀椾綅',
component: 'ApiSelect',
+ rules: 'required',
componentProps: {
api: getSimplePostList,
labelField: 'name',
diff --git a/src/views/wls/arrivalnotice/index.vue b/src/views/wls/arrivalnotice/index.vue
index 5cfe5b4..198da46 100644
--- a/src/views/wls/arrivalnotice/index.vue
+++ b/src/views/wls/arrivalnotice/index.vue
@@ -155,13 +155,14 @@
<template #toolbar-tools>
<TableAction
:actions="[
- {
- label: $t('ui.actionTitle.create', ['鍒拌揣閫氱煡鍗�']),
- type: 'primary',
- icon: ACTION_ICON.ADD,
- auth: ['mes:wm-arrival-notice:create'],
- onClick: handleCreate,
- },
+ // 娉ㄩ噴锛氶殣钘忔柊澧炲埌璐ч�氱煡鍗曟寜閽�
+ // {
+ // label: $t('ui.actionTitle.create', ['鍒拌揣閫氱煡鍗�']),
+ // type: 'primary',
+ // icon: ACTION_ICON.ADD,
+ // auth: ['mes:wm-arrival-notice:create'],
+ // onClick: handleCreate,
+ // },
{
label: '浠庨噰璐鍗曠敓鎴�',
type: 'primary',
diff --git a/src/views/wls/arrivalnotice/modules/po-select-dialog.vue b/src/views/wls/arrivalnotice/modules/po-select-dialog.vue
index 6d6d492..4dd38ed 100644
--- a/src/views/wls/arrivalnotice/modules/po-select-dialog.vue
+++ b/src/views/wls/arrivalnotice/modules/po-select-dialog.vue
@@ -2,7 +2,7 @@
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import { Page, useVbenModal } from '@vben/common-ui';
+import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
@@ -62,7 +62,7 @@
field: 'totalPrice',
title: '鎬婚噾棰�',
width: 120,
- formatter: 'formatMoney',
+ formatter: 'formatAmount2',
},
{
field: 'status',
@@ -74,7 +74,7 @@
},
},
],
- height: 'auto',
+ height: 460,
keepSource: true,
proxyConfig: {
ajax: {
@@ -108,16 +108,14 @@
emit('selected', selectedRows.id);
await modalApi.close();
},
- async onOpenChange(isOpen: boolean) {
- if (isOpen) {
- await gridApi.query();
- }
+ async onOpened() {
+ await gridApi.query();
},
});
</script>
<template>
- <Modal title="閫夋嫨閲囪喘璁㈠崟" class="w-2/3">
+ <Modal title="閫夋嫨閲囪喘璁㈠崟" class="w-[1000px]">
<Grid table-title="閲囪喘璁㈠崟鍒楄〃" />
</Modal>
</template>
diff --git a/src/views/wls/barcode/config/index.vue b/src/views/wls/barcode/config/index.vue
index b6cc804..1f00587 100644
--- a/src/views/wls/barcode/config/index.vue
+++ b/src/views/wls/barcode/config/index.vue
@@ -22,17 +22,26 @@
destroyOnClose: true,
});
-/** 鑷姩鐢熸垚寮�鍏冲彉鏇� */
+/**
+ * 鑷姩鐢熸垚寮�鍏冲彉鏇�
+ *
+ * 娉ㄦ剰锛欳ellSwitch 鐨� beforeChange 璋冪敤绾﹀畾涓� (newVal, row)
+ * 鈥斺�� 绗竴涓弬鏁版槸寮�鍏虫柊鍊硷紙antd Switch 閰嶇疆 checkedValue=1/unCheckedValue=0锛�
+ * 鍥犳鍙栧�兼槸鏁板瓧 1/0锛夛紝绗簩涓弬鏁版墠鏄暣琛屾暟鎹�傚師鍏堟妸 newVal 褰撴垚浜� row锛�
+ * 瀵艰嚧 updateBarcodeConfig(row) 鐨勮姹備綋琚簭鍒楀寲鎴愭暟瀛� 1/0锛屽悗绔В鏋� 500銆�
+ */
async function handleAutoGenerateChange(
+ newVal: number | boolean,
row: MesWmBarcodeConfigApi.BarcodeConfig,
): Promise<boolean> {
- const text = row.autoGenerateFlag ? '鍚敤' : '鍋滅敤';
+ const next = newVal === true || newVal === 1; // 缁熶竴杞垚甯冨皵
+ const text = next ? '鍚敤' : '鍋滅敤';
try {
await confirm(`纭瑕�${text}鑷姩鐢熸垚鍚楋紵`);
} catch {
return false;
}
- await updateBarcodeConfig(row);
+ await updateBarcodeConfig({ ...row, autoGenerateFlag: next });
message.success(`${text}鎴愬姛`);
return true;
}
diff --git a/src/views/wls/barcode/data.ts b/src/views/wls/barcode/data.ts
index dfd288c..c34a07b 100644
--- a/src/views/wls/barcode/data.ts
+++ b/src/views/wls/barcode/data.ts
@@ -609,6 +609,7 @@
/** 閰嶇疆鍒楄〃鐨勫瓧娈� */
export function useConfigGridColumns(
onAutoGenerateChange: (
+ newVal: number | boolean,
row: MesWmBarcodeConfigApi.BarcodeConfig,
) => Promise<boolean>,
): VxeTableGridOptions<MesWmBarcodeConfigApi.BarcodeConfig>['columns'] {
diff --git a/src/views/wls/itemreceipt/modules/an-select-dialog.vue b/src/views/wls/itemreceipt/modules/an-select-dialog.vue
index eef4eef..dfdc0c5 100644
--- a/src/views/wls/itemreceipt/modules/an-select-dialog.vue
+++ b/src/views/wls/itemreceipt/modules/an-select-dialog.vue
@@ -79,7 +79,7 @@
},
},
],
- height: 'auto',
+ height: 460,
keepSource: true,
proxyConfig: {
ajax: {
@@ -114,10 +114,8 @@
emit('selected', selectedRows.id);
await modalApi.close();
},
- async onOpenChange(isOpen: boolean) {
- if (isOpen) {
- await gridApi.query();
- }
+ async onOpened() {
+ await gridApi.query();
},
});
</script>
diff --git a/src/views/wls/materialstock/components/import-form.vue b/src/views/wls/materialstock/components/import-form.vue
new file mode 100644
index 0000000..b5518a7
--- /dev/null
+++ b/src/views/wls/materialstock/components/import-form.vue
@@ -0,0 +1,177 @@
+<script lang="ts" setup>
+import type { FileType } from 'ant-design-vue/es/upload/interface';
+import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { downloadFileFromBlobPart } from '@vben/utils';
+
+import { Button, message, Progress, Upload } from 'ant-design-vue';
+
+import {
+ importMaterialStock,
+ importMaterialStockTemplate,
+} from '#/api/mes/wm/materialstock';
+
+defineOptions({ name: 'WmMaterialStockImportModal' });
+
+const emit = defineEmits<{ success: [] }>();
+
+const fileList = ref<FileType[]>([]);
+const importResult = ref<MesWmMaterialStockApi.ImportResult>();
+const importing = ref(false);
+const progressPercent = ref(0);
+let progressTimer: ReturnType<typeof setInterval> | undefined;
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ let shouldClose = false;
+ if (fileList.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸鍏ョ殑鏂囦欢');
+ return;
+ }
+ if (importing.value) return;
+ modalApi.lock();
+ importing.value = true;
+ startMockProgress();
+ try {
+ importResult.value = await importMaterialStock(fileList.value[0] as File);
+ // 瀹屾垚锛氳繘搴︽潯璧版弧
+ stopMockProgress();
+ progressPercent.value = 100;
+ await delay(300);
+ emit('success');
+ const failedCount = Object.keys(importResult.value?.failureNames ?? {}).length;
+ if (failedCount > 0) {
+ message.warning(
+ `瀵煎叆瀹屾垚锛氭垚鍔� ${createCount.value} 鏉°�佽鐩� ${updateCount.value} 鏉★紝澶辫触 ${failedCount} 鏉★紝璇锋煡鐪嬩笅鏂瑰け璐ユ槑缁哷,
+ );
+ } else {
+ message.success('瀵煎叆瀹屾垚');
+ shouldClose = true;
+ }
+ } finally {
+ stopMockProgress();
+ importing.value = false;
+ modalApi.unlock();
+ if (shouldClose) {
+ await modalApi.close();
+ }
+ }
+ },
+});
+
+/** 妯℃嫙杩涘害鏉★細璇锋眰鏈熼棿浠� 0 璧板埌 90% */
+function startMockProgress() {
+ progressPercent.value = 0;
+ progressTimer = setInterval(() => {
+ if (progressPercent.value < 90) {
+ progressPercent.value = Math.min(
+ 90,
+ progressPercent.value + Math.random() * 12,
+ );
+ }
+ }, 300);
+}
+
+function stopMockProgress() {
+ if (progressTimer) {
+ clearInterval(progressTimer);
+ progressTimer = undefined;
+ }
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+function beforeUpload(file: FileType) {
+ fileList.value = [file];
+ importResult.value = undefined;
+ return false;
+}
+
+function handleRemove() {
+ fileList.value = [];
+ importResult.value = undefined;
+}
+
+async function handleDownloadTemplate() {
+ const data = await importMaterialStockTemplate();
+ downloadFileFromBlobPart({ fileName: '搴撳瓨鐜版湁閲忓鍏ユā鏉�.xls', source: data });
+}
+
+const createCount = computed(() => importResult.value?.createNames?.length ?? 0);
+const updateCount = computed(() => importResult.value?.updateNames?.length ?? 0);
+const failureCount = computed(
+ () => Object.keys(importResult.value?.failureNames ?? {}).length,
+);
+</script>
+
+<template>
+ <Modal title="瀵煎叆搴撳瓨鐜版湁閲�" class="w-[640px]">
+ <div class="mx-4">
+ <Upload
+ :max-count="1"
+ accept=".xls,.xlsx"
+ :file-list="fileList"
+ :before-upload="beforeUpload"
+ @remove="handleRemove"
+ >
+ <Button type="primary">閫夋嫨 Excel 鏂囦欢</Button>
+ </Upload>
+ <div
+ class="mt-3 rounded-md border border-gray-200 bg-gray-50 p-3 text-sm leading-6 text-gray-600"
+ >
+ 妯℃澘鍒楋細鐗╂枡缂栫爜銆佷粨搴撱�佸簱鍖恒�佸簱浣嶃�佹壒娆″彿銆佷緵搴斿晢銆�<b>鎿嶄綔绫诲瀷</b>銆�<b>鍦ㄥ簱鏁伴噺</b>銆�<br />
+ 鎿嶄綔绫诲瀷蹇呭~锛�
+ <b class="text-gray-800">澧炲姞</b> / <b class="text-gray-800">鍑忓皯</b>
+ 鎸�"鍦ㄥ簱鏁伴噺"浣滀负鏈鍙樺姩閲忓姞鍑忓簱瀛橈紱
+ <b class="text-gray-800">瑕嗙洊</b>
+ 鎶婅璁板綍鍦ㄥ簱鏁伴噺鐩存帴璁句负"鍦ㄥ簱鏁伴噺"锛堟湡鍒濆缓璐︽帹鑽愶級锛屽凡瀛樺湪璁板綍鑷姩鎸夊樊棰濊皟鏁淬��<br />
+ 浠撳簱/搴撳尯/搴撲綅/渚涘簲鍟嗗彲濉紪鐮佹垨鍚嶇О锛涚墿鏂欏惎鐢ㄦ壒娆$鐞嗘椂鎵规鍙峰繀濉紱姣忚鐙珛鎵ц锛屽け璐ヨ涓嶅奖鍝嶅叾瀹冭锛涗粎鏀寔
+ xls銆亁lsx 鏂囦欢
+ </div>
+ <!-- 瀵煎叆杩涘害鏉� -->
+ <div v-if="importing" class="mt-4">
+ <div class="mb-2 text-sm text-gray-600">姝e湪瀵煎叆搴撳瓨鏁版嵁锛岃绋嶅��...</div>
+ <Progress :percent="progressPercent" status="active" />
+ </div>
+ <!-- 瀵煎叆缁撴灉 -->
+ <div
+ v-if="importResult"
+ class="mt-4 rounded-md border border-gray-200 bg-gray-50 p-3"
+ >
+ <div class="mb-2 font-medium">瀵煎叆缁撴灉</div>
+ <div class="mb-2 flex flex-wrap gap-4">
+ <span class="text-green-600">鎴愬姛鏂板缓锛歿{ createCount }} 鏉�</span>
+ <span class="text-blue-600">鎴愬姛鏇存柊锛歿{ updateCount }} 鏉�</span>
+ <span class="text-red-600">澶辫触锛歿{ failureCount }} 鏉�</span>
+ </div>
+ <div v-if="createCount > 0" class="max-h-32 overflow-y-auto text-sm leading-6 text-green-600">
+ <div v-for="name in importResult.createNames" :key="`c-${name}`">
+ {{ name }}
+ </div>
+ </div>
+ <div v-if="updateCount > 0" class="max-h-32 overflow-y-auto text-sm leading-6 text-blue-600">
+ <div v-for="name in importResult.updateNames" :key="`u-${name}`">
+ {{ name }}
+ </div>
+ </div>
+ <div v-if="failureCount > 0" class="max-h-40 overflow-y-auto">
+ <div
+ v-for="(reason, name) in importResult.failureNames"
+ :key="`f-${name}`"
+ class="text-sm leading-6 text-red-500"
+ >
+ {{ name }}锛歿{ reason }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <template #prepend-footer>
+ <Button @click="handleDownloadTemplate">涓嬭浇瀵煎叆妯℃澘</Button>
+ </template>
+ </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/materialstock/components/index.ts b/src/views/wls/materialstock/components/index.ts
index 60d3bf1..7ecabaf 100644
--- a/src/views/wls/materialstock/components/index.ts
+++ b/src/views/wls/materialstock/components/index.ts
@@ -2,3 +2,4 @@
export { default as WmMaterialStockSelect } from './select.vue';
export { default as WmStockAdjustModal } from './stock-adjust-modal.vue';
export { default as WmStockMoveModal } from './move-modal.vue';
+export { default as WmMaterialStockImportModal } from './import-form.vue';
diff --git a/src/views/wls/materialstock/components/stock-adjust-modal.vue b/src/views/wls/materialstock/components/stock-adjust-modal.vue
index aeb5883..53ad6ca 100644
--- a/src/views/wls/materialstock/components/stock-adjust-modal.vue
+++ b/src/views/wls/materialstock/components/stock-adjust-modal.vue
@@ -20,7 +20,7 @@
} from '#/api/mes/wm/materialstock';
import { getItem } from '#/api/mdm/item';
import { MdmItemSelect } from '#/views/basicData/mdm/components';
-import { ErpSupplierSelect } from '#/views/erp/purchase/supplier/components';
+import { SrmSupplierSelect } from '#/views/srm/supplier/components';
import BatchConfigDisplay from './batch-config-display.vue';
import {
WmWarehouseAreaSelect,
@@ -297,11 +297,12 @@
{
fieldName: 'vendorId',
label: '渚涘簲鍟�',
- component: markRaw(ErpSupplierSelect),
+ component: markRaw(SrmSupplierSelect),
componentProps: {
placeholder: '璇烽�夋嫨渚涘簲鍟�',
+ allowClear: true,
},
- rules: 'selectRequired',
+ // 渚涘簲鍟嗘敼涓洪潪蹇呭~锛氬彲鐣欑┖锛岄�夋嫨鍚庡弬涓庡簱瀛樼淮搴﹁褰�
},
// 閲囪喘璁㈠崟鍙凤紙鏍规嵁鎵规閰嶇疆鏄剧ず锛屼粎鍏ュ簱锛�
{
diff --git a/src/views/wls/materialstock/index.vue b/src/views/wls/materialstock/index.vue
index 17e3625..91a398b 100644
--- a/src/views/wls/materialstock/index.vue
+++ b/src/views/wls/materialstock/index.vue
@@ -19,7 +19,11 @@
import { MdItemTypeTree } from '#/views/mes/md/item/type/components';
import { WmBatchDetail } from '#/views/wls/batch/components';
import AreaForm from '#/views/wls/warehouse/area/modules/form.vue';
-import { WmStockAdjustModal, WmStockMoveModal } from './components';
+import {
+ WmMaterialStockImportModal,
+ WmStockAdjustModal,
+ WmStockMoveModal,
+} from './components';
import { useGridColumns, useGridFormSchema } from './data';
@@ -35,6 +39,11 @@
const [StockMoveModal, stockMoveModalApi] = useVbenModal({
connectedComponent: WmStockMoveModal,
+ destroyOnClose: true,
+});
+
+const [ImportModal, importModalApi] = useVbenModal({
+ connectedComponent: WmMaterialStockImportModal,
destroyOnClose: true,
});
@@ -72,6 +81,16 @@
/** 鎵撳紑搴撳瓨璋冩暣寮圭獥 */
function handleOpenStockAdjust() {
stockAdjustModalApi.setData({}).open();
+}
+
+/** 鎵撳紑搴撳瓨鐜版湁閲忓鍏ュ脊绐� */
+function handleOpenImport() {
+ importModalApi.open();
+}
+
+/** 瀵煎叆鎴愬姛鍥炶皟 */
+function handleStockImportSuccess() {
+ gridApi.query();
}
/** 搴撳瓨璋冩暣鎴愬姛鍥炶皟 */
@@ -155,6 +174,7 @@
<WmBatchDetail ref="batchDetailRef" />
<StockAdjustModal @success="handleStockAdjustSuccess" />
<StockMoveModal @success="handleStockMoveSuccess" />
+ <ImportModal @success="handleStockImportSuccess" />
<div class="flex h-full w-full">
<!-- 宸︿晶鐗╂枡鍒嗙被鏍� -->
@@ -175,6 +195,13 @@
onClick: handleOpenStockAdjust,
},
{
+ label: '瀵煎叆',
+ type: 'primary',
+ icon: ACTION_ICON.UPLOAD,
+ auth: ['mes:wm-material-stock:import'],
+ onClick: handleOpenImport,
+ },
+ {
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
diff --git a/src/views/wls/productissue/components/index.ts b/src/views/wls/productissue/components/index.ts
new file mode 100644
index 0000000..aa8b15e
--- /dev/null
+++ b/src/views/wls/productissue/components/index.ts
@@ -0,0 +1,2 @@
+export { default as WmProductIssueLineSelect } from './select.vue';
+export { default as WmProductIssueLineSelectDialog } from './select-dialog.vue';
diff --git a/src/views/wls/productissue/components/select-dialog.vue b/src/views/wls/productissue/components/select-dialog.vue
new file mode 100644
index 0000000..09bb732
--- /dev/null
+++ b/src/views/wls/productissue/components/select-dialog.vue
@@ -0,0 +1,106 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
+
+import { nextTick, ref } from 'vue';
+
+import { message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getReturnableIssueLines } from '#/api/mes/wm/productissue/line';
+
+const props = defineProps<{
+ workOrderId?: number;
+}>();
+
+const emit = defineEmits<{
+ selected: [rows: MesWmProductIssueLineApi.ProductIssueLine[]];
+}>();
+
+const open = ref(false);
+const selectedRows = ref<MesWmProductIssueLineApi.ProductIssueLine[]>([]);
+
+const columns: VxeTableGridOptions<MesWmProductIssueLineApi.ProductIssueLine>['columns'] = [
+ { type: 'radio', width: 60 },
+ { field: 'itemCode', title: '鐗╂枡缂栫爜', minWidth: 120 },
+ { field: 'itemName', title: '鐗╂枡鍚嶇О', minWidth: 140 },
+ { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 120 },
+ { field: 'unitMeasureName', title: '鍗曚綅', width: 80 },
+ { field: 'quantity', title: '鍙��鏁伴噺', width: 100 },
+ { field: 'batchCode', title: '鎵规鍙�', minWidth: 120 },
+];
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns,
+ height: 480,
+ keepSource: true,
+ radioConfig: { highlight: true, trigger: 'row' },
+ proxyConfig: {
+ ajax: {
+ query: async () => {
+ if (!props.workOrderId) {
+ return { list: [], total: 0 };
+ }
+ const list = await getReturnableIssueLines(props.workOrderId);
+ return { list, total: list.length };
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ },
+ } as VxeTableGridOptions<MesWmProductIssueLineApi.ProductIssueLine>,
+ gridEvents: {
+ radioChange: ({ row }: { row: MesWmProductIssueLineApi.ProductIssueLine }) => {
+ selectedRows.value = [row];
+ },
+ cellDblclick: async ({ row }: { row: MesWmProductIssueLineApi.ProductIssueLine }) => {
+ selectedRows.value = [row];
+ await gridApi.grid.setRadioRow(row);
+ handleConfirm();
+ },
+ },
+});
+
+async function openModal() {
+ open.value = true;
+ selectedRows.value = [];
+ await nextTick();
+ await gridApi.grid.clearRadioRow();
+ await gridApi.query();
+}
+
+function closeModal() {
+ open.value = false;
+ selectedRows.value = [];
+}
+
+function handleConfirm() {
+ if (selectedRows.value.length === 0) {
+ message.warning('璇烽�夋嫨涓�鏉¢鏂欒褰�');
+ return;
+ }
+ emit('selected', selectedRows.value);
+ open.value = false;
+}
+
+defineExpose({ open: openModal });
+</script>
+
+<template>
+ <Modal
+ v-model:open="open"
+ :destroy-on-close="true"
+ title="閫夋嫨棰嗘枡璁板綍"
+ width="70%"
+ @cancel="closeModal"
+ @ok="handleConfirm"
+ >
+ <Grid table-title="鍙��鏂欑殑棰嗘枡璁板綍" />
+ </Modal>
+</template>
diff --git a/src/views/wls/productissue/components/select.vue b/src/views/wls/productissue/components/select.vue
new file mode 100644
index 0000000..8a86b5f
--- /dev/null
+++ b/src/views/wls/productissue/components/select.vue
@@ -0,0 +1,141 @@
+<script lang="ts" setup>
+import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
+
+import { computed, ref, useAttrs, watch } from 'vue';
+
+import { IconifyIcon } from '@vben/icons';
+
+import { Input, Tooltip } from 'ant-design-vue';
+
+import { getProductIssueLine } from '#/api/mes/wm/productissue/line';
+
+import ProductIssueLineSelectDialog from './select-dialog.vue';
+
+defineOptions({ name: 'WmProductIssueLineSelect', inheritAttrs: false });
+
+const props = withDefaults(
+ defineProps<{
+ allowClear?: boolean;
+ disabled?: boolean;
+ modelValue?: number;
+ placeholder?: string;
+ workOrderId?: number;
+ }>(),
+ {
+ allowClear: true,
+ disabled: false,
+ modelValue: undefined,
+ placeholder: '璇烽�夋嫨棰嗘枡璁板綍',
+ workOrderId: undefined,
+ },
+);
+
+const emit = defineEmits<{
+ change: [item: MesWmProductIssueLineApi.ProductIssueLine | undefined];
+ 'update:modelValue': [value: number | undefined];
+}>();
+
+const attrs = useAttrs();
+const dialogRef = ref<InstanceType<typeof ProductIssueLineSelectDialog>>();
+const hovering = ref(false);
+const selectedItem = ref<MesWmProductIssueLineApi.ProductIssueLine>();
+
+const displayLabel = computed(() => {
+ const item = selectedItem.value;
+ if (!item) {
+ return '';
+ }
+ return `${item.itemCode || '-'} | ${item.itemName || '-'} | 鏁伴噺:${item.quantity}`;
+});
+
+const showClear = computed(
+ () =>
+ props.allowClear &&
+ !props.disabled &&
+ hovering.value &&
+ props.modelValue !== null,
+);
+
+async function resolveItemById(id: number | undefined) {
+ if (!id) {
+ selectedItem.value = undefined;
+ return;
+ }
+ if (selectedItem.value?.id === id) {
+ return;
+ }
+ selectedItem.value = await getProductIssueLine(id);
+}
+
+watch(() => props.modelValue, resolveItemById, { immediate: true });
+
+function clearSelected() {
+ selectedItem.value = undefined;
+ emit('update:modelValue', undefined);
+ emit('change', undefined);
+}
+
+function handleClick(event: MouseEvent) {
+ if (props.disabled) {
+ return;
+ }
+ const target = event.target as HTMLElement;
+ if (showClear.value && target.closest('.ant-input-suffix')) {
+ event.stopPropagation();
+ clearSelected();
+ return;
+ }
+ dialogRef.value?.open();
+}
+
+function handleSelected(rows: MesWmProductIssueLineApi.ProductIssueLine[]) {
+ const item = rows[0];
+ if (!item) {
+ return;
+ }
+ selectedItem.value = item;
+ emit('update:modelValue', item.id);
+ emit('change', item);
+}
+</script>
+
+<template>
+ <div
+ v-bind="attrs"
+ class="w-full"
+ :class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
+ @click="handleClick"
+ @mouseenter="hovering = true"
+ @mouseleave="hovering = false"
+ >
+ <Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
+ <template #title>
+ <div v-if="selectedItem" class="leading-6">
+ <div>鐗╂枡缂栫爜锛歿{ selectedItem.itemCode || '-' }}</div>
+ <div>鐗╂枡鍚嶇О锛歿{ selectedItem.itemName || '-' }}</div>
+ <div>瑙勬牸鍨嬪彿锛歿{ selectedItem.specification || '-' }}</div>
+ <div>鍙��鏁伴噺锛歿{ selectedItem.quantity ?? '-' }}</div>
+ <div>鎵规鍙凤細{{ selectedItem.batchCode || '-' }}</div>
+ </div>
+ </template>
+ <Input
+ :disabled="disabled"
+ :placeholder="placeholder"
+ :value="displayLabel"
+ readonly
+ >
+ <template #suffix>
+ <IconifyIcon
+ class="size-4"
+ :icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
+ />
+ </template>
+ </Input>
+ </Tooltip>
+ </div>
+ <ProductIssueLineSelectDialog
+ ref="dialogRef"
+ :work-order-id="workOrderId"
+ @selected="handleSelected"
+ />
+</template>
diff --git a/src/views/wls/productissue/data.ts b/src/views/wls/productissue/data.ts
index 2ee6dcc..896c624 100644
--- a/src/views/wls/productissue/data.ts
+++ b/src/views/wls/productissue/data.ts
@@ -1,5 +1,6 @@
锘縤mport type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProTaskApi } from '#/api/mes/pro/task';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue';
import type { MesWmProductIssueDetailApi } from '#/api/mes/wm/productissue/detail';
@@ -20,6 +21,7 @@
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
+import { ProTaskSelect } from '#/views/mes/pro/task/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { WmMaterialStockSelect } from '#/views/wls/materialstock/components';
import {
@@ -110,7 +112,33 @@
label: '鐢熶骇璁㈠崟',
component: markRaw(ProWorkOrderSelect),
componentProps: {
+ placeholder: '璇烽�夋嫨鐢熶骇璁㈠崟',
status: MesProWorkOrderStatusEnum.CONFIRMED,
+ // 璁㈠崟鍙樻洿锛氭竻绌哄凡閫夌敓浜т换鍔★紝閬垮厤浠诲姟涓庤鍗曚笉涓�鑷�
+ onChange: async () => {
+ await formApi?.setFieldValue('taskId', undefined);
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'taskId',
+ label: '鐢熶骇浠诲姟',
+ component: markRaw(ProTaskSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐢熶骇浠诲姟',
+ // 閫変腑浠诲姟鍚庤嚜鍔ㄥ甫鍑哄叾宸ヤ綔绔�
+ onChange: async (task?: MesProTaskApi.Task) => {
+ await formApi?.setFieldValue('workstationId', task?.workstationId);
+ },
+ },
+ dependencies: {
+ triggerFields: ['workOrderId'],
+ componentProps: (values) => ({
+ disabled: !values.workOrderId,
+ placeholder: values.workOrderId ? '璇烽�夋嫨鐢熶骇浠诲姟' : '璇峰厛閫夋嫨鐢熶骇璁㈠崟',
+ workOrderId: values.workOrderId,
+ }),
},
rules: 'selectRequired',
},
@@ -198,6 +226,11 @@
{
field: 'workOrderCode',
title: '鐢熶骇璁㈠崟',
+ minWidth: 140,
+ },
+ {
+ field: 'taskCode',
+ title: '鐢熶骇浠诲姟',
minWidth: 140,
},
{
@@ -370,6 +403,14 @@
},
},
{
+ fieldName: 'batchId',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
fieldName: 'itemId',
label: '鐗╂枡',
component: markRaw(MdItemSelect),
@@ -382,12 +423,13 @@
label: '搴撳瓨璁板綍',
component: markRaw(WmMaterialStockSelect),
componentProps: {
- // 閫夋嫨搴撳瓨璁板綍鍚庯紝鑷姩鍥炲~浠撳簱/搴撳尯/搴撲綅/鎵规/鏁伴噺
+ // 閫夋嫨搴撳瓨璁板綍鍚庯紝鑷姩鍥炲~鐗╂枡/浠撳簱/搴撳尯/搴撲綅/鎵规/鏁伴噺锛屽叏閮ㄤ互搴撳瓨璁板綍涓哄噯
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
await formApi?.setValues({
areaId: stock?.areaId,
batchCode: stock?.batchCode,
batchId: stock?.batchId,
+ itemId: stock?.itemId,
locationId: stock?.locationId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
@@ -399,6 +441,7 @@
dependencies: {
triggerFields: ['itemId'],
componentProps: (values) => ({
+ // 搴撳瓨閫夋嫨寮圭獥鍙瘑鍒� mdmItemId 鍏ュ弬锛屽苟鐢ㄥ畠浣滀负 itemId 杩囨护鏉′欢
mdmItemId: values.itemId,
}),
},
diff --git a/src/views/wls/productreceipt/data.ts b/src/views/wls/productreceipt/data.ts
index 07cc1bf..f249a84 100644
--- a/src/views/wls/productreceipt/data.ts
+++ b/src/views/wls/productreceipt/data.ts
@@ -1,4 +1,4 @@
-锘縤mport type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt';
@@ -108,6 +108,7 @@
component: markRaw(ProWorkOrderSelect),
componentProps: {
status: MesProWorkOrderStatusEnum.CONFIRMED,
+ checkMethod: (row: any) => row.productionStatus !== 0,
},
},
{
diff --git a/src/views/wls/returnissue/data.ts b/src/views/wls/returnissue/data.ts
index 4ca6336..691b573 100644
--- a/src/views/wls/returnissue/data.ts
+++ b/src/views/wls/returnissue/data.ts
@@ -1,6 +1,7 @@
锘縤mport type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
+import type { MesWmProductIssueLineApi } from '#/api/mes/wm/productissue/line';
import type { MesWmReturnIssueApi } from '#/api/mes/wm/returnissue';
import type { MesWmReturnIssueDetailApi } from '#/api/mes/wm/returnissue/detail';
import type { MesWmReturnIssueLineApi } from '#/api/mes/wm/returnissue/line';
@@ -20,6 +21,7 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
+import { WmProductIssueLineSelect } from '#/views/wls/productissue/components';
import { WmMaterialStockSelect } from '#/views/wls/materialstock/components';
import {
WmWarehouseAreaSelect,
@@ -316,27 +318,29 @@
}
/** 閫�鏂欏崟琛屾柊澧�/淇敼鐨勮〃鍗� */
-export function useLineFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+export function useLineFormSchema(
+ formApi?: VbenFormApi,
+ workOrderId?: number,
+): VbenFormSchema[] {
return [
{
- fieldName: 'materialStockId',
- label: '搴撳瓨璁板綍',
- component: markRaw(WmMaterialStockSelect),
+ fieldName: 'issueLineId',
+ label: '棰嗘枡璁板綍',
+ component: markRaw(WmProductIssueLineSelect),
componentProps: {
- // 閫夋嫨搴撳瓨璁板綍鍚庯紝鑷姩鍥炲~鐗╂枡/鎵规/鏁伴噺
- onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
+ workOrderId,
+ onChange: async (line?: MesWmProductIssueLineApi.ProductIssueLine) => {
await formApi?.setValues({
- batchCode: stock?.batchCode,
- batchId: stock?.batchId,
- itemId: stock?.itemId,
- materialDisplayName: stock
- ? [stock.itemName, stock.specification].filter(Boolean).join(' | ')
+ batchCode: line?.batchCode,
+ batchId: line?.batchId,
+ itemId: line?.itemId,
+ materialDisplayName: line
+ ? [line.itemName, line.specification].filter(Boolean).join(' | ')
: undefined,
- quantity: stock?.quantity,
- quantityMax: stock?.quantity,
+ quantity: line?.quantity,
+ quantityMax: line?.quantity,
});
},
- virtualFilter: 'only',
},
rules: 'selectRequired',
},
@@ -390,7 +394,7 @@
component: 'Input',
componentProps: {
disabled: true,
- placeholder: '閫夋嫨搴撳瓨鍚庤嚜鍔ㄥ甫鍑�',
+ placeholder: '閫夋嫨棰嗘枡璁板綍鍚庤嚜鍔ㄥ甫鍑�',
},
},
{
@@ -399,7 +403,7 @@
component: 'Input',
componentProps: {
disabled: true,
- placeholder: '閫夋嫨搴撳瓨鍚庤嚜鍔ㄥ甫鍑�',
+ placeholder: '閫夋嫨棰嗘枡璁板綍鍚庤嚜鍔ㄥ甫鍑�',
},
},
{
diff --git a/src/views/wls/returnissue/modules/form.vue b/src/views/wls/returnissue/modules/form.vue
index 1546dd4..2d340e9 100644
--- a/src/views/wls/returnissue/modules/form.vue
+++ b/src/views/wls/returnissue/modules/form.vue
@@ -198,7 +198,7 @@
<template v-if="formData?.id">
<Divider>鐗╂枡淇℃伅</Divider>
<div class="mx-4">
- <LineList :form-type="formType" :issue-id="formData.id" />
+ <LineList :form-type="formType" :issue-id="formData.id" :work-order-id="formData.workOrderId" />
</div>
</template>
<template #prepend-footer>
diff --git a/src/views/wls/returnissue/modules/line-form.vue b/src/views/wls/returnissue/modules/line-form.vue
index fc72468..4e14a8d 100644
--- a/src/views/wls/returnissue/modules/line-form.vue
+++ b/src/views/wls/returnissue/modules/line-form.vue
@@ -19,7 +19,8 @@
const emit = defineEmits(['success']);
const formData = ref<MesWmReturnIssueLineApi.ReturnIssueLine>();
-const issueId = ref<number>(); // 鎵�灞為��鏂欏崟缂栧彿
+const issueId = ref<number>();
+const workOrderId = ref<number>();
const getTitle = computed(() => {
return formData.value?.id
@@ -69,17 +70,16 @@
formData.value = undefined;
return;
}
- formApi.setState({ schema: useLineFormSchema(formApi) });
- // 鍔犺浇鏁版嵁
- const data = modalApi.getData<{ id?: number; issueId: number }>();
+ const data = modalApi.getData<{ id?: number; issueId: number; workOrderId?: number }>();
issueId.value = data.issueId;
+ workOrderId.value = data.workOrderId;
+ formApi.setState({ schema: useLineFormSchema(formApi, workOrderId.value) });
if (!data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getReturnIssueLine(data.id);
- // 璁剧疆鍒� values锛屽苟鏋勫缓鐗╂枡灞曠ず鍚�
await formApi.setValues({
...formData.value,
materialDisplayName: [formData.value.itemName, formData.value.specification]
diff --git a/src/views/wls/returnissue/modules/line-list.vue b/src/views/wls/returnissue/modules/line-list.vue
index 98c6682..14c7ccf 100644
--- a/src/views/wls/returnissue/modules/line-list.vue
+++ b/src/views/wls/returnissue/modules/line-list.vue
@@ -29,6 +29,7 @@
const props = defineProps<{
formType: FormType;
issueId: number;
+ workOrderId?: number;
}>();
const isEditable = computed(() =>
@@ -61,12 +62,12 @@
/** 娣诲姞鐗╂枡 */
function handleCreate() {
- lineFormModalApi.setData({ issueId: props.issueId }).open();
+ lineFormModalApi.setData({ issueId: props.issueId, workOrderId: props.workOrderId }).open();
}
/** 缂栬緫鐗╂枡 */
function handleEdit(row: MesWmReturnIssueLineApi.ReturnIssueLine) {
- lineFormModalApi.setData({ id: row.id, issueId: props.issueId }).open();
+ lineFormModalApi.setData({ id: row.id, issueId: props.issueId, workOrderId: props.workOrderId }).open();
}
/** 鍒犻櫎鐗╂枡 */
diff --git a/src/views/wls/returnvendor/data.ts b/src/views/wls/returnvendor/data.ts
index 1903c81..7a4442d 100644
--- a/src/views/wls/returnvendor/data.ts
+++ b/src/views/wls/returnvendor/data.ts
@@ -439,7 +439,10 @@
}
/** 鎷h揣鏄庣粏鏂板/淇敼鐨勮〃鍗� */
-export function useDetailFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+export function useDetailFormSchema(
+ formApi?: VbenFormApi,
+ onStockChange?: (stock?: MesWmMaterialStockApi.MaterialStock) => void,
+): VbenFormSchema[] {
return [
{
fieldName: 'quantityMax',
@@ -464,10 +467,10 @@
componentProps: {
// 閫夋嫨搴撳瓨璁板綍鍚庯紝鑷姩鍥炲~浠撳簱/搴撳尯/搴撲綅/鎵规/鏁伴噺
onChange: async (stock?: MesWmMaterialStockApi.MaterialStock) => {
+ onStockChange?.(stock);
await formApi?.setValues({
areaId: stock?.areaId,
batchCode: stock?.batchCode,
- batchId: stock?.batchId,
locationId: stock?.locationId,
quantity: stock?.quantity,
quantityMax: stock?.quantity,
diff --git a/src/views/wls/returnvendor/modules/detail-form.vue b/src/views/wls/returnvendor/modules/detail-form.vue
index f4a2788..d0607c7 100644
--- a/src/views/wls/returnvendor/modules/detail-form.vue
+++ b/src/views/wls/returnvendor/modules/detail-form.vue
@@ -21,6 +21,7 @@
const formData = ref<MesWmReturnVendorDetailApi.ReturnVendorDetail>();
const returnId = ref<number>(); // 鎵�灞為��璐у崟缂栧彿
const lineId = ref<number>(); // 鎵�灞為��璐у崟琛岀紪鍙�
+const batchId = ref<number>(); // 褰撳墠閫夋嫨搴撳瓨鐨勬壒娆$紪鍙�
const getTitle = computed(() => {
return formData.value?.id
@@ -54,6 +55,7 @@
(await formApi.getValues()) as MesWmReturnVendorDetailApi.ReturnVendorDetail;
data.returnId = returnId.value;
data.lineId = lineId.value;
+ data.batchId = batchId.value;
try {
await (formData.value?.id
? updateReturnVendorDetail({ ...data, id: formData.value.id })
@@ -69,9 +71,14 @@
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
+ batchId.value = undefined;
return;
}
- formApi.setState({ schema: useDetailFormSchema(formApi) });
+ formApi.setState({
+ schema: useDetailFormSchema(formApi, (stock) => {
+ batchId.value = stock?.batchId;
+ }),
+ });
// 鍔犺浇鏁版嵁
const data = modalApi.getData<{
detailId?: number;
@@ -85,6 +92,7 @@
modalApi.lock();
try {
formData.value = await getReturnVendorDetail(data.detailId);
+ batchId.value = formData.value.batchId;
// 璁剧疆鍒� values
await formApi.setValues(formData.value);
} finally {
diff --git a/src/views/wls/returnvendor/modules/form.vue b/src/views/wls/returnvendor/modules/form.vue
index 8890852..252e785 100644
--- a/src/views/wls/returnvendor/modules/form.vue
+++ b/src/views/wls/returnvendor/modules/form.vue
@@ -7,6 +7,7 @@
import { confirm, useVbenModal } from '@vben/common-ui';
import { MesWmReturnVendorStatusEnum } from '@vben/constants';
+import { formatDate } from '@vben/utils';
import { Button, Divider, message, Popconfirm } from 'ant-design-vue';
@@ -69,6 +70,17 @@
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
+
+function normalizeReturnDate(value: unknown): string | undefined {
+ if (Array.isArray(value) && value.length >= 3) {
+ const [year, month, day] = value;
+ return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} 00:00:00`;
+ }
+ if (typeof value === 'number' || typeof value === 'string') {
+ return formatDate(value, 'YYYY-MM-DD HH:mm:ss');
+ }
+ return undefined;
+}
/** 鎻愪氦閫�璐у崟锛氳〃鍗曟湁淇敼鏃跺厛淇濆瓨锛屽啀璋冪敤鎻愪氦鎺ュ彛 */
async function handleSubmit() {
@@ -187,7 +199,10 @@
try {
formData.value = await getReturnVendor(data.id);
// 璁剧疆鍒� values
- await formApi.setValues(formData.value);
+ await formApi.setValues({
+ ...formData.value,
+ returnDate: normalizeReturnDate(formData.value.returnDate),
+ });
} finally {
modalApi.unlock();
}
diff --git a/src/views/wls/stocktaking/plan/data.ts b/src/views/wls/stocktaking/plan/data.ts
index 263e3fa..9fd43c7 100644
--- a/src/views/wls/stocktaking/plan/data.ts
+++ b/src/views/wls/stocktaking/plan/data.ts
@@ -116,6 +116,7 @@
fieldName: 'blindFlag',
label: '鏄惁鐩茬洏',
component: 'Switch',
+ defaultValue: false,
componentProps: {
checkedChildren: '鏄�',
unCheckedChildren: '鍚�',
@@ -126,6 +127,7 @@
fieldName: 'frozen',
label: '鍐荤粨搴撳瓨',
component: 'Switch',
+ defaultValue: false,
componentProps: {
checkedChildren: '鏄�',
unCheckedChildren: '鍚�',
diff --git a/src/views/wls/stocktaking/plan/modules/param-form.vue b/src/views/wls/stocktaking/plan/modules/param-form.vue
index b2a87bc..228f32e 100644
--- a/src/views/wls/stocktaking/plan/modules/param-form.vue
+++ b/src/views/wls/stocktaking/plan/modules/param-form.vue
@@ -52,7 +52,7 @@
const valueValid =
values.type === WlsStockTakingParamTypeEnum.QUALITY_STATUS
? !!values.valueCode
- : values.valueId !== null;
+ : values.valueId !== undefined && values.valueId !== null;
if (!valueValid) {
message.warning('璇烽�夋嫨鏉′欢鍊�');
return;
diff --git a/src/views/wls/warehouse/area/data.ts b/src/views/wls/warehouse/area/data.ts
index 0d2d766..383994b 100644
--- a/src/views/wls/warehouse/area/data.ts
+++ b/src/views/wls/warehouse/area/data.ts
@@ -171,6 +171,7 @@
checkedChildren: '鏄�',
unCheckedChildren: '鍚�',
},
+ defaultValue: false,
rules: z.boolean().default(false),
},
{
diff --git a/src/views/wls/warehouse/data.ts b/src/views/wls/warehouse/data.ts
index 6eaf201..8c449ce 100644
--- a/src/views/wls/warehouse/data.ts
+++ b/src/views/wls/warehouse/data.ts
@@ -108,6 +108,7 @@
checkedChildren: '鏄�',
unCheckedChildren: '鍚�',
},
+ defaultValue: false,
rules: z.boolean().default(false),
},
{
diff --git a/src/views/wls/warehouse/location/data.ts b/src/views/wls/warehouse/location/data.ts
index 703c33e..4aeb226 100644
--- a/src/views/wls/warehouse/location/data.ts
+++ b/src/views/wls/warehouse/location/data.ts
@@ -98,6 +98,7 @@
checkedChildren: '鏄�',
unCheckedChildren: '鍚�',
},
+ defaultValue: false,
rules: z.boolean().default(false),
},
{
diff --git a/vite.config.ts b/vite.config.ts
index 3bb15c1..2d42c23 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,27 +1,39 @@
import { resolve } from "node:path";
import { loadEnv } from "vite";
import { defineConfig } from "./internal/vite-config/src";
-
-/** 鍏徃閰嶇疆锛氭柊澧炲叕鍙稿彧闇�鍔犱竴琛岋紝logo 鍥剧墖鏀� public/logos/ 鐩綍 */
-const companyConfigs: Record<string, { favicon: string; logo: string; name: string }> = {
- TEST: { name: '娴嬭瘯鍏徃', logo: '/logos/TEST.png', favicon: '/logos/TEST.ico' },
-};
+import { companies } from "./company.config";
export default defineConfig(async ({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_');
- const companyKey = env.VITE_COMPANY || '';
- const company = companyConfigs[companyKey];
+ // deploy.bat 閫氳繃 set "VITE_COMPANY=xx" 浼犲叆锛屾槸杩涚▼鐜鍙橀噺锛宭oadEnv 璇讳笉鍒帮紝闇�浼樺厛鍙� process.env
+ const companyKey = process.env.VITE_COMPANY || env.VITE_COMPANY || '';
+ const company = companies[companyKey];
const appLogo = company?.logo ?? '';
const appFavicon = company?.favicon ?? '';
- console.log('[company]', companyKey, '-> logo:', appLogo, 'favicon:', appFavicon);
+ const appApiUrl = company?.apiUrl ?? '';
+ const appName = company?.name ?? '';
+ const isDev = mode === 'development';
+ if (appName) {
+ // 瑕嗙洊娴忚鍣ㄦ爣绛鹃〉鏍囬锛歏ite 鐨� resolveEnv 浼樺厛璇� process.env锛屼粠鑰屾浛鎹� index.html 閲岀殑 %VITE_APP_TITLE%
+ process.env.VITE_APP_TITLE = appName;
+ }
+ console.log('[company]', companyKey, '-> name:', appName, 'logo:', appLogo, 'api:', appApiUrl || '(from env)');
+
+ const defineVars: Record<string, string> = {
+ __APP_LOGO__: JSON.stringify(appLogo),
+ __APP_FAVICON__: JSON.stringify(appFavicon),
+ __APP_NAME__: JSON.stringify(appName),
+ };
+ if (!isDev && appApiUrl) {
+ process.env.VITE_GLOB_API_URL = appApiUrl;
+ defineVars['import.meta.env.VITE_GLOB_API_URL'] = JSON.stringify(appApiUrl);
+ defineVars['import.meta.env.VITE_BASE_URL'] = JSON.stringify(new URL(appApiUrl).origin);
+ }
return {
application: {},
vite: {
- define: {
- __APP_LOGO__: JSON.stringify(appLogo),
- __APP_FAVICON__: JSON.stringify(appFavicon),
- },
+ define: defineVars,
resolve: {
alias: {
'@vue-office/pdf': resolve(__dirname, 'node_modules/@vue-office/pdf/lib/v3/vue-office-pdf.mjs'),
@@ -71,8 +83,7 @@
proxy: {
"/admin-api": {
changeOrigin: true,
- rewrite: path => path.replace(/^\/admin-api/, ""),
- target: "http://localhost:48081/admin-api",
+ target: "http://192.168.0.10:48080",
ws: true,
},
},
--
Gitblit v1.9.3