From 0333d66e4b397c161c6a44ce1e2a121c2cc41082 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 28 五月 2026 09:20:20 +0800
Subject: [PATCH] Merge branch 'dev_NEW_pro' into dev_天津_中兴实强
---
src/pages/productionDesign/processManagement/index.vue | 261
src/api/productionManagement/workOrder.js | 23
src/pages/sales/salesAccount/goOut.vue | 976
src/pages/oa/ReimburseManage/_components/ReimburseApprovalFlowEditor.vue | 245
src/pages/oa/_utils/useOaPage.js | 6
src/static/images/icon/guihuandengji.svg | 1
src/pages/productionManagement/productionScheduling/index.vue | 241
src/static/images/icon/shengchanhesuan.svg | 1
src/pages/productionManagement/productionOrder/index.vue | 697
src/pages/fileManagement/return/index.vue | 262
src/api/inventoryManagement/stockInventory.js | 101
src/pages/productionManagement/productionTraceability/index.vue | 1032 +
src/pages/oa/ApproveManage/approve-template/edit.vue | 2634 ++++
src/api/productionManagement/bom.js | 82
src/pages/oa/EnterpriseNews/news-manage/index.vue | 18
src/pages/fileManagement/return/edit.vue | 313
src/pages/cooperativeOffice/collaborativeApproval/approve.vue | 812
src/api/login.js | 71
src/pages/oa/HrManage/staff-contract/index.vue | 18
src/pages/productionManagement/productionDispatching/components/DispatchModal.vue | 708
src/pages/cooperativeOffice/collaborativeApproval/index.vue | 28
src/pages/productionManagement/mainProductionPlan/index.vue | 300
src/api/basicData/parameterMaintenance.js | 36
src/api/productionManagement/productionReporting.js | 3
src/pages/productionDesign/processManagement/params.vue | 413
src/pages/inspectionUpload/index.vue | 1488 --
src/pages/productionManagement/productionOrder/source.vue | 166
src/api/productionManagement/processManagement.js | 87
src/pages/oa/_styles/oa-approval-list.scss | 414
src/pages/oa/ReimburseManage/_utils/travelReimburseUtils.js | 82
src/api/fileManagement/statistics.js | 75
src/pages/qualityManagement/processInspection/detail.vue | 5
src/pages/productionManagement/mainProductionPlan/detail.vue | 252
src/pages/equipmentManagement/repair/index.vue | 16
src/config/oaWorkbench.js | 75
src/pages/oa/ReimburseManage/_utils/expenseDetailDisplay.js | 33
src/api/fileManagement/return.js | 61
src/pages/productionManagement/productionReporting/ledger.vue | 424
src/static/images/icon/bom.svg | 1
src/pages/qualityManagement/materialInspection/index.vue | 19
src/pages/sales/salesQuotation/edit.vue | 547
src/api/oa/finReimbursement.js | 71
src/pages/oa/_utils/approvalTemplateType.js | 178
src/pages/productionManagement/productionDispatching/components/formDia.vue | 265
src/pages/inspectionUpload/upload.vue | 982 +
src/pages/oa/ApproveManage/approve-list/index.vue | 282
src/pages/productionDesign/bom/index.vue | 179
src/pages/indexItem.vue | 6
src/pages/qualityManagement/materialInspection/add.vue | 30
src/static/images/icon/kucunguanli.svg | 1
src/pages/oa/ReimburseManage/reimburse-detail/index.vue | 120
src/api/productionManagement/productionCosting.js | 18
src/static/images/icon/shengchanpaichan.svg | 1
src/pages/oa/HrManage/work-handover/index.vue | 12
src/api/collaborativeApproval/approvalProcess.js | 89
src/api/productionManagement/processRoute.js | 37
src/pages/productionManagement/productionAccounting/index.vue | 506
src/pages/oa/_utils/finReimbursementMappers.js | 1058 +
src/pages/equipmentManagement/repair/add.vue | 57
src/pages/productionDesign/basicParameters/edit.vue | 290
src/pages/qualityManagement/materialInspection/detail.vue | 3
src/pages/sales/salesQuotation/detail.vue | 54
src/pages/qualityManagement/processInspection/add.vue | 29
src/api/fileManagement/bookshelf.js | 129
src/pages/oa/HrManage/staff-archive/index.vue | 18
src/pages/index.vue | 448
src/pages/oa/_utils/oaPageRegistry.js | 256
src/pages/equipmentManagement/upkeep/fileList.vue | 133
src/pages/oa/ReimburseManage/travel-reimburse/index.vue | 11
src/pages/oa/_utils/approvalModuleListSearch.js | 456
src/pages/works.vue | 303
src/pages/oa/ReimburseManage/reimburse-detail/reimburse-detail.scss | 344
src/static/images/icon/jieyuedengji.svg | 1
src/pages/oa/_utils/oaStorage.js | 26
src/store/modules/user.ts | 72
src/pages/productionManagement/productionDispatching/index.vue | 421
src/pages/fileManagement/borrow/edit.vue | 333
src/api/basicData/storageAttachment.js | 29
src/pages/oa/ApproveManage/approve-list/apply.vue | 1274 ++
src/pages/oa/_utils/reimburseApproveBridge.js | 99
src/api/basic/enum.js | 9
src/pages/inspectionUpload/attachment.vue | 485
src/pages/productionDesign/basicParameters/index.vue | 245
src/pages/oa/_components/OaListPage.vue | 182
src/api/procurementManagement/procurementLedger.js | 10
src/api/productionManagement/productionOrder.js | 78
src/api/productionManagement/productionPlan.js | 29
src/pages/productionManagement/processRoute/items.vue | 554
src/pages/qualityManagement/finalInspection/add.vue | 15
src/pages/procurementManagement/procurementLedger/detail.vue | 52
src/pages/productionManagement/processStatistics/index.vue | 370
src/pages/qualityManagement/processInspection/index.vue | 19
src/pages/oa/_utils/userPickerUtils.js | 53
src/pages/oa/ReimburseManage/_components/ReimburseExpenseDetailSheet.vue | 315
src/api/oa/approvalInstance.js | 52
src/pages/oa/ApproveManage/approve-list/detail.vue | 174
src/pages/oa/HrManage/regular-apply/index.vue | 12
src/pages/equipmentManagement/upkeep/maintain.vue | 97
src/pages/oa/ApproveManage/approve-template/index.vue | 322
src/pages/qualityManagement/finalInspection/detail.vue | 3
src/pages/oa/ContractManage/sale-contract/index.vue | 26
src/static/images/icon/jichucanshu.svg | 1
src/pages/oa/_utils/approvalFormField.js | 372
src/pages/fileManagement/borrow/index.vue | 262
src/pages/oa/ReimburseManage/_utils/finReimbursementDetailExtras.js | 159
src/pages/qualityManagement/finalInspection/index.vue | 19
src/pages/cooperativeOffice/collaborativeApproval/detail.vue | 1113 -
src/pages/oa/_components/ApprovalInstanceListPage.vue | 353
src/pages/productionDesign/bom/BomStructureItem.vue | 256
src/static/images/icon/shengchandingdan.svg | 1
src/static/images/icon/shengchanzhuisu.svg | 1
src/pages/oa/_utils/approveListUtils.js | 358
src/api/productionManagement/productProcessRoute.js | 11
src/pages/oa/HrManage/resign-apply/index.vue | 18
src/pages/productionDesign/processManagement/edit.vue | 236
src/pages/login.vue | 60
src/pages/oa/ContractManage/purchase-contract/index.vue | 26
src/pages/oa/HrManage/transfer-apply/index.vue | 12
src/pages/productionManagement/productionReport/index.vue | 375
src/api/productionManagement/productionProductMain.js | 29
src/pages/productionDesign/bom/structure.vue | 100
src/pages/oa/ApproveManage/approve-list/approve.vue | 180
src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue | 426
src/pages/inventoryManagement/stockManagement/Record.vue | 443
src/pages/equipmentManagement/upkeep/index.vue | 17
src/config/oaPaths.js | 47
src/pages.json | 352
src/pages/oa/_components/OaUserSearchPicker.vue | 261
src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js | 440
src/static/images/icon/shengchanjihua.svg | 1
src/pages/oa/_utils/oaUi.js | 13
src/static/images/icon/shengchanshikuang.svg | 1
src/pages/oa/_utils/approvalModuleApplyExtras.js | 293
src/api/fileManagement/borrow.js | 47
src/pages/oa/HrManage/post-manage/index.vue | 18
src/static/images/icon/baogongtaizhang.svg | 1
src/pages/oa/ReimburseManage/reimburse-form/reimburse-form.scss | 354
src/pages/message.vue | 2
src/pages/oa/_components/ApprovalModuleSearchPopup.vue | 268
src/manifest.json | 1
src/components/CommonUpload.vue | 164
src/pages/oa/ApproveManage/approve-template/detail.vue | 419
src/pages/oa/NoticeAnnouncement/notice-manage/index.vue | 18
src/api/oa/approvalTemplate.js | 56
src/pages/oa/AttendManage/overtime-apply/index.vue | 12
src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue | 413
src/pages/equipmentManagement/upkeep/add.vue | 767
src/pages/oa/ApproveManage/approve-list/template-select.vue | 378
src/pages/productionManagement/processRoute/index.vue | 287
src/utils/versionUpgrade.js | 6
src/pages/inventoryManagement/stockManagement/index.vue | 133
src/pages/oa/AttendManage/leave-apply/index.vue | 12
src/api/fileManagement/document.js | 189
src/pages/oa/_utils/approvalModuleRegistry.js | 189
src/pages/sales/salesQuotation/index.vue | 118
src/static/images/icon/gongxuguanli.svg | 1
src/pages/inspectionUpload/components/formDia.vue | 228
/dev/null | 134
src/api/system/post.js | 10
src/static/images/icon/gongyiluxian.svg | 1
src/pages/oa/_components/FinReimbursementListPage.vue | 362
src/pages/oa/ReimburseManage/cost-reimburse/index.vue | 11
src/pages/oa/ReimburseManage/reimburse-form/index.vue | 564
src/pages/productionManagement/productionOrder/pickingDetail.vue | 350
src/App.vue | 5
src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js | 120
166 files changed, 31,030 insertions(+), 5,594 deletions(-)
diff --git a/src/App.vue b/src/App.vue
index a28785c..d427b1e 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -16,12 +16,11 @@
showSplash.value = false;
}, 5000);
- // 鍒濆鍖栨帹閫佹湇鍔�
+ // 鍒濆鍖栨帹閫佹湇鍔�,鏆傛椂娉ㄩ噴锛屽鎴烽渶瑕佹墦寮�
// initPushService();
});
// 鍒濆鍖栨帹閫佹湇鍔★紙uni-push 1.0锛�
const initPushService = () => {
- return;
// #ifdef APP-PLUS
console.log("寮�濮嬪垵濮嬪寲鎺ㄩ�佹湇鍔★紙uni-push 1.0锛�");
if (typeof plus !== "undefined" && plus.push) {
@@ -109,4 +108,4 @@
<style lang="scss">
@import "uview-plus/index.scss";
@import "@/static/scss/index.scss";
-</style>
+</style>
\ No newline at end of file
diff --git a/src/api/basic/enum.js b/src/api/basic/enum.js
new file mode 100644
index 0000000..982123f
--- /dev/null
+++ b/src/api/basic/enum.js
@@ -0,0 +1,9 @@
+import request from "@/utils/request";
+
+/** 瀹℃壒妯℃澘绫诲瀷鏋氫妇 GET /basic/enum/TypeEnums */
+export function getTypeEnums() {
+ return request({
+ url: "/basic/enum/TypeEnums",
+ method: "get",
+ });
+}
diff --git a/src/api/basicData/parameterMaintenance.js b/src/api/basicData/parameterMaintenance.js
new file mode 100644
index 0000000..387c2e3
--- /dev/null
+++ b/src/api/basicData/parameterMaintenance.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍩虹鍙傛暟鍒楄〃
+export function getBaseParamList(query) {
+ return request({
+ url: "/technologyParam/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鍩虹鍙傛暟
+export function addBaseParam(data) {
+ return request({
+ url: "/technologyParam/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缂栬緫鍩虹鍙傛暟
+export function editBaseParam(data) {
+ return request({
+ url: "/technologyParam/edit",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鍩虹鍙傛暟
+export function removeBaseParam(id) {
+ return request({
+ url: "/technologyParam/remove/" + id,
+ method: "delete",
+ });
+}
diff --git a/src/api/basicData/storageAttachment.js b/src/api/basicData/storageAttachment.js
new file mode 100644
index 0000000..3e241f6
--- /dev/null
+++ b/src/api/basicData/storageAttachment.js
@@ -0,0 +1,29 @@
+// 闄勪欢椤甸潰鎺ュ彛
+import request from '@/utils/request'
+
+// 闄勪欢鏌ヨ
+export function attachmentList(query) {
+ return request({
+ url: '/storageAttachment/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 闄勪欢鏂板
+export function createAttachment(data) {
+ return request({
+ url: '/storageAttachment/add',
+ method: 'post',
+ data
+ })
+}
+
+// 闄勪欢鍒犻櫎
+export function deleteAttachment(data) {
+ return request({
+ url: '/storageAttachment/delete',
+ method: 'delete',
+ data
+ })
+}
diff --git a/src/api/collaborativeApproval/approvalProcess.js b/src/api/collaborativeApproval/approvalProcess.js
index 47cf3ec..0d2b129 100644
--- a/src/api/collaborativeApproval/approvalProcess.js
+++ b/src/api/collaborativeApproval/approvalProcess.js
@@ -2,63 +2,72 @@
import request from "@/utils/request";
export function approveProcessListPage(query) {
- return request({
- url: '/approveProcess/list',
- method: 'get',
- params: query,
- })
+ return request({
+ url: "/approveProcess/list",
+ method: "get",
+ params: query,
+ });
}
export function getDept(query) {
- return request({
- url: '/approveProcess/getDept',
- method: 'get',
- params: query,
- })
+ return request({
+ url: "/approveProcess/getDept",
+ method: "get",
+ params: query,
+ });
}
export function approveProcessGetInfo(query) {
- return request({
- url: '/approveProcess/get',
- method: 'get',
- params: query,
- })
+ return request({
+ url: "/approveProcess/get",
+ method: "get",
+ params: query,
+ });
}
// 鏂板瀹℃壒娴佺▼
export function approveProcessAdd(query) {
- return request({
- url: '/approveProcess/add',
- method: 'post',
- data: query,
- })
+ return request({
+ url: "/approveProcess/add",
+ method: "post",
+ data: query,
+ });
}
// 淇敼瀹℃壒娴佺▼
export function approveProcessUpdate(query) {
- return request({
- url: '/approveProcess/update',
- method: 'post',
- data: query,
- })
+ return request({
+ url: "/approveProcess/update",
+ method: "post",
+ data: query,
+ });
}
// 鎻愪氦瀹℃壒
export function updateApproveNode(query) {
- return request({
- url: '/approveNode/updateApproveNode',
- method: 'post',
- data: query,
- })
+ return request({
+ url: "/approveNode/updateApproveNode",
+ method: "post",
+ data: query,
+ });
}
// 鍒犻櫎瀹℃壒娴佺▼
export function approveProcessDelete(query) {
- return request({
- url: '/approveProcess/deleteIds',
- method: 'delete',
- data: query,
- })
+ return request({
+ url: "/approveProcess/deleteIds",
+ method: "delete",
+ data: query,
+ });
}
// 鏌ヨ瀹℃壒娴佺▼
export function approveProcessDetails(query) {
- return request({
- url: '/approveNode/details/' + query,
- method: 'get',
- })
-}
\ No newline at end of file
+ return request({
+ url: "/approveNode/details/" + query,
+ method: "get",
+ });
+}
+
+// 瀹℃壒璇︽儏
+export function getDeliveryDetailByShippingNo(query) {
+ return request({
+ url: "/shippingInfo/getDateilByShippingNo",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/fileManagement/bookshelf.js b/src/api/fileManagement/bookshelf.js
new file mode 100644
index 0000000..eb3a88f
--- /dev/null
+++ b/src/api/fileManagement/bookshelf.js
@@ -0,0 +1,129 @@
+import request from "@/utils/request";
+
+/**
+ * 涔︽灦绠$悊鐩稿叧API鎺ュ彛
+ * 鍖呭惈浠撳簱绠$悊銆佽揣鏋剁鐞嗐�佸浘涔︾鐞嗙瓑鍔熻兘鐨勬帴鍙�
+ */
+
+/**
+ * 鑾峰彇浠撳簱鍒楄〃
+ * @description 鑾峰彇鎵�鏈変粨搴撶殑鍩烘湰淇℃伅鍒楄〃
+ * @returns {Promise} 杩斿洖浠撳簱鍒楄〃鏁版嵁
+ */
+export function getWarehouseList() {
+ return request({
+ url: "/warehouse/tree",
+ method: "get",
+ });
+}
+
+/**
+ * 鏂板浠撳簱
+ * @description 鍒涘缓鏂扮殑浠撳簱璁板綍
+ * @param {Object} data 浠撳簱淇℃伅瀵硅薄锛屽寘鍚粨搴撳悕绉扮瓑瀛楁
+ * @returns {Promise} 杩斿洖鏂板缁撴灉
+ */
+export function addWarehouse(data) {
+ return request({
+ url: "/warehouse/add",
+ method: "post",
+ data,
+ });
+}
+
+/**
+ * 鏇存柊浠撳簱淇℃伅
+ * @description 淇敼鐜版湁浠撳簱鐨勫熀鏈俊鎭�
+ * @param {Object} data 浠撳簱淇℃伅瀵硅薄锛屽繀椤诲寘鍚粨搴揑D
+ * @returns {Promise} 杩斿洖鏇存柊缁撴灉
+ */
+export function updateWarehouse(data) {
+ return request({
+ url: "/warehouse/update",
+ method: "put",
+ data,
+ });
+}
+
+/**
+ * 鍒犻櫎浠撳簱
+ * @description 鏍规嵁浠撳簱ID鍒犻櫎鎸囧畾鐨勪粨搴撹褰�
+ * @param {string|number} id 浠撳簱ID
+ * @returns {Promise} 杩斿洖鍒犻櫎缁撴灉
+ */
+export function deleteWarehouse(data) {
+ return request({
+ url: `/warehouse/delete/`,
+ method: "delete",
+ data,
+ });
+}
+
+/**
+ * 鑾峰彇璐ф灦鍒楄〃
+ * @description 鏍规嵁浠撳簱ID鑾峰彇璇ヤ粨搴撲笅鐨勬墍鏈夎揣鏋朵俊鎭�
+ * @param {string|number} warehouseId 浠撳簱ID
+ * @returns {Promise} 杩斿洖璐ф灦鍒楄〃鏁版嵁
+ */
+export function getShelfList(warehouseId) {
+ return request({
+ url: `/shelf/list/${warehouseId}`,
+ method: "get",
+ });
+}
+
+/**
+ * 鏂板璐ф灦
+ * @description 鍦ㄦ寚瀹氫粨搴撲笅鍒涘缓鏂扮殑璐ф灦璁板綍
+ * @param {Object} data 璐ф灦淇℃伅瀵硅薄锛屽寘鍚揣鏋跺悕绉般�佸眰鏁般�佸垪鏁扮瓑瀛楁
+ * @returns {Promise} 杩斿洖鏂板缁撴灉
+ */
+export function addShelf(data) {
+ return request({
+ url: "/warehouse/goodsShelves/add",
+ method: "post",
+ data,
+ });
+}
+
+/**
+ * 鏇存柊璐ф灦淇℃伅
+ * @description 淇敼鐜版湁璐ф灦鐨勫熀鏈俊鎭�
+ * @param {Object} data 璐ф灦淇℃伅瀵硅薄锛屽繀椤诲寘鍚揣鏋禝D
+ * @returns {Promise} 杩斿洖鏇存柊缁撴灉
+ */
+export function updateShelf(data) {
+ return request({
+ url: "/warehouse/goodsShelves/update",
+ method: "put",
+ data,
+ });
+}
+
+/**
+ * 鍒犻櫎璐ф灦
+ * @description 鏍规嵁璐ф灦ID鍒犻櫎鎸囧畾鐨勮揣鏋惰褰曪紝鍚庣瑕佹眰浼犲叆 ID 鏁扮粍锛堟敮鎸佹壒閲忥級
+ * @param {Array<string|number>} data 璐ф灦ID鏁扮粍
+ * @returns {Promise} 杩斿洖鍒犻櫎缁撴灉
+ */
+export function deleteShelf(data) {
+ return request({
+ url: `/warehouse/goodsShelves/delete/`,
+ method: "delete",
+ data,
+ });
+}
+
+/**
+ * 鑾峰彇浠撳簱缁撴瀯
+ * @description 鑾峰彇鎸囧畾浠撳簱鐨勫畬鏁寸粨鏋勪俊鎭紝鍖呮嫭璐ф灦銆佸眰鏁般�佸垪鏁扮瓑
+ * @param {string|number} warehouseId 浠撳簱ID
+ * @returns {Promise} 杩斿洖浠撳簱鐨勫畬鏁寸粨鏋勬暟鎹�
+ */
+export function getWarehouseStructure(data) {
+ return request({
+ url: `/warehouse/goodsShelvesRowcol/list`,
+ method: "get",
+ params: data,
+ });
+}
diff --git a/src/api/fileManagement/borrow.js b/src/api/fileManagement/borrow.js
new file mode 100644
index 0000000..1f4c72c
--- /dev/null
+++ b/src/api/fileManagement/borrow.js
@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+
+// 鏂囨。鍊熼槄绠$悊鐩稿叧鎺ュ彛
+
+// 鑾峰彇鏂囨。鍒楄〃锛堢敤浜庡�熼槄涔︾睄閫夋嫨锛�
+export function getDocumentList() {
+ return request({
+ url: "/documentation/list",
+ method: "get",
+ });
+}
+
+// 鍊熼槄鍒嗛〉鏌ヨ
+export function getBorrowList(params) {
+ return request({
+ url: "/documentationBorrowManagement/listPage",
+ method: "get",
+ params: params,
+ });
+}
+
+// 鏂板鍊熼槄
+export function addBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鏇存柊鍊熼槄
+export function updateBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鍊熼槄
+export function deleteBorrow(ids) {
+ return request({
+ url: "/documentationBorrowManagement/delete",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/api/fileManagement/document.js b/src/api/fileManagement/document.js
new file mode 100644
index 0000000..f3d5f4f
--- /dev/null
+++ b/src/api/fileManagement/document.js
@@ -0,0 +1,189 @@
+import request from "@/utils/request";
+
+// 鑾峰彇鍒嗙被鏍�
+export function getCategoryTree() {
+ return request({
+ url: "/warehouse/documentClassification/getList",
+ method: "get",
+ });
+}
+
+// 鏂板鍒嗙被
+export function addCategory(data) {
+ return request({
+ url: "/warehouse/documentClassification/add",
+ method: "post",
+ data: {
+ category: data.category,
+ parentId: data.parentId,
+ },
+ });
+}
+
+// 淇敼鍒嗙被
+export function updateCategory(data) {
+ return request({
+ url: "/warehouse/documentClassification/update",
+ method: "put",
+ data: {
+ id: data.id,
+ category: data.category,
+ },
+ });
+}
+
+// 鍒犻櫎鍒嗙被
+export function deleteCategory(ids) {
+ return request({
+ url: "/warehouse/documentClassification/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鑾峰彇鏂囨。鍒楄〃锛堝垎椤碉級
+export function getDocumentList(query) {
+ return request({
+ url: "/documentation/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鏂囨。
+export function addDocument(data) {
+ return request({
+ url: "/documentation/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鏂囨。
+export function updateDocument(data) {
+ return request({
+ url: "/documentation/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鏂囨。
+export function deleteDocument(ids) {
+ return request({
+ url: "/documentation/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鑾峰彇鏂囨。璇︽儏
+export function getDocumentDetail(id) {
+ return request({
+ url: "/document/" + id,
+ method: "get",
+ });
+}
+
+// 鎼滅储鏂囨。
+export function searchDocument(query) {
+ return request({
+ url: "/document/search",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鑾峰彇浠撳簱缁撴瀯
+export function getWarehouseStructure() {
+ return request({
+ url: "/document/warehouse/structure",
+ method: "get",
+ });
+}
+
+// 闄勪欢绠$悊鐩稿叧鎺ュ彛
+// 娣诲姞闄勪欢
+export function addDocumentationFile(data) {
+ return request({
+ url: "/documentation/documentationFile/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鑾峰彇闄勪欢鍒楄〃
+export function getDocumentationFileList(params) {
+ return request({
+ url: "/documentation/documentationFile/listPage",
+ method: "get",
+ params: params,
+ });
+}
+
+// 鍒犻櫎闄勪欢
+export function deleteDocumentationFile(ids) {
+ return request({
+ url: "/documentation/documentationFile/del",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鏂囨。鍊熼槄绠$悊鐩稿叧鎺ュ彛
+export function getBorrowList(params) {
+ return request({
+ url: "/documentationBorrowManagement/listPage",
+ method: "get",
+ params: params,
+ });
+}
+
+export function addBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+
+export function updateBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/update",
+ method: "put",
+ data: data,
+ });
+}
+
+export function deleteBorrow(ids) {
+ return request({
+ url: "/documentationBorrowManagement/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 缁熻鐩稿叧鎺ュ彛
+// 鑾峰彇鎬讳綋缁熻鏁版嵁
+export function getDocumentationOverview() {
+ return request({
+ url: "/documentation/overview",
+ method: "get",
+ });
+}
+
+// 鑾峰彇鍒嗙被缁熻鏁版嵁
+export function getDocumentationCategoryStats() {
+ return request({
+ url: "/documentation/category",
+ method: "get",
+ });
+}
+
+// 鑾峰彇鐘舵�佺粺璁℃暟鎹�
+export function getDocumentationStatusStats() {
+ return request({
+ url: "/documentation/status",
+ method: "get",
+ });
+}
diff --git a/src/api/fileManagement/return.js b/src/api/fileManagement/return.js
new file mode 100644
index 0000000..9021ac9
--- /dev/null
+++ b/src/api/fileManagement/return.js
@@ -0,0 +1,61 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ褰掕繕璁板綍
+export function getReturnListPage(query) {
+ return request({
+ url: "/documentationBorrowManagement/listPageReturn",
+ method: "get",
+ params: query,
+ });
+}
+
+// 褰掕繕鎿嶄綔
+export function returnDocument(data) {
+ return request({
+ url: "/documentationBorrowManagement/revent",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎褰掕繕璁板綍
+export function deleteReturn(ids) {
+ return request({
+ url: "/documentationBorrowManagement/reventDelete",
+ method: "delete",
+ data: ids,
+ });
+}
+//鏍规嵁涔︾睄id鏌ヨ鍊熼槄璁板綍
+export function getBorrowListByDocumentationId(id) {
+ return request({
+ url: "/documentationBorrowManagement/getByDocumentationId/"+id,
+ method: "get"
+ });
+}
+
+// 鏇存柊鍊熼槄璁板綍
+export function updateBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 褰掕繕鏇存柊
+export function reventUpdate(data) {
+ return request({
+ url: "/documentationBorrowManagement/reventUpdate",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鑾峰彇鏂囨。鍒楄〃
+export function getDocumentList() {
+ return request({
+ url: "/documentationBorrowManagement/list",
+ method: "get",
+ });
+}
diff --git a/src/api/fileManagement/statistics.js b/src/api/fileManagement/statistics.js
new file mode 100644
index 0000000..d77375c
--- /dev/null
+++ b/src/api/fileManagement/statistics.js
@@ -0,0 +1,75 @@
+import request from "@/utils/request";
+
+// 鑾峰彇妗f鎬讳綋缁熻
+export function getDocumentStatistics() {
+ return request({
+ url: "/fileManagement/statistics/overview",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鍒嗙被缁熻
+export function getCategoryStatistics() {
+ return request({
+ url: "/fileManagement/statistics/category",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鐘舵�佺粺璁�
+export function getStatusStatistics() {
+ return request({
+ url: "/fileManagement/statistics/status",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鍊熼槄缁熻
+export function getBorrowStatistics() {
+ return request({
+ url: "/fileManagement/statistics/borrow",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f骞村害缁熻
+export function getYearStatistics() {
+ return request({
+ url: "/fileManagement/statistics/year",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f浣嶇疆缁熻
+export function getLocationStatistics() {
+ return request({
+ url: "/fileManagement/statistics/location",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f瓒嬪娍缁熻
+export function getTrendStatistics(params) {
+ return request({
+ url: "/fileManagement/statistics/trend",
+ method: "get",
+ params: params,
+ });
+}
+
+// 鑾峰彇妗f鍊熼槄鎺掕
+export function getBorrowRanking() {
+ return request({
+ url: "/fileManagement/statistics/borrowRanking",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鍒嗙被璇︽儏缁熻
+export function getCategoryDetailStatistics(categoryId) {
+ return request({
+ url: `/fileManagement/statistics/categoryDetail/${categoryId}`,
+ method: "get",
+ });
+}
+
diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
index dfa5f46..328657c 100644
--- a/src/api/inventoryManagement/stockInventory.js
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -1,61 +1,78 @@
import request from "@/utils/request";
// 鍒嗛〉鏌ヨ搴撳瓨璁板綍鍒楄〃
-export const getStockInventoryListPage = (params) => {
- return request({
- url: "/stockInventory/pagestockInventory",
- method: "get",
- params,
- });
+export const getStockInventoryListPage = params => {
+ return request({
+ url: "/stockInventory/pagestockInventory",
+ method: "get",
+ params,
+ });
+};
+
+// 鍒嗛〉鏌ヨ鑱斿悎搴撳瓨璁板綍鍒楄〃锛堝寘鍚晢鍝佷俊鎭級
+export const getStockInventoryListPageCombined = params => {
+ return request({
+ url: "/stockInventory/pageListCombinedStockInventory",
+ method: "get",
+ params,
+ });
};
// 鍒涘缓搴撳瓨璁板綍
-export const createStockInventory = (params) => {
- return request({
- url: "/stockInventory/addstockInventory",
- method: "post",
- data: params,
- });
+export const createStockInventory = params => {
+ return request({
+ url: "/stockInventory/addstockInventory",
+ method: "post",
+ data: params,
+ });
};
// 鍑忓皯搴撳瓨璁板綍
-export const subtractStockInventory = (params) => {
- return request({
- url: "/stockInventory/subtractStockInventory",
- method: "post",
- data: params,
- });
+export const subtractStockInventory = params => {
+ return request({
+ url: "/stockInventory/subtractStockInventory",
+ method: "post",
+ data: params,
+ });
};
-export const getStockInventoryReportList = (params) => {
- return request({
- url: "/stockInventory/stockInventoryPage",
- method: "get",
- params,
- });
+export const getStockInventoryReportList = params => {
+ return request({
+ url: "/stockInventory/stockInventoryPage",
+ method: "get",
+ params,
+ });
};
-export const getStockInventoryInAndOutReportList = (params) => {
- return request({
- url: "/stockInventory/stockInAndOutRecord",
- method: "get",
- params,
- });
+export const getStockInventoryInAndOutReportList = params => {
+ return request({
+ url: "/stockInventory/stockInAndOutRecord",
+ method: "get",
+ params,
+ });
};
// 鍐荤粨搴撳瓨璁板綍
-export const frozenStockInventory = (params) => {
- return request({
- url: "/stockInventory/frozenStock",
- method: "post",
- data: params,
- });
+export const frozenStockInventory = params => {
+ return request({
+ url: "/stockInventory/frozenStock",
+ method: "post",
+ data: params,
+ });
};
// 瑙e喕搴撳瓨璁板綍
-export const thawStockInventory = (params) => {
- return request({
- url: "/stockInventory/thawStock",
- method: "post",
- data: params,
- });
+export const thawStockInventory = params => {
+ return request({
+ url: "/stockInventory/thawStock",
+ method: "post",
+ data: params,
+ });
+};
+
+export const getStockInventoryByModelId = productModelId => {
+ return request({
+ url: "/stockInventory/getByModelId",
+ method: "get",
+ params: { productModelId },
+ });
};
diff --git a/src/api/login.js b/src/api/login.js
index 29a149d..561b98c 100644
--- a/src/api/login.js
+++ b/src/api/login.js
@@ -1,81 +1,82 @@
-import request from '@/utils/request'
+import request from "@/utils/request";
// 鐧诲綍鏂规硶
-export function loginCheckFactory(username, password) {
+export function loginCheckFactory(username, password, factoryId) {
const data = {
username,
password,
- }
+ factoryId,
+ };
return request({
- url: '/loginCheckFactory',
+ url: "/loginCheckFactory",
headers: {
- isToken: false
+ isToken: false,
+ repeatSubmit: false,
},
- method: 'post',
- data: data
- })
+ method: "post",
+ data: data,
+ });
}
// 鑾峰彇鐢ㄦ埛璇︾粏淇℃伅
export function getInfo() {
return request({
- url: '/getInfo',
- method: 'get'
- })
+ url: "/getInfo",
+ method: "get",
+ });
}
// 閫�鍑烘柟娉�
export function logout() {
return request({
- url: '/logout',
- method: 'post'
- })
+ url: "/logout",
+ method: "post",
+ });
}
// 鑾峰彇鍏徃鍒楄〃
export function userLoginFacotryList(params) {
return request({
- url: '/userLoginFacotryList',
- method: 'get',
- params: params
- })
+ url: "/userLoginFacotryList",
+ method: "get",
+ params: params,
+ });
}
// 鑾峰彇鏈繃鏈熷叕鍛婃暟閲�
export function noticesList(params) {
return request({
- url: '/collaborativeApproval/notice/page',
- method: 'get',
- params: params
- })
+ url: "/collaborativeApproval/notice/page",
+ method: "get",
+ params: params,
+ });
}
// 鍙戦�佸鎴风鎺ㄩ�佹爣璇嗗埌鏈嶅姟鍣�
export function updateClientId(data) {
return request({
- url: '/system/client/addOrUpdateClientId',
- method: 'post',
- data: data
- })
+ url: "/system/client/addOrUpdateClientId",
+ method: "post",
+ data: data,
+ });
}
-
// 鏌ヨ鍏憡鍒楄〃
export function listNotice(query) {
return request({
- url: '/system/notice/list',
- method: 'get',
- params: query
- })
+ url: "/system/notice/list",
+ method: "get",
+ params: query,
+ });
}
// 鑾峰彇鏈娑堟伅鏁伴噺
export function getNoticeCount(consigneeId) {
return request({
- url: '/system/notice/getCount',
- method: 'get',
- params: { consigneeId }
- })
+ url: "/system/notice/getCount",
+ method: "get",
+ params: { consigneeId },
+ });
}
// 鏍囪娑堟伅涓哄凡璇�
diff --git a/src/api/oa/approvalInstance.js b/src/api/oa/approvalInstance.js
new file mode 100644
index 0000000..fa60c77
--- /dev/null
+++ b/src/api/oa/approvalInstance.js
@@ -0,0 +1,52 @@
+import request from "@/utils/request";
+
+/** 瀹℃壒瀹炰緥鍒嗛〉鏌ヨ GET /approvalInstance/listPage */
+export function listApprovalInstancePage(params) {
+ return request({
+ url: "/approvalInstance/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板缓瀹℃壒瀹炰緥 POST /approvalInstance/save */
+export function saveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/save",
+ method: "post",
+ data: { approvalInstanceDto },
+ });
+}
+
+/**
+ * 淇敼瀹℃壒瀹炰緥 PUT /approvalInstance/update
+ * @param {Object} approvalInstanceDto 瀹℃壒瀹炰緥锛堥渶鍚� id锛屽叾浣欏瓧娈垫寜涓氬姟淇濈暀/鏇存柊锛�
+ */
+export function updateApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/update",
+ method: "put",
+ data: { approvalInstanceDto },
+ });
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛塒OST /approvalInstance/approve */
+export function approveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/approve",
+ method: "post",
+ data: { approvalInstanceDto },
+ });
+}
+
+/** 鍒犻櫎瀹℃壒瀹炰緥 DELETE /approvalInstance/delete */
+export function deleteApprovalInstance(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter(
+ id => id != null && id !== ""
+ );
+ return request({
+ url: "/approvalInstance/delete",
+ method: "delete",
+ data: idList,
+ });
+}
diff --git a/src/api/oa/approvalTemplate.js b/src/api/oa/approvalTemplate.js
new file mode 100644
index 0000000..ab13897
--- /dev/null
+++ b/src/api/oa/approvalTemplate.js
@@ -0,0 +1,56 @@
+import request from "@/utils/request";
+
+/**
+ * 鎸� templateType 鏌ヨ宸插惎鐢ㄦā鏉垮垪琛紙闈� businessType锛�
+ * GET /approvalTemplate/list/{templateType} 渚嬶細list/1 = 鑷畾涔夊凡鍚敤
+ */
+export function listApprovalTemplateByType(templateType) {
+ return request({
+ url: `/approvalTemplate/list/${templateType}`,
+ method: "get",
+ });
+}
+
+/** 瀹℃壒妯℃澘鍒嗛〉鏌ヨ */
+export function listApprovalTemplatePage(params) {
+ return request({
+ url: "/approvalTemplate/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 瀹℃壒妯℃澘璇︽儏 */
+export function getApprovalTemplateDetail(id) {
+ return request({
+ url: `/approvalTemplate/detail/${id}`,
+ method: "get",
+ });
+}
+
+/** 鏂板瀹℃壒妯℃澘 */
+export function addApprovalTemplate(data) {
+ return request({
+ url: "/approvalTemplate/add",
+ method: "post",
+ data,
+ });
+}
+
+/** 淇敼瀹℃壒妯℃澘 */
+export function updateApprovalTemplate(data) {
+ return request({
+ url: "/approvalTemplate/update",
+ method: "put",
+ data,
+ });
+}
+
+/** 鍒犻櫎瀹℃壒妯℃澘锛堜紶 ID 鏁扮粍锛� */
+export function deleteApprovalTemplate(ids) {
+ return request({
+ url: "/approvalTemplate/delete",
+ method: "post",
+ data: ids,
+ });
+}
diff --git a/src/api/oa/finReimbursement.js b/src/api/oa/finReimbursement.js
new file mode 100644
index 0000000..84c3560
--- /dev/null
+++ b/src/api/oa/finReimbursement.js
@@ -0,0 +1,71 @@
+import request from "@/utils/request";
+
+/** 鍒嗛〉鏌ヨ璐㈠姟鎶ラ攢 GET /finReimbursement/listPage */
+export function listFinReimbursementPage(params) {
+ return request({
+ url: "/finReimbursement/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 璇︽儏 query锛歋pring 缁戝畾 finReimbursementDto.id锛屽嬁鐢� finReimbursementDto[id] */
+function buildFinReimbursementDetailParams(idOrDto) {
+ const raw =
+ typeof idOrDto === "object" && idOrDto !== null
+ ? idOrDto.id ?? idOrDto.reimbursementId
+ : idOrDto;
+ return {
+ "finReimbursementDto.id": raw,
+ id: raw,
+ };
+}
+
+/** 鏌ヨ璐㈠姟鎶ラ攢璇︽儏 GET /finReimbursement/detail */
+export function getFinReimbursementDetail(idOrDto) {
+ return request({
+ url: "/finReimbursement/detail",
+ method: "get",
+ params: buildFinReimbursementDetailParams(idOrDto),
+ });
+}
+
+/** 鏂板璐㈠姟鎶ラ攢 POST /finReimbursement/save */
+export function saveFinReimbursement(finReimbursementDto) {
+ return request({
+ url: "/finReimbursement/save",
+ method: "post",
+ data: finReimbursementDto,
+ });
+}
+
+/** 淇敼璐㈠姟鎶ラ攢 POST /finReimbursement/update */
+export function updateFinReimbursement(finReimbursementDto) {
+ return request({
+ url: "/finReimbursement/update",
+ method: "post",
+ data: finReimbursementDto,
+ });
+}
+
+/** 鍒犻櫎璐㈠姟鎶ラ攢 DELETE /finReimbursement/delete锛坆ody 涓� ID 鏁扮粍锛� */
+export function deleteFinReimbursement(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter(
+ (id) => id != null && id !== ""
+ );
+ return request({
+ url: "/finReimbursement/delete",
+ method: "delete",
+ data: idList,
+ });
+}
+
+/** 鏂板璧� save锛屼慨鏀硅蛋 update锛堜笌鎺ュ彛鏂囨。涓�鑷达級 */
+export function persistFinReimbursement(finReimbursementDto, isEdit = false) {
+ if (isEdit) {
+ return updateFinReimbursement(finReimbursementDto);
+ }
+ const payload = { ...finReimbursementDto };
+ delete payload.id;
+ return saveFinReimbursement(payload);
+}
diff --git a/src/api/procurementManagement/procurementLedger.js b/src/api/procurementManagement/procurementLedger.js
index 0a05d2e..5c0bb83 100644
--- a/src/api/procurementManagement/procurementLedger.js
+++ b/src/api/procurementManagement/procurementLedger.js
@@ -72,6 +72,16 @@
method: "get",
});
}
+
+// 鏌ヨ閲囪喘璇︽儏
+export function getPurchaseByCode(query) {
+ return request({
+ url: "/purchase/ledger/getPurchaseByCode",
+ method: "get",
+ params: query,
+ });
+}
+
export function approveProcessGetInfo(query) {
return request({
url: '/approveProcess/get',
diff --git a/src/api/productionManagement/bom.js b/src/api/productionManagement/bom.js
new file mode 100644
index 0000000..cb42000
--- /dev/null
+++ b/src/api/productionManagement/bom.js
@@ -0,0 +1,82 @@
+import request from "@/utils/request";
+
+// BOM 鍒楄〃鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/technologyBom/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板 BOM
+export function add(data) {
+ return request({
+ url: "/technologyBom/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼 BOM
+export function update(data) {
+ return request({
+ url: "/technologyBom/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎 BOM
+export function batchDelete(ids) {
+ return request({
+ url: "/technologyBom/batchDelete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 澶嶅埗 BOM
+export function copy(data) {
+ return request({
+ url: "/technologyBom/copy",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鑾峰彇浜у搧鍒楄〃 (鐢ㄤ簬鏂板BOM鏃堕�夋嫨浜у搧)
+export function getProductList(query) {
+ return request({
+ url: "/product/ledger/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// --- BOM 缁撴瀯鐩稿叧 ---
+
+// 鏍规嵁 BOM ID 鑾峰彇缁撴瀯鍒楄〃
+export function queryStructureList(bomId) {
+ return request({
+ url: "/technologyBomStructure/listByBomId/" + bomId,
+ method: "get",
+ });
+}
+
+// 淇濆瓨 BOM 缁撴瀯
+export function addStructure(data) {
+ return request({
+ url: "/technologyBomStructure/batchSave",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎 BOM 缁撴瀯椤�
+export function deleteStructure(id) {
+ return request({
+ url: "/technologyBomStructure/batchDelete/" + id,
+ method: "delete",
+ });
+}
diff --git a/src/api/productionManagement/processManagement.js b/src/api/productionManagement/processManagement.js
new file mode 100644
index 0000000..0f8dc06
--- /dev/null
+++ b/src/api/productionManagement/processManagement.js
@@ -0,0 +1,87 @@
+import request from "@/utils/request";
+
+export function getProcessList(query) {
+ return request({
+ url: "/technologyOperation/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function list() {
+ return request({
+ url: "/technologyOperation/list",
+ method: "get",
+ });
+}
+
+export function add(data) {
+ return request({
+ url: "/technologyOperation/add",
+ method: "post",
+ data: data,
+ });
+}
+
+export function update(data) {
+ return request({
+ url: "/technologyOperation/update",
+ method: "put",
+ data: data,
+ });
+}
+
+export function del(ids) {
+ return request({
+ url: "/technologyOperation/batchDelete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+export function getProcessParamList(params) {
+ return request({
+ url: "/technologyOperationParam/list",
+ method: "get",
+ params,
+ });
+}
+
+export function addProcessParam(data) {
+ return request({
+ url: "/technologyOperationParam/",
+ method: "post",
+ data: data,
+ });
+}
+
+export function editProcessParam(data) {
+ return request({
+ url: "/technologyOperationParam/",
+ method: "post",
+ data: data,
+ });
+}
+
+export function deleteProcessParam(id) {
+ return request({
+ url: `/technologyOperationParam/batchDelete/${id}`,
+ method: "delete",
+ });
+}
+
+export function getDeviceLedger(query) {
+ return request({
+ url: "/device/ledger/getDeviceLedger",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getBaseParamList(query) {
+ return request({
+ url: "/technologyParam/list",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/processRoute.js b/src/api/productionManagement/processRoute.js
new file mode 100644
index 0000000..bf49b11
--- /dev/null
+++ b/src/api/productionManagement/processRoute.js
@@ -0,0 +1,37 @@
+// 宸ヨ壓璺嚎鐩稿叧鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ宸ヨ壓璺嚎鍒楄〃
+export function listPage(query) {
+ return request({
+ url: "/technologyRouting/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ宸ヨ壓璺嚎椤圭洰鍒楄〃
+export function findProcessRouteItemList(query) {
+ return request({
+ url: "/technologyRoutingOperation/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鑾峰彇宸ュ簭鍙傛暟鍒楄〃
+export function getProcessParamList(query) {
+ return request({
+ url: "/technologyRoutingOperationParam/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨBOM缁撴瀯 (宸ヨ壓璺嚎)
+export function queryBomList(bomId) {
+ return request({
+ url: "/technologyBomStructure/listByBomId/" + bomId,
+ method: "get",
+ });
+}
diff --git a/src/api/productionManagement/productProcessRoute.js b/src/api/productionManagement/productProcessRoute.js
new file mode 100644
index 0000000..6584473
--- /dev/null
+++ b/src/api/productionManagement/productProcessRoute.js
@@ -0,0 +1,11 @@
+// 鐢熶骇鎶ュ伐椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鑾峰彇宸ュ簭鍙傛暟鍒楄〃-鐢熶骇璁㈠崟
+export function findProcessParamListOrder(query) {
+ return request({
+ url: `/productionOrderRoutingOperationParam/list`,
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionCosting.js b/src/api/productionManagement/productionCosting.js
index 8cc0251..ed969f6 100644
--- a/src/api/productionManagement/productionCosting.js
+++ b/src/api/productionManagement/productionCosting.js
@@ -8,4 +8,22 @@
method: "get",
params: query,
});
+}
+
+// 宸﹁竟琛ㄦ牸鐨勬帴鍙� (姹囨��)
+export function salesLedgerProductionAccountingList(query) {
+ return request({
+ url: "/productionAccount/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍙宠竟琛ㄦ牸鐨勬帴鍙� (鏄庣粏)
+export function salesLedgerProductionAccountingListProductionDetails(query) {
+ return request({
+ url: "/productionAccount/listProductionDetails",
+ method: "get",
+ params: query,
+ });
}
\ No newline at end of file
diff --git a/src/api/productionManagement/productionOrder.js b/src/api/productionManagement/productionOrder.js
index ab3dc06..3038550 100644
--- a/src/api/productionManagement/productionOrder.js
+++ b/src/api/productionManagement/productionOrder.js
@@ -1,19 +1,79 @@
// 鐢熶骇璁㈠崟椤甸潰鎺ュ彛
import request from "@/utils/request";
-// 鍒嗛〉鏌ヨ
-export function schedulingListPage(query) {
+// 鍒嗛〉鏌ヨ鐢熶骇璁㈠崟
+export function productOrderListPage(query) {
return request({
- url: "/salesLedger/scheduling/listPage",
+ url: "/productionOrder/page",
method: "get",
params: query,
});
}
-// 鐢熶骇娲惧伐
-export function productionDispatch(query) {
+
+// 鐢熶骇璁㈠崟婧簮璇︽儏
+export function getOrderDetail(npsNo) {
return request({
- url: "/salesLedger/scheduling/productionDispatch",
- method: "post",
- data: query,
+ url: "/productionOrder/ordeDetail",
+ method: "get",
+ params: { npsNo },
});
-}
\ No newline at end of file
+}
+
+// 鑾峰彇鐢熶骇璁㈠崟鏉ユ簮鏁版嵁
+export function getProductOrderSource(id) {
+ return request({
+ url: `/productionOrder/source/${id}`,
+ method: "get",
+ });
+}
+
+// 棰嗘枡璇︽儏鍒楄〃
+export function listMaterialPickingDetail(productionOrderId) {
+ return request({
+ url: "/productionOrderPick/detail/" + productionOrderId,
+ method: "get",
+ });
+}
+
+// 琛ユ枡璁板綍鍒楄〃
+export function listMaterialSupplementRecord(query) {
+ return request({
+ url: "/productionOrderPickRecord/feeding",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鑾峰彇棰嗘枡BOM淇℃伅 (鍙�夛紝澶囩敤)
+export function listMaterialPickingBom(productionOrderId) {
+ return request({
+ url: "/productionOrder/pick/" + productionOrderId,
+ method: "get",
+ });
+}
+
+// 鑾峰彇鐢熶骇璁㈠崟鍏宠仈鐨勫伐鑹鸿矾绾夸富淇℃伅
+export function getOrderProcessRouteMain(orderId) {
+ return request({
+ url: "/productionOrderRouting/listMain",
+ method: "get",
+ params: { orderId },
+ });
+}
+
+// 鏌ヨBOM缁撴瀯 (鐢熶骇璁㈠崟)
+export function queryOrderBomList(bomId) {
+ return request({
+ url: "/productionBomStructure/listByBomId/" + bomId,
+ method: "get",
+ });
+}
+
+// 鑾峰彇宸ュ簭鍙傛暟鍒楄〃 (鐢熶骇璁㈠崟)
+export function findProcessParamListOrder(query) {
+ return request({
+ url: "/productionOrderRoutingOperationParam/list",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionPlan.js b/src/api/productionManagement/productionPlan.js
new file mode 100644
index 0000000..ec9a1ae
--- /dev/null
+++ b/src/api/productionManagement/productionPlan.js
@@ -0,0 +1,29 @@
+// 涓荤敓浜ц鍒掓帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鍒楄〃
+export function productionPlanListPage(query) {
+ return request({
+ url: "/productionPlan/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鎷夊彇鏁版嵁
+export function loadProdData(query) {
+ return request({
+ url: "/productionPlan/loadProdData",
+ method: "get",
+ params: query,
+ });
+}
+
+// 姹囨�荤粺璁�
+export function summaryByProductType(query) {
+ return request({
+ url: "/productionPlan/summaryByProductType",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionProductMain.js b/src/api/productionManagement/productionProductMain.js
new file mode 100644
index 0000000..4242e1b
--- /dev/null
+++ b/src/api/productionManagement/productionProductMain.js
@@ -0,0 +1,29 @@
+// 鐢熶骇鎶ュ伐椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ鐢熶骇鎶ュ伐涓昏〃
+export function productionProductMainListPage(query) {
+ return request({
+ url: "/productionProductMain/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍒犻櫎鎶ュ伐
+export function productionReportDelete(query) {
+ return request({
+ url: "/productionProductMain/delete",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ鎶曞叆鍒楄〃
+export function productionProductInputListPage(query) {
+ return request({
+ url: "/productionProductInput/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionReporting.js b/src/api/productionManagement/productionReporting.js
index 33faadb..be352d5 100644
--- a/src/api/productionManagement/productionReporting.js
+++ b/src/api/productionManagement/productionReporting.js
@@ -20,9 +20,8 @@
// 鏍规嵁ID鑾峰彇宸ュ崟璇︽儏
export function getProductWorkOrderById(query) {
return request({
- url: "/productWorkOrder/getProductWorkOrderById",
+ url: "/productionOperationTask/" + query.id,
method: "get",
- params: query,
});
}
// 鐢熶骇鎶ュ伐
diff --git a/src/api/productionManagement/workOrder.js b/src/api/productionManagement/workOrder.js
index 7e8bd86..d3e0033 100644
--- a/src/api/productionManagement/workOrder.js
+++ b/src/api/productionManagement/workOrder.js
@@ -2,7 +2,7 @@
export function productWorkOrderPage(query) {
return request({
- url: "/productWorkOrder/page",
+ url: "/productionOperationTask/page",
method: "get",
params: query,
});
@@ -10,7 +10,7 @@
export function updateProductWorkOrder(data) {
return request({
- url: "/productWorkOrder/updateProductWorkOrder",
+ url: "/productionOperationTask/updateProductWorkOrder",
method: "post",
data: data,
});
@@ -24,12 +24,29 @@
});
}
+export function assignProductWorkOrder(data) {
+ return request({
+ url: "/productionOperationTask/assign",
+ method: "post",
+ data: data,
+ });
+}
+
// 涓嬭浇宸ュ崟娴佽浆鍗★紙杩斿洖鏂囦欢娴侊級
export function downProductWorkOrder(id) {
return request({
- url: "/productWorkOrder/down",
+ url: "/productionOperationTask/down",
method: "post",
data: { id },
responseType: "blob",
});
}
+
+// 鑾峰彇宸ュ簭缁熻鏁版嵁
+export function getOperationStatistics(query) {
+ return request({
+ url: "/productionOperationTask/getOperation",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/system/post.js b/src/api/system/post.js
new file mode 100644
index 0000000..c3f70c2
--- /dev/null
+++ b/src/api/system/post.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+/** 宀椾綅涓嬫媺 GET /system/post/optionselect */
+export function findPostOptions(query) {
+ return request({
+ url: "/system/post/optionselect",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/components/CommonUpload.vue b/src/components/CommonUpload.vue
new file mode 100644
index 0000000..8910206
--- /dev/null
+++ b/src/components/CommonUpload.vue
@@ -0,0 +1,164 @@
+<template>
+ <view class="common-upload">
+ <u-upload
+ :fileList="internalFileList"
+ @afterRead="afterRead"
+ @delete="deleteFile"
+ :name="name"
+ :multiple="multiple"
+ :maxCount="maxCount"
+ :accept="accept"
+ :disabled="disabled"
+ ></u-upload>
+ </view>
+</template>
+
+<script setup>
+import { ref, watch, onMounted } from 'vue';
+import { getToken } from "@/utils/auth";
+import config from "@/config";
+
+const props = defineProps({
+ // 鐖剁粍浠朵紶鍏ョ殑鏂囦欢鍒楄〃锛堝搴斿悗绔瓨鍌ㄧ殑瀵硅薄鍒楄〃锛�
+ modelValue: {
+ type: Array,
+ default: () => []
+ },
+ // 鏈�澶т笂浼犳暟閲�
+ maxCount: {
+ type: Number,
+ default: 9
+ },
+ // 鏄惁鏀寔澶氶��
+ multiple: {
+ type: Boolean,
+ default: true
+ },
+ // 鎺ュ彈鐨勬枃浠剁被鍨�
+ accept: {
+ type: String,
+ default: 'image'
+ },
+ // 涓婁紶鎺ュ彛瀵瑰簲鐨勫弬鏁板悕
+ name: {
+ type: String,
+ default: 'file'
+ },
+ // 鏄惁绂佺敤
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+});
+
+const emit = defineEmits(['update:modelValue']);
+
+// 鐢ㄤ簬 u-upload 鏄剧ず鐨勫唴閮ㄥ垪琛�
+const internalFileList = ref([]);
+
+// 鐩戝惉澶栭儴 modelValue 鍙樺寲锛屽悓姝ュ埌鍐呴儴鏄剧ず鍒楄〃
+watch(() => props.modelValue, (newVal) => {
+ if (newVal) {
+ internalFileList.value = newVal.map(item => ({
+ ...item,
+ url: item.url || item.previewURL,
+ status: 'success',
+ message: ''
+ }));
+ }
+}, { immediate: true, deep: true });
+
+const showToast = (message) => {
+ uni.showToast({
+ title: message,
+ icon: "none",
+ });
+};
+
+// 涓婁紶閫昏緫
+const uploadFilePromise = (url) => {
+ return new Promise((resolve, reject) => {
+ uni.uploadFile({
+ url: config.baseUrl + "/common/upload",
+ filePath: url,
+ name: "files", // 娉ㄦ剰锛氳繖閲屾牴鎹師浠g爜鏄� "files"
+ header: {
+ Authorization: "Bearer " + getToken(),
+ },
+ success: (res) => {
+ try {
+ const data = JSON.parse(res.data);
+ if (data.code === 200) {
+ // 濡傛灉杩斿洖鐨勬槸鏁扮粍锛屽彇绗竴涓厓绱�
+ const resultData = Array.isArray(data.data) ? data.data[0] : data.data;
+
+ // 澶勭悊 url 璧嬪��
+ if (!resultData.url && resultData.previewURL) {
+ resultData.url = resultData.previewURL;
+ }
+ // 鍏煎鍘熶唬鐮佷腑鐨� name 璧嬪��
+ if (!resultData.name && resultData.originalFilename) {
+ resultData.name = resultData.originalFilename;
+ }
+
+ resolve(resultData);
+ } else {
+ reject(data.msg || "涓婁紶澶辫触");
+ }
+ } catch (e) {
+ reject("瑙f瀽鍝嶅簲澶辫触");
+ }
+ },
+ fail: (err) => {
+ reject(err);
+ },
+ });
+ });
+};
+
+// 涓婁紶鍚庣殑澶勭悊
+const afterRead = async (event) => {
+ let lists = [].concat(event.file);
+ let currentModelValue = [...props.modelValue];
+
+ // 鍏堝湪鍐呴儴鍒楄〃涓坊鍔犲崰浣嶏紙涓婁紶涓姸鎬侊級
+ lists.forEach(item => {
+ internalFileList.value.push({
+ ...item,
+ status: 'uploading',
+ message: '涓婁紶涓�'
+ });
+ });
+
+ for (let i = 0; i < lists.length; i++) {
+ try {
+ const result = await uploadFilePromise(lists[i].url);
+
+ // 鏇存柊 modelValue
+ currentModelValue.push(result);
+ emit('update:modelValue', currentModelValue);
+
+ } catch (e) {
+ // 濡傛灉涓婁紶澶辫触锛屼粠鍐呴儴鍒楄〃涓Щ闄ゅ垰鎵嶆坊鍔犵殑椤�
+ const errorIndex = internalFileList.value.findIndex(item => item.status === 'uploading');
+ if (errorIndex > -1) {
+ internalFileList.value.splice(errorIndex, 1);
+ }
+ showToast(typeof e === "string" ? e : "涓婁紶澶辫触");
+ }
+ }
+};
+
+// 鍒犻櫎澶勭悊
+const deleteFile = (event) => {
+ const newList = [...props.modelValue];
+ newList.splice(event.index, 1);
+ emit('update:modelValue', newList);
+};
+</script>
+
+<style scoped lang="scss">
+.common-upload {
+ width: 100%;
+}
+</style>
diff --git a/src/config/oaPaths.js b/src/config/oaPaths.js
new file mode 100644
index 0000000..561db54
--- /dev/null
+++ b/src/config/oaPaths.js
@@ -0,0 +1,47 @@
+/**
+ * OA 妯″潡璺緞甯搁噺锛坧ages.json path 涓嶅惈鍓嶇紑 /锛�
+ * 瀵艰埅浣跨敤锛歶ni.navigateTo({ url: OA_NAV.xxx })
+ */
+const P = "pages/oa";
+
+export const OA_NAV = {
+ /** 浜轰簨绠$悊 */
+ staffArchive: `/${P}/HrManage/staff-archive/index`,
+ staffContract: `/${P}/HrManage/staff-contract/index`,
+ regularApply: `/${P}/HrManage/regular-apply/index`,
+ transferApply: `/${P}/HrManage/transfer-apply/index`,
+ resignApply: `/${P}/HrManage/resign-apply/index`,
+ workHandover: `/${P}/HrManage/work-handover/index`,
+ postManage: `/${P}/HrManage/post-manage/index`,
+ /** 鍋囧嫟绠$悊 */
+ leaveApply: `/${P}/AttendManage/leave-apply/index`,
+ overtimeApply: `/${P}/AttendManage/overtime-apply/index`,
+ /** 鎶ラ攢绠$悊 */
+ travelReimburse: `/${P}/ReimburseManage/travel-reimburse/index`,
+ costReimburse: `/${P}/ReimburseManage/cost-reimburse/index`,
+ reimburseDetail: `/${P}/ReimburseManage/reimburse-detail/index`,
+ reimburseForm: `/${P}/ReimburseManage/reimburse-form/index`,
+ /** 鍚堝悓绠$悊 */
+ purchaseContract: `/${P}/ContractManage/purchase-contract/index`,
+ saleContract: `/${P}/ContractManage/sale-contract/index`,
+ /** 瀹℃壒绠$悊 */
+ approveList: `/${P}/ApproveManage/approve-list/index`,
+ approveListTemplateSelect: `/${P}/ApproveManage/approve-list/template-select`,
+ approveListApply: `/${P}/ApproveManage/approve-list/apply`,
+ approveListDetail: `/${P}/ApproveManage/approve-list/detail`,
+ approveListApprove: `/${P}/ApproveManage/approve-list/approve`,
+ approveTemplate: `/${P}/ApproveManage/approve-template/index`,
+ approveTemplateEdit: `/${P}/ApproveManage/approve-template/edit`,
+ approveTemplateDetail: `/${P}/ApproveManage/approve-template/detail`,
+ /** 浼佷笟鏂伴椈 / 鍏憡閫氱煡 */
+ enterpriseNews: `/${P}/EnterpriseNews/news-manage/index`,
+ noticeAnnouncement: `/${P}/NoticeAnnouncement/notice-manage/index`,
+};
+
+/** pages.json 娉ㄥ唽鐢� path锛堟棤 / 鍓嶇紑锛� */
+export const OA_PAGE_PATHS = Object.fromEntries(
+ Object.entries(OA_NAV).map(([key, url]) => [
+ key,
+ url.replace(/^\//, ""),
+ ])
+);
diff --git a/src/config/oaWorkbench.js b/src/config/oaWorkbench.js
new file mode 100644
index 0000000..e4f6d7a
--- /dev/null
+++ b/src/config/oaWorkbench.js
@@ -0,0 +1,75 @@
+import { OA_NAV } from "./oaPaths.js";
+
+/**
+ * OA 妯″潡鍒嗙粍锛堝伐浣滃彴灞曠ず / 鏂囨。瀵圭収锛�
+ */
+export const OA_MODULES = [
+ {
+ key: "HrManage",
+ name: "浜轰簨绠$悊",
+ children: [
+ // { label: "鍛樺伐妗f", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.staffArchive },
+ // { label: "鍛樺伐鍚堝悓", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.staffContract },
+ { label: "杞鐢宠", icon: "/static/images/icon/hetongguanli.svg", path: OA_NAV.regularApply },
+ { label: "璋冨矖鐢宠", icon: "/static/images/icon/renyuanxinzi.svg", path: OA_NAV.transferApply },
+ // { label: "绂昏亴鐢宠", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.resignApply },
+ { label: "宸ヤ綔浜ゆ帴", icon: "/static/images/icon/gongchuguanli.svg", path: OA_NAV.workHandover },
+ // { label: "宀椾綅绠$悊", icon: "/static/images/icon/gongxuguanli.svg", path: OA_NAV.postManage },
+ ],
+ },
+ {
+ key: "AttendManage",
+ name: "鍋囧嫟绠$悊",
+ children: [
+ { label: "璇峰亣鐢宠", icon: "/static/images/icon/qingjiaguanli.svg", path: OA_NAV.leaveApply },
+ { label: "鍔犵彮鐢宠", icon: "/static/images/icon/dakaqiandao.svg", path: OA_NAV.overtimeApply },
+ ],
+ },
+ {
+ key: "ReimburseManage",
+ name: "鎶ラ攢绠$悊",
+ children: [
+ { label: "宸梾鎶ラ攢", icon: "/static/images/icon/chuchaiguanli.svg", path: OA_NAV.travelReimburse },
+ { label: "璐圭敤鎶ラ攢", icon: "/static/images/icon/baoxiaoguanli.svg", path: OA_NAV.costReimburse },
+ ],
+ },
+ // {
+ // key: "ContractManage",
+ // name: "鍚堝悓绠$悊",
+ // children: [
+ // { label: "閲囪喘鍚堝悓", icon: "/static/images/icon/caigoutaizhang.svg", path: OA_NAV.purchaseContract },
+ // { label: "閿�鍞悎鍚�", icon: "/static/images/icon/xiaoshoutaizhang.svg", path: OA_NAV.saleContract },
+ // ],
+ // },
+ {
+ key: "ApproveManage",
+ name: "瀹℃壒绠$悊",
+ children: [
+ { label: "瀹℃壒鍒楄〃", icon: "/static/images/icon/xietongshenpi.svg", path: OA_NAV.approveList },
+ { label: "瀹℃壒妯℃澘", icon: "/static/images/icon/guizhangzhidu.svg", path: OA_NAV.approveTemplate },
+ ],
+ },
+ // {
+ // key: "EnterpriseNews",
+ // name: "浼佷笟鏂伴椈",
+ // children: [
+ // { label: "浼佷笟鏂伴椈", icon: "/static/images/icon/zhishiku.svg", path: OA_NAV.enterpriseNews },
+ // ],
+ // },
+ // {
+ // key: "NoticeAnnouncement",
+ // name: "鍏憡閫氱煡",
+ // children: [
+ // { label: "鍏憡閫氱煡", icon: "/static/images/icon/tongzhigonggao.svg", path: OA_NAV.noticeAnnouncement },
+ // ],
+ // },
+];
+
+/** 宸ヤ綔鍙版墎骞宠彍鍗曪紙绾墠绔厤缃級 */
+export const OA_WORKBENCH_ITEMS = OA_MODULES.flatMap(module =>
+ module.children.map(item => ({
+ ...item,
+ module: module.name,
+ moduleKey: module.key,
+ }))
+);
diff --git a/src/manifest.json b/src/manifest.json
index 688e2f3..acdeac9 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -24,7 +24,6 @@
"modules" : {
"Camera" : {},
"Barcode" : {},
- // "Push" : {},
"Maps" : {}
},
/* 搴旂敤鍙戝竷淇℃伅 */
diff --git a/src/pages.json b/src/pages.json
index e52db8a..1aa49ad 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -367,6 +367,55 @@
}
},
{
+ "path": "pages/productionDesign/basicParameters/index",
+ "style": {
+ "navigationBarTitleText": "鍩虹鍙傛暟",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/basicParameters/edit",
+ "style": {
+ "navigationBarTitleText": "鍙傛暟璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/processManagement/index",
+ "style": {
+ "navigationBarTitleText": "宸ュ簭绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/processManagement/edit",
+ "style": {
+ "navigationBarTitleText": "宸ュ簭璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/processManagement/params",
+ "style": {
+ "navigationBarTitleText": "宸ュ簭鍙傛暟閰嶇疆",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/bom/index",
+ "style": {
+ "navigationBarTitleText": "BOM绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionDesign/bom/structure",
+ "style": {
+ "navigationBarTitleText": "BOM缁撴瀯",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/cooperativeOffice/collaborativeApproval/index1",
"style": {
"navigationBarTitleText": "鍏嚭绠$悊",
@@ -712,6 +761,20 @@
}
},
{
+ "path": "pages/inspectionUpload/upload",
+ "style": {
+ "navigationBarTitleText": "涓婁紶宸℃璁板綍",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inspectionUpload/attachment",
+ "style": {
+ "navigationBarTitleText": "鏌ョ湅闄勪欢",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/equipmentManagement/faultAnalysis/index",
"style": {
"navigationBarTitleText": "鏁呴殰鍒嗘瀽杩芥函",
@@ -722,6 +785,34 @@
"path": "pages/productionManagement/productionOrder/index",
"style": {
"navigationBarTitleText": "鐢熶骇璁㈠崟",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/productionOrder/source",
+ "style": {
+ "navigationBarTitleText": "鏉ユ簮鏁版嵁",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/productionOrder/pickingDetail",
+ "style": {
+ "navigationBarTitleText": "棰嗘枡璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/processRoute/index",
+ "style": {
+ "navigationBarTitleText": "宸ヨ壓璺嚎",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/processRoute/items",
+ "style": {
+ "navigationBarTitleText": "璺嚎椤圭洰",
"navigationStyle": "custom"
}
},
@@ -747,19 +838,61 @@
}
},
{
+ "path": "pages/productionManagement/productionReporting/ledger",
+ "style": {
+ "navigationBarTitleText": "鎶ュ伐鍙拌处",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/productionManagement/workOrder/index",
"style": {
"navigationBarTitleText": "鐢熶骇宸ュ崟",
"navigationStyle": "custom"
}
},
- // {
- // "path": "pages/productionManagement/productionCosting/index",
- // "style": {
- // "navigationBarTitleText": "鐢熶骇鏍哥畻",
- // "navigationStyle": "custom"
- // }
- // },
+ {
+ "path": "pages/productionManagement/mainProductionPlan/index",
+ "style": {
+ "navigationBarTitleText": "涓荤敓浜ц鍒�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/mainProductionPlan/detail",
+ "style": {
+ "navigationBarTitleText": "鐢熶骇璁″垝璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/productionScheduling/index",
+ "style": {
+ "navigationBarTitleText": "鐢熶骇鎺掍骇",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/productionAccounting/index",
+ "style": {
+ "navigationBarTitleText": "鐢熶骇鏍哥畻",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/productionTraceability/index",
+ "style": {
+ "navigationBarTitleText": "鐢熶骇杩芥函",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/productionManagement/processStatistics/index",
+ "style": {
+ "navigationBarTitleText": "宸ュ簭鐢熶骇瀹炲喌",
+ "navigationStyle": "custom"
+ }
+ },
{
"path": "pages/inventoryManagement/receiptManagement/index",
"style": {
@@ -1143,6 +1276,209 @@
"style": {
"navigationBarTitleText": "娑堟伅涓績"
}
+ },
+ {
+ "path": "pages/fileManagement/borrow/index",
+ "style": {
+ "navigationBarTitleText": "鍊熼槄绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/fileManagement/borrow/edit",
+ "style": {
+ "navigationBarTitleText": "鍊熼槄鐧昏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/fileManagement/return/index",
+ "style": {
+ "navigationBarTitleText": "褰掕繕绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/fileManagement/return/edit",
+ "style": {
+ "navigationBarTitleText": "褰掕繕鐧昏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/HrManage/staff-archive/index",
+ "style": {
+ "navigationBarTitleText": "鍛樺伐妗f",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/HrManage/staff-contract/index",
+ "style": {
+ "navigationBarTitleText": "鍛樺伐鍚堝悓",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/HrManage/regular-apply/index",
+ "style": {
+ "navigationBarTitleText": "杞鐢宠",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/HrManage/transfer-apply/index",
+ "style": {
+ "navigationBarTitleText": "璋冨矖鐢宠",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/HrManage/resign-apply/index",
+ "style": {
+ "navigationBarTitleText": "绂昏亴鐢宠",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/HrManage/work-handover/index",
+ "style": {
+ "navigationBarTitleText": "宸ヤ綔浜ゆ帴",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/HrManage/post-manage/index",
+ "style": {
+ "navigationBarTitleText": "宀椾綅绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/AttendManage/leave-apply/index",
+ "style": {
+ "navigationBarTitleText": "璇峰亣鐢宠",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/AttendManage/overtime-apply/index",
+ "style": {
+ "navigationBarTitleText": "鍔犵彮鐢宠",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ReimburseManage/travel-reimburse/index",
+ "style": {
+ "navigationBarTitleText": "宸梾鎶ラ攢",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ReimburseManage/cost-reimburse/index",
+ "style": {
+ "navigationBarTitleText": "璐圭敤鎶ラ攢",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ReimburseManage/reimburse-detail/index",
+ "style": {
+ "navigationBarTitleText": "鎶ラ攢璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ReimburseManage/reimburse-form/index",
+ "style": {
+ "navigationBarTitleText": "鎶ラ攢濉姤",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ContractManage/purchase-contract/index",
+ "style": {
+ "navigationBarTitleText": "閲囪喘鍚堝悓",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ContractManage/sale-contract/index",
+ "style": {
+ "navigationBarTitleText": "閿�鍞悎鍚�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-list/index",
+ "style": {
+ "navigationBarTitleText": "瀹℃壒鍒楄〃",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-list/template-select",
+ "style": {
+ "navigationBarTitleText": "閫夋嫨瀹℃壒妯℃澘",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-list/apply",
+ "style": {
+ "navigationBarTitleText": "鍙戣捣瀹℃壒",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-list/detail",
+ "style": {
+ "navigationBarTitleText": "瀹℃壒璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-list/approve",
+ "style": {
+ "navigationBarTitleText": "瀹℃壒澶勭悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-template/index",
+ "style": {
+ "navigationBarTitleText": "瀹℃壒妯℃澘",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-template/edit",
+ "style": {
+ "navigationBarTitleText": "鏂板缓瀹℃壒妯℃澘",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/ApproveManage/approve-template/detail",
+ "style": {
+ "navigationBarTitleText": "妯℃澘璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/EnterpriseNews/news-manage/index",
+ "style": {
+ "navigationBarTitleText": "浼佷笟鏂伴椈",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/oa/NoticeAnnouncement/notice-manage/index",
+ "style": {
+ "navigationBarTitleText": "鍏憡閫氱煡",
+ "navigationStyle": "custom"
+ }
}
],
"subPackages": [
@@ -1379,4 +1715,4 @@
"navigationBarTitleText": "RuoYi",
"navigationBarBackgroundColor": "#FFFFFF"
}
-}
+}
\ No newline at end of file
diff --git a/src/pages/cooperativeOffice/collaborativeApproval/approve.vue b/src/pages/cooperativeOffice/collaborativeApproval/approve.vue
index aaad83e..b3c8687 100644
--- a/src/pages/cooperativeOffice/collaborativeApproval/approve.vue
+++ b/src/pages/cooperativeOffice/collaborativeApproval/approve.vue
@@ -1,8 +1,7 @@
<template>
<view class="approve-page">
-
- <PageHeader title="瀹℃牳" @back="goBack" />
-
+ <PageHeader title="瀹℃牳"
+ @back="goBack" />
<!-- 鐢宠淇℃伅 -->
<view class="application-info">
<view class="info-header">
@@ -25,7 +24,6 @@
<text class="info-label">鐢宠鏃ユ湡</text>
<text class="info-value">{{ approvalData.approveTime }}</text>
</view>
-
<!-- approveType=2 璇峰亣鐩稿叧瀛楁 -->
<template v-if="approvalData.approveType === 2">
<view class="info-row">
@@ -37,462 +35,472 @@
<text class="info-value">{{ approvalData.endDate || '-' }}</text>
</view>
</template>
-
<!-- approveType=3 鍑哄樊鐩稿叧瀛楁 -->
- <view v-if="approvalData.approveType === 3" class="info-row">
+ <view v-if="approvalData.approveType === 3"
+ class="info-row">
<text class="info-label">鍑哄樊鍦扮偣</text>
<text class="info-value">{{ approvalData.location || '-' }}</text>
</view>
-
<!-- approveType=4 鎶ラ攢鐩稿叧瀛楁 -->
- <view v-if="approvalData.approveType === 4" class="info-row">
+ <view v-if="approvalData.approveType === 4"
+ class="info-row">
<text class="info-label">鎶ラ攢閲戦</text>
<text class="info-value">{{ approvalData.price ? `楼${approvalData.price}` : '-' }}</text>
</view>
</view>
</view>
-
<!-- 瀹℃壒娴佺▼ -->
<view class="approval-process">
<view class="process-header">
<text class="process-title">瀹℃壒娴佺▼</text>
</view>
-
<view class="process-steps">
- <view
- v-for="(step, index) in approvalSteps"
- :key="index"
- class="process-step"
- :class="{
+ <view v-for="(step, index) in approvalSteps"
+ :key="index"
+ class="process-step"
+ :class="{
'completed': step.status === 'completed',
'current': step.status === 'current',
'pending': step.status === 'pending',
'rejected': step.status === 'rejected'
- }"
- >
+ }">
<view class="step-indicator">
<view class="step-dot">
- <text v-if="step.status === 'completed'" class="step-icon">鉁�</text>
- <text v-else-if="step.status === 'rejected'" class="step-icon">鉁�</text>
- <text v-else class="step-number">{{ index + 1 }}</text>
+ <text v-if="step.status === 'completed'"
+ class="step-icon">鉁�</text>
+ <text v-else-if="step.status === 'rejected'"
+ class="step-icon">鉁�</text>
+ <text v-else
+ class="step-number">{{ index + 1 }}</text>
</view>
- <view v-if="index < approvalSteps.length - 1" class="step-line"></view>
+ <view v-if="index < approvalSteps.length - 1"
+ class="step-line"></view>
</view>
-
<view class="step-content">
<view class="step-info">
<text class="step-title">{{ step.title }}</text>
<text class="step-approver">{{ step.approverName }}</text>
- <text v-if="step.approveTime" class="step-time">{{ step.approveTime }}</text>
+ <text v-if="step.approveTime"
+ class="step-time">{{ step.approveTime }}</text>
</view>
-
- <view v-if="step.opinion" class="step-opinion">
+ <view v-if="step.opinion"
+ class="step-opinion">
<text class="opinion-label">瀹℃壒鎰忚锛�</text>
<text class="opinion-content">{{ step.opinion }}</text>
</view>
<!-- 绛惧悕灞曠ず -->
- <view v-if="step.urlTem" class="step-opinion" style="margin-top:8px;">
+ <view v-if="step.urlTem"
+ class="step-opinion"
+ style="margin-top:8px;">
<text class="opinion-label">绛惧悕锛�</text>
- <image :src="step.urlTem" mode="widthFix" style="width:180px;border-radius:6px;border:1px solid #eee;" />
+ <image :src="step.urlTem"
+ mode="widthFix"
+ style="width:180px;border-radius:6px;border:1px solid #eee;" />
</view>
</view>
</view>
</view>
</view>
-
<!-- 瀹℃牳鎰忚杈撳叆 -->
- <view v-if="canApprove" class="approval-input">
+ <view v-if="canApprove"
+ class="approval-input">
<view class="input-header">
<text class="input-title">瀹℃牳鎰忚</text>
</view>
-
<view class="input-content">
- <u-textarea
- v-model="approvalOpinion"
- rows="4"
- placeholder="璇疯緭鍏ュ鏍告剰瑙�"
- maxlength="200"
- count
- />
+ <u-textarea v-model="approvalOpinion"
+ rows="4"
+ placeholder="璇疯緭鍏ュ鏍告剰瑙�"
+ maxlength="200"
+ count />
</view>
</view>
-
<!-- 搴曢儴鎿嶄綔鎸夐挳 -->
- <view v-if="canApprove" class="footer-actions">
- <u-button class="reject-btn" @click="handleReject">椹冲洖</u-button>
- <u-button class="approve-btn" @click="handleApprove">閫氳繃</u-button>
+ <view v-if="canApprove"
+ class="footer-actions">
+ <u-button class="reject-btn"
+ @click="handleReject">椹冲洖</u-button>
+ <u-button class="approve-btn"
+ @click="handleApprove">閫氳繃</u-button>
</view>
</view>
</template>
<script setup>
-import { ref, onMounted, computed } from 'vue'
-import { approveProcessGetInfo, approveProcessDetails, updateApproveNode } from '@/api/collaborativeApproval/approvalProcess'
-import useUserStore from '@/store/modules/user'
-const showToast = (message) => {
- uni.showToast({
- title: message,
- icon: 'none'
- })
-}
-import PageHeader from "@/components/PageHeader.vue";
+ import { ref, onMounted, computed } from "vue";
+ import {
+ approveProcessGetInfo,
+ approveProcessDetails,
+ updateApproveNode,
+ } from "@/api/collaborativeApproval/approvalProcess";
+ import useUserStore from "@/store/modules/user";
+ const showToast = message => {
+ uni.showToast({
+ title: message,
+ icon: "none",
+ });
+ };
+ import PageHeader from "@/components/PageHeader.vue";
-const userStore = useUserStore()
-const approvalData = ref({})
-const approvalSteps = ref([])
-const approvalOpinion = ref('')
-const approveId = ref('')
+ const userStore = useUserStore();
+ const approvalData = ref({});
+ const approvalSteps = ref([]);
+ const approvalOpinion = ref("");
+ const approveId = ref("");
-// 浠庤鎯呮帴鍙e瓧娈靛榻� canApprove锛氫粎褰撴湁 isShen 鐨勮妭鐐规椂鍙鎵�
-const canApprove = computed(() => {
- return approvalSteps.value.some(step => step.isShen === true)
-})
+ // 浠庤鎯呮帴鍙e瓧娈靛榻� canApprove锛氫粎褰撴湁 isShen 鐨勮妭鐐规椂鍙鎵�
+ const canApprove = computed(() => {
+ return approvalSteps.value.some(step => step.isShen === true);
+ });
-onMounted(() => {
- approveId.value = uni.getStorageSync('approveId')
- if (approveId.value) {
- loadApprovalData()
- }
-})
-
-const loadApprovalData = () => {
- // 鍩烘湰鐢宠淇℃伅
- approveProcessGetInfo({ id: approveId.value }).then(res => {
- approvalData.value = res.data || {}
- })
- // 瀹℃壒鑺傜偣璇︽儏
- approveProcessDetails(approveId.value).then(res => {
- const list = Array.isArray(res.data) ? res.data : []
- // 淇濆瓨鍘熷鑺傜偣鏁版嵁渚涙彁浜や娇鐢�
- activities.value = list
-
- approvalSteps.value = list.map((it, idx) => {
- // 鑺傜偣鐘舵�佹槧灏勶細1=閫氳繃锛�2=涓嶉�氳繃锛屽惁鍒欑湅鏄惁褰撳墠(isShen)锛屽啀榛樿涓哄緟澶勭悊
- let status = 'pending'
- if (it.approveNodeStatus === 1) status = 'completed'
- else if (it.approveNodeStatus === 2) status = 'rejected'
- else if (it.isShen) status = 'current'
- return {
- title: `绗�${idx + 1}姝ュ鎵筦,
- approverName: it.approveNodeUser || '鏈煡鐢ㄦ埛',
- status,
- approveTime: it.approveTime || null,
- opinion: it.approveNodeReason || '',
- urlTem: it.urlTem || '',
- isShen: !!it.isShen
- }
- })
- })
-}
-
-const goBack = () => {
- uni.removeStorageSync('approveId');
- uni.navigateBack()
-}
-
-const submitForm = (status) => {
- // 鍙�夛細鏍¢獙瀹℃牳鎰忚
- if (!approvalOpinion.value?.trim()) {
- showToast('璇疯緭鍏ュ鏍告剰瑙�')
- return
- }
- // 鎵惧埌褰撳墠鍙鎵硅妭鐐�
- const filteredActivities = activities.value.filter(activity => activity.isShen)
- if (!filteredActivities.length) {
- showToast('褰撳墠鏃犲彲瀹℃壒鑺傜偣')
- return
- }
- // 鍐欏叆鐘舵�佸拰鎰忚
- filteredActivities[0].approveNodeStatus = status
- filteredActivities[0].approveNodeReason = approvalOpinion.value || ''
- // 璁$畻鏄惁涓烘渶鍚庝竴姝�
- const isLast = activities.value.findIndex(a => a.isShen) === activities.value.length - 1
- // 璋冪敤鍚庣
- updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
- const msg = status === 1 ? '瀹℃壒閫氳繃' : '瀹℃壒宸查┏鍥�'
- showToast(msg)
- // 鎻愮ず鍚庤繑鍥炰笂涓�涓〉闈�
- setTimeout(() => {
- goBack() // 鍐呴儴鏄� uni.navigateBack()
- }, 800)
- })
-}
-
-const handleApprove = () => {
- uni.showModal({
- title: '纭鎿嶄綔',
- content: '纭畾瑕侀�氳繃姝ゅ鎵瑰悧锛�',
- success: (res) => {
- if (res.confirm) submitForm(1)
+ onMounted(() => {
+ approveId.value = uni.getStorageSync("approveId");
+ if (approveId.value) {
+ loadApprovalData();
}
- })
-}
+ });
-const handleReject = () => {
- uni.showModal({
- title: '纭鎿嶄綔',
- content: '纭畾瑕侀┏鍥炴瀹℃壒鍚楋紵',
- success: (res) => {
- if (res.confirm) submitForm(2)
+ const loadApprovalData = () => {
+ // 鍩烘湰鐢宠淇℃伅
+ approveProcessGetInfo({ id: approveId.value }).then(res => {
+ approvalData.value = res.data || {};
+ });
+ // 瀹℃壒鑺傜偣璇︽儏
+ approveProcessDetails(approveId.value).then(res => {
+ const list = Array.isArray(res.data) ? res.data : [];
+ // 淇濆瓨鍘熷鑺傜偣鏁版嵁渚涙彁浜や娇鐢�
+ activities.value = list;
+
+ approvalSteps.value = list.map((it, idx) => {
+ // 鑺傜偣鐘舵�佹槧灏勶細1=閫氳繃锛�2=涓嶉�氳繃锛屽惁鍒欑湅鏄惁褰撳墠(isShen)锛屽啀榛樿涓哄緟澶勭悊
+ let status = "pending";
+ if (it.approveNodeStatus === 1) status = "completed";
+ else if (it.approveNodeStatus === 2) status = "rejected";
+ else if (it.isShen) status = "current";
+ return {
+ title: `绗�${idx + 1}姝ュ鎵筦,
+ approverName: it.approveNodeUser || "鏈煡鐢ㄦ埛",
+ status,
+ approveTime: it.approveTime || null,
+ opinion: it.approveNodeReason || "",
+ urlTem: it.urlTem || "",
+ isShen: !!it.isShen,
+ };
+ });
+ });
+ };
+
+ const goBack = () => {
+ uni.removeStorageSync("approveId");
+ uni.navigateBack();
+ };
+
+ const submitForm = status => {
+ // 鍙�夛細鏍¢獙瀹℃牳鎰忚
+ if (!approvalOpinion.value?.trim()) {
+ showToast("璇疯緭鍏ュ鏍告剰瑙�");
+ return;
}
- })
-}
-// 鍘熷鑺傜偣鏁版嵁锛堢敤浜庢彁浜ら�昏緫锛�
-const activities = ref([])
+ // 鎵惧埌褰撳墠鍙鎵硅妭鐐�
+ const filteredActivities = activities.value.filter(
+ activity => activity.isShen
+ );
+ if (!filteredActivities.length) {
+ showToast("褰撳墠鏃犲彲瀹℃壒鑺傜偣");
+ return;
+ }
+ // 鍐欏叆鐘舵�佸拰鎰忚
+ filteredActivities[0].approveNodeStatus = status;
+ filteredActivities[0].approveNodeReason = approvalOpinion.value || "";
+ // 璁$畻鏄惁涓烘渶鍚庝竴姝�
+ const isLast =
+ activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
+ // 璋冪敤鍚庣
+ updateApproveNode({ ...filteredActivities[0], isLast }).then(() => {
+ const msg = status === 1 ? "瀹℃壒閫氳繃" : "瀹℃壒宸查┏鍥�";
+ showToast(msg);
+ // 鎻愮ず鍚庤繑鍥炰笂涓�涓〉闈�
+ setTimeout(() => {
+ goBack(); // 鍐呴儴鏄� uni.navigateBack()
+ }, 800);
+ });
+ };
+
+ const handleApprove = () => {
+ uni.showModal({
+ title: "纭鎿嶄綔",
+ content: "纭畾瑕侀�氳繃姝ゅ鎵瑰悧锛�",
+ success: res => {
+ if (res.confirm) submitForm(1);
+ },
+ });
+ };
+
+ const handleReject = () => {
+ uni.showModal({
+ title: "纭鎿嶄綔",
+ content: "纭畾瑕侀┏鍥炴瀹℃壒鍚楋紵",
+ success: res => {
+ if (res.confirm) submitForm(2);
+ },
+ });
+ };
+ // 鍘熷鑺傜偣鏁版嵁锛堢敤浜庢彁浜ら�昏緫锛�
+ const activities = ref([]);
</script>
<style scoped lang="scss">
-.approve-page {
- min-height: 100vh;
- background: #f8f9fa;
- padding-bottom: 80px;
-}
-
-.header {
- display: flex;
- align-items: center;
- background: #fff;
- padding: 16px 20px;
- border-bottom: 1px solid #f0f0f0;
- position: sticky;
- top: 0;
- z-index: 100;
-}
-
-.title {
- flex: 1;
- text-align: center;
- font-size: 18px;
- font-weight: 600;
- color: #333;
-}
-
-.application-info {
- background: #fff;
- margin: 16px;
- border-radius: 12px;
- overflow: hidden;
-}
-
-.info-header {
- padding: 16px;
- border-bottom: 1px solid #f0f0f0;
- background: #f8f9fa;
-}
-
-.info-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
-}
-
-.info-content {
- padding: 16px;
-}
-
-.info-row {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
-
- &:last-child {
- margin-bottom: 0;
+ .approve-page {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 80px;
}
-}
-.info-label {
- font-size: 14px;
- color: #666;
- width: 80px;
- flex-shrink: 0;
-}
+ .header {
+ display: flex;
+ align-items: center;
+ background: #fff;
+ padding: 16px 20px;
+ border-bottom: 1px solid #f0f0f0;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ }
-.info-value {
- font-size: 14px;
- color: #333;
- flex: 1;
-}
+ .title {
+ flex: 1;
+ text-align: center;
+ font-size: 18px;
+ font-weight: 600;
+ color: #333;
+ }
-.approval-process {
- background: #fff;
- margin: 16px;
- border-radius: 12px;
- overflow: hidden;
-}
+ .application-info {
+ background: #fff;
+ margin: 16px;
+ border-radius: 12px;
+ overflow: hidden;
+ }
-.process-header {
- padding: 16px;
- border-bottom: 1px solid #f0f0f0;
- background: #f8f9fa;
-}
+ .info-header {
+ padding: 16px;
+ border-bottom: 1px solid #f0f0f0;
+ background: #f8f9fa;
+ }
-.process-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
-}
+ .info-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ }
-.process-steps {
- padding: 20px;
-}
+ .info-content {
+ padding: 16px;
+ }
-.process-step {
- display: flex;
- position: relative;
- margin-bottom: 24px;
-
- &:last-child {
- margin-bottom: 0;
-
- .step-line {
- display: none;
+ .info-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+
+ &:last-child {
+ margin-bottom: 0;
}
}
-}
-.step-indicator {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin-right: 16px;
-}
+ .info-label {
+ font-size: 14px;
+ color: #666;
+ width: 80px;
+ flex-shrink: 0;
+ }
-.step-dot {
- width: 32px;
- height: 32px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- font-weight: 600;
- position: relative;
- z-index: 2;
-}
+ .info-value {
+ font-size: 14px;
+ color: #333;
+ flex: 1;
+ }
-.process-step.completed .step-dot {
- background: #52c41a;
- color: #fff;
-}
+ .approval-process {
+ background: #fff;
+ margin: 16px;
+ border-radius: 12px;
+ overflow: hidden;
+ }
-.process-step.current .step-dot {
- background: #1890ff;
- color: #fff;
- animation: pulse 2s infinite;
-}
+ .process-header {
+ padding: 16px;
+ border-bottom: 1px solid #f0f0f0;
+ background: #f8f9fa;
+ }
-.process-step.pending .step-dot {
- background: #d9d9d9;
- color: #999;
-}
+ .process-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ }
-.step-line {
- width: 2px;
- height: 40px;
- background: #d9d9d9;
- margin-top: 8px;
-}
+ .process-steps {
+ padding: 20px;
+ }
-.process-step.completed .step-line {
- background: #52c41a;
-}
+ .process-step {
+ display: flex;
+ position: relative;
+ margin-bottom: 24px;
-.process-step.rejected .step-dot {
- background: #ff4d4f;
- color: #fff;
-}
-.process-step.rejected .step-line {
- background: #ff4d4f;
-}
+ &:last-child {
+ margin-bottom: 0;
-.step-content {
- flex: 1;
- padding-top: 4px;
-}
+ .step-line {
+ display: none;
+ }
+ }
+ }
-.step-info {
- margin-bottom: 8px;
-}
+ .step-indicator {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-right: 16px;
+ }
-.step-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
- display: block;
- margin-bottom: 4px;
-}
+ .step-dot {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: 600;
+ position: relative;
+ z-index: 2;
+ }
-.step-approver {
- font-size: 14px;
- color: #666;
- display: block;
- margin-bottom: 4px;
-}
+ .process-step.completed .step-dot {
+ background: #52c41a;
+ color: #fff;
+ }
-.step-time {
- font-size: 12px;
- color: #999;
- display: block;
-}
+ .process-step.current .step-dot {
+ background: #1890ff;
+ color: #fff;
+ animation: pulse 2s infinite;
+ }
-.step-opinion {
- background: #f8f9fa;
- padding: 12px;
- border-radius: 8px;
- border-left: 4px solid #52c41a;
-}
+ .process-step.pending .step-dot {
+ background: #d9d9d9;
+ color: #999;
+ }
-.opinion-label {
- font-size: 12px;
- color: #666;
- display: block;
- margin-bottom: 4px;
-}
+ .step-line {
+ width: 2px;
+ height: 40px;
+ background: #d9d9d9;
+ margin-top: 8px;
+ }
-.opinion-content {
- font-size: 14px;
- color: #333;
- line-height: 1.5;
-}
+ .process-step.completed .step-line {
+ background: #52c41a;
+ }
-.approval-input {
- background: #fff;
- margin: 16px;
- border-radius: 12px;
- overflow: hidden;
-}
+ .process-step.rejected .step-dot {
+ background: #ff4d4f;
+ color: #fff;
+ }
+ .process-step.rejected .step-line {
+ background: #ff4d4f;
+ }
-.input-header {
- padding: 16px;
- border-bottom: 1px solid #f0f0f0;
- background: #f8f9fa;
-}
+ .step-content {
+ flex: 1;
+ padding-top: 4px;
+ }
-.input-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
-}
+ .step-info {
+ margin-bottom: 8px;
+ }
-.input-content {
- padding: 16px;
-}
+ .step-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ display: block;
+ margin-bottom: 4px;
+ }
-.footer-actions {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- background: #fff;
- display: flex;
- justify-content: space-around;
- align-items: center;
- padding: 16px;
- box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
- z-index: 1000;
-}
+ .step-approver {
+ font-size: 14px;
+ color: #666;
+ display: block;
+ margin-bottom: 4px;
+ }
-.reject-btn {
+ .step-time {
+ font-size: 12px;
+ color: #999;
+ display: block;
+ }
+
+ .step-opinion {
+ background: #f8f9fa;
+ padding: 12px;
+ border-radius: 8px;
+ border-left: 4px solid #52c41a;
+ }
+
+ .opinion-label {
+ font-size: 12px;
+ color: #666;
+ display: block;
+ margin-bottom: 4px;
+ }
+
+ .opinion-content {
+ font-size: 14px;
+ color: #333;
+ line-height: 1.5;
+ }
+
+ .approval-input {
+ background: #fff;
+ margin: 16px;
+ border-radius: 12px;
+ overflow: hidden;
+ }
+
+ .input-header {
+ padding: 16px;
+ border-bottom: 1px solid #f0f0f0;
+ background: #f8f9fa;
+ }
+
+ .input-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .input-content {
+ padding: 16px;
+ }
+
+ .footer-actions {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ padding: 16px;
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+ }
+
+ .reject-btn {
width: 120px;
background: #ff4d4f;
color: #fff;
@@ -503,47 +511,47 @@
background: #52c41a;
color: #fff;
}
-
+
/* 閫傞厤u-button鏍峰紡 */
:deep(.u-button) {
border-radius: 6px;
}
-@keyframes pulse {
- 0% {
- box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
+ @keyframes pulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.7);
+ }
+ 70% {
+ box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
+ }
}
- 70% {
- box-shadow: 0 0 0 10px rgba(24, 144, 255, 0);
+ .signature-section {
+ background: #fff;
+ padding: 12px 16px 16px;
+ border-top: 1px solid #f0f0f0;
}
- 100% {
- box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
+ .signature-header {
+ margin-bottom: 8px;
}
-}
-.signature-section {
- background: #fff;
- padding: 12px 16px 16px;
- border-top: 1px solid #f0f0f0;
-}
-.signature-header {
- margin-bottom: 8px;
-}
-.signature-title {
- font-size: 14px;
- font-weight: 600;
- color: #333;
-}
-.signature-box {
- width: 100%;
- height: 180px;
- background: #fff;
- border: 1px dashed #d9d9d9;
- border-radius: 8px;
- overflow: hidden;
-}
-.signature-actions {
- margin-top: 8px;
- display: flex;
- justify-content: flex-end;
-}
+ .signature-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ }
+ .signature-box {
+ width: 100%;
+ height: 180px;
+ background: #fff;
+ border: 1px dashed #d9d9d9;
+ border-radius: 8px;
+ overflow: hidden;
+ }
+ .signature-actions {
+ margin-top: 8px;
+ display: flex;
+ justify-content: flex-end;
+ }
</style>
\ No newline at end of file
diff --git a/src/pages/cooperativeOffice/collaborativeApproval/detail.vue b/src/pages/cooperativeOffice/collaborativeApproval/detail.vue
index 04a4c18..16f9923 100644
--- a/src/pages/cooperativeOffice/collaborativeApproval/detail.vue
+++ b/src/pages/cooperativeOffice/collaborativeApproval/detail.vue
@@ -1,6 +1,6 @@
<template>
<view class="account-detail">
- <PageHeader title="瀹℃壒娴佺▼"
+ <PageHeader :title="operationType === 'detail' ? '璇︽儏' : '瀹℃壒娴佺▼'"
@back="goBack" />
<!-- 琛ㄥ崟鍖哄煙 -->
<u-form ref="formRef"
@@ -8,38 +8,39 @@
:rules="rules"
:model="form"
label-width="140rpx">
- <u-form-item prop="approveReason"
- label="娴佺▼缂栧彿">
- <u-input v-model="form.approveId"
- disabled
- placeholder="鑷姩缂栧彿" />
- </u-form-item>
- <u-form-item prop="approveReason"
- :label="approveType === 5 ? '閲囪喘浜嬬敱' : '鐢宠浜嬬敱'"
- required>
- <u-input v-model="form.approveReason"
- type="textarea"
- rows="2"
- auto-height
- maxlength="200"
- :placeholder="approveType === 5 ? '璇疯緭鍏ラ噰璐簨鐢�' : '璇疯緭鍏ョ敵璇蜂簨鐢�'"
- show-word-limit />
- </u-form-item>
- <u-form-item prop="approveDeptName"
- label="鐢宠閮ㄩ棬"
- required>
- <!-- <u-input v-model="form.approveDeptName"
+ <template v-if="operationType !== 'detail'">
+ <u-form-item prop="approveReason"
+ label="娴佺▼缂栧彿">
+ <u-input v-model="form.approveId"
+ disabled
+ placeholder="鑷姩缂栧彿" />
+ </u-form-item>
+ <u-form-item prop="approveReason"
+ :label="approveType === 5 ? '閲囪喘浜嬬敱' : '鐢宠浜嬬敱'"
+ required>
+ <u-input v-model="form.approveReason"
+ type="textarea"
+ rows="2"
+ auto-height
+ maxlength="200"
+ :placeholder="approveType === 5 ? '璇疯緭鍏ラ噰璐簨鐢�' : '璇疯緭鍏ョ敵璇蜂簨鐢�'"
+ show-word-limit />
+ </u-form-item>
+ <u-form-item prop="approveDeptName"
+ label="鐢宠閮ㄩ棬"
+ required>
+ <!-- <u-input v-model="form.approveDeptName"
placeholder="璇烽�夋嫨鐢宠閮ㄩ棬" /> -->
- <u-input v-model="form.approveDeptName"
- readonly
- placeholder="璇烽�夋嫨鐢宠閮ㄩ棬"
- @click="showPicker = true" />
- <template #right>
- <up-icon name="arrow-right"
- @click="showPicker = true"></up-icon>
- </template>
- </u-form-item>
- <u-form-item prop="approveUser"
+ <u-input v-model="form.approveDeptName"
+ readonly
+ placeholder="璇烽�夋嫨鐢宠閮ㄩ棬"
+ @click="showPicker = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showPicker = true"></up-icon>
+ </template>
+ </u-form-item>
+ <!-- <u-form-item prop="approveUser"
label="鐢宠浜�"
required>
<u-input v-model="form.approveUserName"
@@ -57,141 +58,277 @@
<up-icon name="arrow-right"
@click="showDatePicker"></up-icon>
</template>
- </u-form-item>
- <!-- approveType=2 璇峰亣鐩稿叧瀛楁 -->
- <template v-if="approveType === 2">
- <u-form-item prop="startDate"
- label="寮�濮嬫椂闂�"
+ </u-form-item> -->
+ <!-- approveType=2 璇峰亣鐩稿叧瀛楁 -->
+ <template v-if="approveType === 2">
+ <u-form-item prop="startDate"
+ label="寮�濮嬫椂闂�"
+ required>
+ <u-input v-model="form.startDate"
+ readonly
+ placeholder="璇峰亣寮�濮嬫椂闂�"
+ @click="showStartDatePicker" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showStartDatePicker"></up-icon>
+ </template>
+ </u-form-item>
+ <u-form-item prop="endDate"
+ label="缁撴潫鏃堕棿"
+ required>
+ <u-input v-model="form.endDate"
+ readonly
+ placeholder="璇峰亣缁撴潫鏃堕棿"
+ @click="showEndDatePicker" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showEndDatePicker"></up-icon>
+ </template>
+ </u-form-item>
+ </template>
+ <!-- approveType=3 鍑哄樊鐩稿叧瀛楁 -->
+ <u-form-item v-if="approveType === 3"
+ prop="location"
+ label="鍑哄樊鍦扮偣"
required>
- <u-input v-model="form.startDate"
- readonly
- placeholder="璇峰亣寮�濮嬫椂闂�"
- @click="showStartDatePicker" />
- <template #right>
- <up-icon name="arrow-right"
- @click="showStartDatePicker"></up-icon>
- </template>
+ <u-input v-model="form.location"
+ placeholder="璇疯緭鍏ュ嚭宸湴鐐�"
+ clearable />
</u-form-item>
- <u-form-item prop="endDate"
- label="缁撴潫鏃堕棿"
+ <!-- approveType=4 鎶ラ攢鐩稿叧瀛楁 -->
+ <u-form-item v-if="approveType === 4"
+ prop="price"
+ label="鎶ラ攢閲戦"
required>
- <u-input v-model="form.endDate"
- readonly
- placeholder="璇峰亣缁撴潫鏃堕棿"
- @click="showEndDatePicker" />
- <template #right>
- <up-icon name="arrow-right"
- @click="showEndDatePicker"></up-icon>
- </template>
+ <u-input v-model="form.price"
+ type="number"
+ placeholder="璇疯緭鍏ユ姤閿�閲戦"
+ clearable />
</u-form-item>
</template>
- <!-- approveType=3 鍑哄樊鐩稿叧瀛楁 -->
- <u-form-item v-if="approveType === 3"
- prop="location"
- label="鍑哄樊鍦扮偣"
- required>
- <u-input v-model="form.location"
- placeholder="璇疯緭鍏ュ嚭宸湴鐐�"
- clearable />
- </u-form-item>
- <!-- approveType=4 鎶ラ攢鐩稿叧瀛楁 -->
- <u-form-item v-if="approveType === 4"
- prop="price"
- label="鎶ラ攢閲戦"
- required>
- <u-input v-model="form.price"
- type="number"
- placeholder="璇疯緭鍏ユ姤閿�閲戦"
- clearable />
+ <!-- 鎶ヤ环瀹℃壒璇︽儏 -->
+ <view v-if="isQuotationApproval"
+ style="margin: 20rpx 0;">
+ <u-divider text="鎶ヤ环璇︽儏"
+ text-size="28rpx"
+ color="#2979ff"></u-divider>
+ <u-skeleton :loading="quotationLoading"
+ rows="3"
+ animated>
+ <view v-if="!currentQuotation || !currentQuotation.quotationNo"
+ style="padding: 40rpx; text-align: center; color: #999;">
+ 鏈煡璇㈠埌瀵瑰簲鎶ヤ环璇︽儏
+ </view>
+ <view v-else>
+ <u-cell-group :border="false">
+ <u-cell title="鎶ヤ环鍗曞彿"
+ :value="currentQuotation.quotationNo"></u-cell>
+ <u-cell title="瀹㈡埛鍚嶇О"
+ :value="currentQuotation.customer"></u-cell>
+ <u-cell title="涓氬姟鍛�"
+ :value="currentQuotation.salesperson"></u-cell>
+ <u-cell title="鎶ヤ环鏃ユ湡"
+ :value="currentQuotation.quotationDate"></u-cell>
+ <u-cell title="鏈夋晥鏈熻嚦"
+ :value="currentQuotation.validDate"></u-cell>
+ <u-cell title="浠樻鏂瑰紡"
+ :value="currentQuotation.paymentMethod"></u-cell>
+ <u-cell title="鎶ヤ环鎬婚">
+ <template #value>
+ <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
+ </text>
+ </template>
+ </u-cell>
+ </u-cell-group>
+ <view style="margin-top: 20rpx; padding: 0 30rpx;">
+ <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">浜у搧鏄庣粏</view>
+ <view v-for="(item, index) in (currentQuotation.products || [])"
+ :key="index"
+ style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
+ <view style="display: flex; justify-content: space-between;">
+ <text style="font-weight: bold;">{{ item.product }}</text>
+ <text style="color: #e6a23c;">楼{{ Number(item.unitPrice ?? 0).toFixed(2) }}</text>
+ </view>
+ <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
+ 瑙勬牸: {{ item.specification }} | 鍗曚綅: {{ item.unit }}
+ </view>
+ </view>
+ </view>
+ <view v-if="currentQuotation.remark"
+ style="margin-top: 20rpx; padding: 0 30rpx;">
+ <view style="font-size: 28rpx; font-weight: bold;">澶囨敞</view>
+ <view style="font-size: 26rpx; color: #666; margin-top: 10rpx;">{{ currentQuotation.remark }}</view>
+ </view>
+ </view>
+ </u-skeleton>
+ </view>
+ <!-- 閲囪喘瀹℃壒璇︽儏 -->
+ <view v-if="isPurchaseApproval"
+ style="margin: 20rpx 0;">
+ <u-divider text="閲囪喘璇︽儏"
+ text-size="28rpx"
+ color="#2979ff"></u-divider>
+ <u-skeleton :loading="purchaseLoading"
+ rows="3"
+ animated>
+ <view v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
+ style="padding: 40rpx; text-align: center; color: #999;">
+ 鏈煡璇㈠埌瀵瑰簲閲囪喘璇︽儏
+ </view>
+ <view v-else>
+ <u-cell-group :border="false">
+ <u-cell title="閲囪喘鍚堝悓鍙�"
+ :value="currentPurchase.purchaseContractNumber"></u-cell>
+ <u-cell title="渚涘簲鍟嗗悕绉�"
+ :value="currentPurchase.supplierName"></u-cell>
+ <u-cell title="椤圭洰鍚嶇О"
+ :value="currentPurchase.projectName"></u-cell>
+ <u-cell title="閿�鍞悎鍚屽彿"
+ :value="currentPurchase.salesContractNo"></u-cell>
+ <u-cell title="绛捐鏃ユ湡"
+ :value="currentPurchase.executionDate"></u-cell>
+ <u-cell title="褰曞叆鏃ユ湡"
+ :value="currentPurchase.entryDate"></u-cell>
+ <u-cell title="浠樻鏂瑰紡"
+ :value="currentPurchase.paymentMethod"></u-cell>
+ <u-cell title="鍚堝悓閲戦">
+ <template #value>
+ <text style="font-size: 32rpx; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
+ </text>
+ </template>
+ </u-cell>
+ </u-cell-group>
+ <view style="margin-top: 20rpx; padding: 0 30rpx;">
+ <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">浜у搧鏄庣粏</view>
+ <view v-for="(item, index) in (currentPurchase.productData || [])"
+ :key="index"
+ style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
+ <view style="display: flex; justify-content: space-between;">
+ <text style="font-weight: bold;">{{ item.productCategory }}</text>
+ <text style="color: #e6a23c;">楼{{ Number(item.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</text>
+ </view>
+ <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
+ 瑙勬牸: {{ item.specificationModel }} | 鏁伴噺: {{ item.quantity }} {{ item.unit }}
+ </view>
+ <view style="font-size: 24rpx; color: #999; margin-top: 4rpx;">
+ 鍚◣鍗曚环: 楼{{ Number(item.taxInclusiveUnitPrice ?? 0).toFixed(2) }}
+ </view>
+ </view>
+ </view>
+ </view>
+ </u-skeleton>
+ </view>
+ <!-- 鍙戣揣瀹℃壒璇︽儏 -->
+ <view v-if="isDeliveryApproval"
+ style="margin: 20rpx 0;">
+ <u-divider text="鍙戣揣璇︽儏"
+ text-size="28rpx"
+ color="#2979ff"></u-divider>
+ <u-skeleton :loading="deliveryLoading"
+ rows="3"
+ animated>
+ <view v-if="!currentDelivery || !currentDelivery.shippingInfo"
+ style="padding: 40rpx; text-align: center; color: #999;">
+ 鏈煡璇㈠埌瀵瑰簲鍙戣揣璇︽儏
+ </view>
+ <view v-else>
+ <u-cell-group :border="false">
+ <u-cell title="閿�鍞鍗�"
+ :value="currentDelivery.shippingInfo.salesContractNo || '--'"></u-cell>
+ <u-cell title="鍙戣揣璁㈠崟鍙�"
+ :value="currentDelivery.shippingInfo.shippingNo || '--'"></u-cell>
+ <u-cell title="瀹㈡埛鍚嶇О"
+ :value="currentDelivery.shippingInfo.customerName || '--'"></u-cell>
+ <u-cell title="鍙戣揣绫诲瀷"
+ :value="currentDelivery.shippingInfo.type || '--'"></u-cell>
+ <u-cell title="鍙戣揣鏃ユ湡"
+ :value="currentDelivery.shippingInfo.shippingDate || '--'"></u-cell>
+ <u-cell title="瀹℃牳鐘舵��"
+ :value="currentDelivery.shippingInfo.status || '--'"></u-cell>
+ <u-cell title="鍙戣揣杞︾墝鍙�"
+ :value="currentDelivery.shippingInfo.shippingCarNumber || '--'"></u-cell>
+ <u-cell title="蹇�掑叕鍙�"
+ :value="currentDelivery.shippingInfo.expressCompany || '--'"></u-cell>
+ <u-cell title="蹇�掑崟鍙�"
+ :value="currentDelivery.shippingInfo.expressNumber || '--'"></u-cell>
+ </u-cell-group>
+ <view style="margin-top: 20rpx; padding: 0 30rpx;">
+ <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">浜у搧鏄庣粏</view>
+ <view v-for="(item, index) in deliveryProductList"
+ :key="index"
+ style="background: #f8f8f8; border-radius: 8rpx; padding: 20rpx; margin-bottom: 10rpx;">
+ <view style="display: flex; justify-content: space-between;">
+ <text style="font-weight: bold;">{{ item.productName }}</text>
+ <text style="color: #2979ff;">鏁伴噺: {{ item.deliveryQuantity }}</text>
+ </view>
+ <view style="font-size: 24rpx; color: #666; margin-top: 10rpx;">
+ 瑙勬牸: {{ item.specificationModel }}
+ </view>
+ <view v-if="item.batchNo"
+ style="font-size: 24rpx; color: #999; margin-top: 4rpx;">
+ 鎵瑰彿: {{ item.batchNo }}
+ </view>
+ </view>
+ </view>
+ <view v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
+ style="margin-top: 20rpx; padding: 0 30rpx;">
+ <view style="font-size: 28rpx; font-weight: bold; margin-bottom: 10rpx;">鍙戣揣鍥剧墖</view>
+ <CommonUpload :model-value="currentDelivery.shippingInfo.storageBlobVOs"
+ disabled />
+ </view>
+ </view>
+ </u-skeleton>
+ </view>
+ <u-form-item v-if="operationType !== 'detail'"
+ label="鍥剧墖闄勪欢"
+ prop="storageBlobDTOS"
+ border-bottom>
+ <CommonUpload v-model="form.storageBlobDTOS" />
</u-form-item>
</u-form>
<!-- 閫夋嫨鍣ㄥ脊绐� -->
- <up-action-sheet :show="showPicker"
- :actions="productOptions"
- title="閫夋嫨閮ㄩ棬"
- @select="onConfirm"
- @close="showPicker = false" />
- <!-- 鏃ユ湡閫夋嫨鍣� -->
- <up-popup :show="showDate"
- mode="bottom"
- @close="showDate = false">
- <up-datetime-picker :show="true"
- v-model="currentDate"
- @confirm="onDateConfirm"
- @cancel="showDate = false"
- mode="date" />
- </up-popup>
- <!-- 璇峰亣寮�濮嬫椂闂撮�夋嫨鍣� -->
- <up-popup :show="showStartDate"
- mode="bottom"
- @close="showStartDate = false">
- <up-datetime-picker :show="true"
- v-model="startDateValue"
- @confirm="onStartDateConfirm"
- @cancel="showStartDate = false"
- mode="date" />
- </up-popup>
- <!-- 璇峰亣缁撴潫鏃堕棿閫夋嫨鍣� -->
- <up-popup :show="showEndDate"
- mode="bottom"
- @close="showEndDate = false">
- <up-datetime-picker :show="true"
- v-model="endDateValue"
- @confirm="onEndDateConfirm"
- @cancel="showEndDate = false"
- mode="date" />
- </up-popup>
- <!-- 瀹℃牳娴佺▼鍖哄煙 -->
- <view class="approval-process">
- <view class="approval-header">
- <text class="approval-title">瀹℃牳娴佺▼</text>
- <text class="approval-desc">姣忎釜姝ラ鍙兘閫夋嫨涓�涓鎵逛汉</text>
- </view>
- <view class="approval-steps">
- <view v-for="(step, stepIndex) in approverNodes"
- :key="stepIndex"
- class="approval-step">
- <view class="step-dot"></view>
- <view class="step-title">
- <text>瀹℃壒浜�</text>
- </view>
- <view class="approver-container">
- <view v-if="step.nickName"
- class="approver-item">
- <view class="approver-avatar">
- <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
- <view class="status-dot"></view>
- </view>
- <view class="approver-info">
- <text class="approver-name">{{ step.nickName }}</text>
- </view>
- <view class="delete-approver-btn"
- @click="removeApprover(stepIndex)">脳</view>
- </view>
- <view v-else
- class="add-approver-btn"
- @click="addApprover(stepIndex)">
- <view class="add-circle">+</view>
- <text class="add-label">閫夋嫨瀹℃壒浜�</text>
- </view>
- </view>
- <view class="step-line"
- v-if="stepIndex < approverNodes.length - 1"></view>
- <view class="delete-step-btn"
- v-if="approverNodes.length > 1"
- @click="removeApprovalStep(stepIndex)">鍒犻櫎鑺傜偣</view>
- </view>
- </view>
- <view class="add-step-btn">
- <u-button icon="plus"
- plain
- type="primary"
- style="width: 100%"
- @click="addApprovalStep">鏂板鑺傜偣</u-button>
- </view>
- </view>
+ <template v-if="operationType !== 'detail'">
+ <up-action-sheet :show="showPicker"
+ :actions="productOptions"
+ title="閫夋嫨閮ㄩ棬"
+ @select="onConfirm"
+ @close="showPicker = false" />
+ <!-- 鏃ユ湡閫夋嫨鍣� -->
+ <up-popup :show="showDate"
+ mode="bottom"
+ @close="showDate = false">
+ <up-datetime-picker :show="true"
+ v-model="currentDate"
+ @confirm="onDateConfirm"
+ @cancel="showDate = false"
+ mode="date" />
+ </up-popup>
+ <!-- 璇峰亣寮�濮嬫椂闂撮�夋嫨鍣� -->
+ <up-popup :show="showStartDate"
+ mode="bottom"
+ @close="showStartDate = false">
+ <up-datetime-picker :show="true"
+ v-model="startDateValue"
+ @confirm="onStartDateConfirm"
+ @cancel="showStartDate = false"
+ mode="date" />
+ </up-popup>
+ <!-- 璇峰亣缁撴潫鏃堕棿閫夋嫨鍣� -->
+ <up-popup :show="showEndDate"
+ mode="bottom"
+ @close="showEndDate = false">
+ <up-datetime-picker :show="true"
+ v-model="endDateValue"
+ @confirm="onEndDateConfirm"
+ @cancel="showEndDate = false"
+ mode="date" />
+ </up-popup>
+ </template>
<!-- 搴曢儴鎸夐挳 -->
- <view class="footer-btns">
+ <view class="footer-btns"
+ v-if="operationType !== 'detail'">
<u-button class="cancel-btn"
@click="goBack">鍙栨秷</u-button>
<u-button class="save-btn"
@@ -201,8 +338,17 @@
</template>
<script setup>
- import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
+ import {
+ ref,
+ onMounted,
+ onUnmounted,
+ reactive,
+ toRefs,
+ computed,
+ watch,
+ } from "vue";
import PageHeader from "@/components/PageHeader.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
import useUserStore from "@/store/modules/user";
import { formatDateToYMD } from "@/utils/ruoyi";
import {
@@ -210,14 +356,16 @@
approveProcessGetInfo,
approveProcessAdd,
approveProcessUpdate,
+ getDeliveryDetailByShippingNo,
} from "@/api/collaborativeApproval/approvalProcess";
+ import { getQuotationList } from "@/api/salesManagement/salesQuotation";
+ import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger";
const showToast = message => {
uni.showToast({
title: message,
icon: "none",
});
};
- import { userListNoPageByTenantId } from "@/api/system/user";
const data = reactive({
form: {
@@ -229,8 +377,7 @@
approveDeptId: "",
approveReason: "",
checkResult: "",
- tempFileIds: [],
- approverList: [], // 鏂板瀛楁锛屽瓨鍌ㄦ墍鏈夎妭鐐圭殑瀹℃壒浜篿d
+ storageBlobDTOS: [],
startDate: "",
endDate: "",
location: "",
@@ -258,8 +405,6 @@
const productOptions = ref([]);
const operationType = ref("");
const currentApproveStatus = ref("");
- const approverNodes = ref([]);
- const userList = ref([]);
const formRef = ref(null);
const message = ref("");
const showDate = ref(false);
@@ -270,6 +415,19 @@
const endDateValue = ref(Date.now());
const userStore = useUserStore();
const approveType = ref(0);
+ const isInitialLoading = ref(false);
+
+ const quotationLoading = ref(false);
+ const currentQuotation = ref({});
+ const purchaseLoading = ref(false);
+ const currentPurchase = ref({});
+ const deliveryLoading = ref(false);
+ const currentDelivery = ref({});
+ const deliveryProductList = ref([]);
+
+ const isQuotationApproval = computed(() => Number(approveType.value) === 6);
+ const isPurchaseApproval = computed(() => Number(approveType.value) === 5);
+ const isDeliveryApproval = computed(() => Number(approveType.value) === 7);
const getProductOptions = () => {
getDept().then(res => {
@@ -279,20 +437,133 @@
}));
});
};
- const fileList = ref([]);
- let nextApproverId = 2;
const getCurrentinfo = () => {
userStore.getInfo().then(res => {
form.value.approveDeptId = res.user.tenantId;
console.log(res.user.tenantId, "res.user.tenantId");
});
};
+
+ // 鏄剧ず鏃ユ湡閫夋嫨鍣�
+ const showDatePicker = () => {
+ showDate.value = true;
+ };
+
+ // 纭鏃ユ湡閫夋嫨
+ const onDateConfirm = e => {
+ form.value.approveTime = formatDateToYMD(e.value);
+ currentDate.value = formatDateToYMD(e.value);
+ showDate.value = false;
+ };
+
+ // 鏄剧ず璇峰亣寮�濮嬫椂闂撮�夋嫨鍣�
+ const showStartDatePicker = () => {
+ showStartDate.value = true;
+ };
+
+ // 纭璇峰亣寮�濮嬫椂闂撮�夋嫨
+ const onStartDateConfirm = e => {
+ form.value.startDate = formatDateToYMD(e.value);
+ showStartDate.value = false;
+ };
+
+ const showEndDatePicker = () => {
+ showEndDate.value = true;
+ };
+
+ // 纭璇峰亣缁撴潫鏃堕棿閫夋嫨
+ const onEndDateConfirm = e => {
+ form.value.endDate = formatDateToYMD(e.value);
+ showEndDate.value = false;
+ };
+
+ const fetchDetailData = async row => {
+ // 鎶ヤ环瀹℃壒
+ if (isQuotationApproval.value) {
+ const quotationNo = row?.approveReason;
+ if (quotationNo) {
+ quotationLoading.value = true;
+ getQuotationList({ quotationNo })
+ .then(res => {
+ const records = res?.data?.records || [];
+ currentQuotation.value = records[0] || {};
+ })
+ .finally(() => {
+ quotationLoading.value = false;
+ });
+ }
+ }
+
+ // 閲囪喘瀹℃壒
+ if (isPurchaseApproval.value) {
+ const purchaseContractNumber = row?.approveReason;
+ if (purchaseContractNumber) {
+ purchaseLoading.value = true;
+ getPurchaseByCode({ purchaseContractNumber })
+ .then(res => {
+ currentPurchase.value = res;
+ })
+ .catch(err => {
+ console.error("鏌ヨ閲囪喘璇︽儏澶辫触:", err);
+ })
+ .finally(() => {
+ purchaseLoading.value = false;
+ });
+ }
+ }
+
+ // 鍙戣揣瀹℃壒
+ if (isDeliveryApproval.value) {
+ const deliveryNo = row?.approveReason;
+ if (deliveryNo) {
+ deliveryLoading.value = true;
+ currentDelivery.value = {};
+ deliveryProductList.value = [];
+ getDeliveryDetailByShippingNo({ shippingNo: deliveryNo })
+ .then(res => {
+ const detailData = res?.data || res || {};
+ currentDelivery.value = detailData;
+ deliveryProductList.value =
+ detailData.shippingProductDetailDtoList || [];
+ })
+ .catch(err => {
+ console.error("鏌ヨ鍙戣揣璇︽儏澶辫触:", err);
+ })
+ .finally(() => {
+ deliveryLoading.value = false;
+ });
+ }
+ }
+ };
+
+ // 鐩戝惉瀹℃壒浜嬬敱鍙樺寲锛屽鏋滄槸鐗瑰畾瀹℃壒绫诲瀷鍒欏皾璇曡幏鍙栬鎯�
+ watch(
+ () => form.value.approveReason,
+ newVal => {
+ if (isInitialLoading.value) return;
+ if (
+ newVal &&
+ (isQuotationApproval.value ||
+ isPurchaseApproval.value ||
+ isDeliveryApproval.value)
+ ) {
+ // 寤惰繜涓�浼氬啀璇锋眰锛岄伩鍏嶈緭鍏ヨ繃绋嬩腑棰戠箒瑙﹀彂
+ debounceFetchDetail();
+ }
+ }
+ );
+
+ let timer = null;
+ const debounceFetchDetail = () => {
+ if (timer) clearTimeout(timer);
+ timer = setTimeout(() => {
+ fetchDetailData(form.value);
+ }, 800);
+ };
+
onMounted(async () => {
try {
getProductOptions();
- userListNoPageByTenantId().then(res => {
- userList.value = res.data;
- });
form.value.approveUser = userStore.id;
form.value.approveUserName = userStore.nickName;
form.value.approveTime = getCurrentDate();
@@ -302,57 +573,39 @@
approveType.value = uni.getStorageSync("approveType") || 0;
// 濡傛灉鏄紪杈戞ā寮忥紝浠庢湰鍦板瓨鍌ㄨ幏鍙栨暟鎹�
- if (operationType.value === "edit") {
+ if (operationType.value === "edit" || operationType.value === "detail") {
const storedData = uni.getStorageSync("invoiceLedgerEditRow");
if (storedData) {
const row = JSON.parse(storedData);
- fileList.value = row.commonFileList || [];
- form.value.tempFileIds = fileList.value.map(file => file.id);
currentApproveStatus.value = row.approveStatus;
- approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then(
- res => {
+ isInitialLoading.value = true;
+ approveProcessGetInfo({ id: row.approveId, approveReason: "1" })
+ .then(res => {
form.value = { ...res.data };
- // 鍙嶆樉瀹℃壒浜�
- if (res.data && res.data.approveUserIds) {
- const userIds = res.data.approveUserIds.split(",");
- approverNodes.value = userIds.map((userId, idx) => {
- const userIdNum = parseInt(userId.trim());
- // 浠巙serList涓壘鍒板搴旂殑鐢ㄦ埛淇℃伅
- const userInfo = userList.value.find(
- user => user.userId === userIdNum
- );
- return {
- id: idx + 1,
- userId: userIdNum,
- nickName: userInfo ? userInfo.nickName : null,
- };
- });
- nextApproverId = userIds.length + 1;
- } else {
- // 鏂板妯″紡锛屽垵濮嬪寲涓�涓┖鐨勫鎵硅妭鐐�
- approverNodes.value = [{ id: 1, userId: null, nickName: null }];
- nextApproverId = 2;
+ // 璁剧疆鍥剧墖鍒楄〃鏄剧ず
+ const fileData =
+ res.data.storageBlobVOS || res.data.commonFileList || [];
+ if (fileData.length > 0) {
+ form.value.storageBlobDTOS = fileData;
}
- }
- );
+ // 鑾峰彇棰濆璇︽儏
+ fetchDetailData(res.data);
+ })
+ .finally(() => {
+ // 寤惰繜涓�浼氶噸缃紝纭繚 watch 涓嶄細琚Е鍙�
+ setTimeout(() => {
+ isInitialLoading.value = false;
+ }, 100);
+ });
}
- } else {
- // 鏂板妯″紡锛屽垵濮嬪寲涓�涓┖鐨勫鎵硅妭鐐�
- approverNodes.value = [{ id: 1, userId: null }];
}
-
- // 鐩戝惉鑱旂郴浜洪�夋嫨浜嬩欢
- uni.$on("selectContact", handleSelectContact);
} catch (error) {
- console.error("鑾峰彇閮ㄩ棬鏁版嵁澶辫触:", error);
+ console.error("鑾峰彇鏁版嵁澶辫触:", error);
}
});
- onUnmounted(() => {
- // 绉婚櫎浜嬩欢鐩戝惉
- uni.$off("selectContact", handleSelectContact);
- });
+ onUnmounted(() => {});
const onConfirm = item => {
// 璁剧疆閫変腑鐨勯儴闂�
@@ -375,13 +628,6 @@
};
const submitForm = () => {
- // 妫�鏌ユ瘡涓鎵规楠ゆ槸鍚﹂兘鏈夊鎵逛汉
- const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
- if (hasEmptyStep) {
- showToast("璇蜂负姣忎釜瀹℃壒姝ラ閫夋嫨瀹℃壒浜�");
- return;
- }
-
// 鎵嬪姩妫�鏌ュ繀濉瓧娈碉紝闃叉鍥犳暟鎹被鍨嬮棶棰樺鑷寸殑鏍¢獙澶辫触
if (!form.value.approveReason || !form.value.approveReason.trim()) {
showToast("璇疯緭鍏ョ敵璇蜂簨鐢�");
@@ -406,26 +652,8 @@
.then(valid => {
if (valid) {
// 琛ㄥ崟鏍¢獙閫氳繃锛屽彲浠ユ彁浜ゆ暟鎹�
- // 鏀堕泦鎵�鏈夎妭鐐圭殑瀹℃壒浜篿d
- console.log("approverNodes---", approverNodes.value);
- form.value.approveUserIds = approverNodes.value
- .map(node => node.userId)
- .join(",");
form.value.approveType = approveType.value;
form.value.approveDeptId = Number(form.value.approveDeptId);
- // const submitForm = {
- // approveDeptId: form.value.approveDeptId,
- // approveDeptName: form.value.approveDeptName,
- // approveReason: form.value.approveReason,
- // approveTime: form.value.approveTime,
- // approveType: form.value.approveType,
- // approveUser: form.value.approveUser,
- // approveUserIds: form.value.approveUserIds,
- // endDate: form.value.endDate,
- // startDate: form.value.startDate,
- // };
- // console.log("form.value---", form.value);
- // console.log("submitForm", submitForm);
if (operationType.value === "add" || currentApproveStatus.value == 3) {
approveProcessAdd(form.value).then(res => {
@@ -461,77 +689,6 @@
});
};
- // 澶勭悊鑱旂郴浜洪�夋嫨缁撴灉
- const handleSelectContact = data => {
- const { stepIndex, contact } = data;
- // 灏嗛�変腑鐨勮仈绯讳汉璁剧疆涓哄搴斿鎵规楠ょ殑瀹℃壒浜�
- approverNodes.value[stepIndex].userId = contact.userId;
- approverNodes.value[stepIndex].nickName = contact.nickName;
- };
-
- const addApprover = stepIndex => {
- // 璺宠浆鍒拌仈绯讳汉閫夋嫨椤甸潰
- uni.setStorageSync("stepIndex", stepIndex);
- uni.navigateTo({
- url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
- });
- };
-
- const addApprovalStep = () => {
- // 娣诲姞鏂扮殑瀹℃壒姝ラ
- approverNodes.value.push({ userId: null, nickName: null });
- };
-
- const removeApprover = stepIndex => {
- // 绉婚櫎瀹℃壒浜�
- approverNodes.value[stepIndex].userId = null;
- approverNodes.value[stepIndex].nickName = null;
- };
-
- const removeApprovalStep = stepIndex => {
- // 纭繚鑷冲皯淇濈暀涓�涓鎵规楠�
- if (approverNodes.value.length > 1) {
- approverNodes.value.splice(stepIndex, 1);
- } else {
- uni.showToast({
- title: "鑷冲皯闇�瑕佷竴涓鎵规楠�",
- icon: "none",
- });
- }
- };
- // 鏄剧ず鏃ユ湡閫夋嫨鍣�
- const showDatePicker = () => {
- showDate.value = true;
- };
-
- // 纭鏃ユ湡閫夋嫨
- const onDateConfirm = e => {
- form.value.approveTime = formatDateToYMD(e.value);
- currentDate.value = formatDateToYMD(e.value);
- showDate.value = false;
- };
-
- // 鏄剧ず璇峰亣寮�濮嬫椂闂撮�夋嫨鍣�
- const showStartDatePicker = () => {
- showStartDate.value = true;
- };
-
- // 纭璇峰亣寮�濮嬫椂闂撮�夋嫨
- const onStartDateConfirm = e => {
- form.value.startDate = formatDateToYMD(e.value);
- showStartDate.value = false;
- };
-
- const showEndDatePicker = () => {
- showEndDate.value = true;
- };
-
- // 纭璇峰亣缁撴潫鏃堕棿閫夋嫨
- const onEndDateConfirm = e => {
- form.value.endDate = formatDateToYMD(e.value);
- showEndDate.value = false;
- };
-
// 鑾峰彇褰撳墠鏃ユ湡骞舵牸寮忓寲涓� YYYY-MM-DD
function getCurrentDate() {
const today = new Date();
@@ -544,238 +701,8 @@
<style scoped lang="scss">
@import "@/static/scss/form-common.scss";
-
- .approval-process {
- background: #fff;
- margin: 16px;
- border-radius: 16px;
- padding: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
- }
-
- .approval-header {
- margin-bottom: 16px;
- }
-
- .approval-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
- display: block;
- margin-bottom: 4px;
- }
-
- .approval-desc {
- font-size: 12px;
- color: #999;
- }
-
- /* 鏍峰紡澧炲己涓衡�滅畝娲佸皬鍦嗗湀椋庢牸鈥� */
- .approval-steps {
- padding-left: 22px;
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- left: 11px;
- top: 40px;
- bottom: 40px;
- width: 2px;
- background: linear-gradient(
- to bottom,
- #e6f7ff 0%,
- #bae7ff 50%,
- #91d5ff 100%
- );
- border-radius: 1px;
- }
- }
-
- .approval-step {
- position: relative;
- margin-bottom: 24px;
-
- &::before {
- content: "";
- position: absolute;
- left: -18px;
- top: 14px; // 浠� 8px 璋冩暣涓� 14px锛屼笌鏂囧瓧涓績瀵归綈
- width: 12px;
- height: 12px;
- background: #fff;
- border: 3px solid #006cfb;
- border-radius: 50%;
- z-index: 2;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- }
-
- .step-title {
- top: 12px;
- margin-bottom: 12px;
- position: relative;
- margin-left: 6px;
- }
-
- .step-title text {
- font-size: 14px;
- color: #666;
- background: #f0f0f0;
- padding: 4px 12px;
- border-radius: 12px;
- position: relative;
- line-height: 1.4; // 纭繚鏂囧瓧琛岄珮涓�鑷�
- }
-
- .approver-item {
- display: flex;
- align-items: center;
- background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
- border-radius: 16px;
- padding: 16px;
- gap: 12px;
- position: relative;
- border: 1px solid #e6f7ff;
- box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08);
- transition: all 0.3s ease;
- }
-
- .approver-avatar {
- width: 48px;
- height: 48px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
- }
-
- .avatar-text {
- color: #fff;
- font-size: 18px;
- font-weight: 600;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- }
-
- .approver-info {
- flex: 1;
- position: relative;
- }
-
- .approver-name {
- display: block;
- font-size: 16px;
- color: #333;
- font-weight: 500;
- position: relative;
- }
-
- .approver-dept {
- font-size: 12px;
- color: #999;
- background: rgba(0, 108, 251, 0.05);
- padding: 2px 8px;
- border-radius: 8px;
- display: inline-block;
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- left: 4px;
- top: 50%;
- transform: translateY(-50%);
- width: 2px;
- height: 2px;
- background: #006cfb;
- border-radius: 50%;
- }
- }
-
- .delete-approver-btn {
- font-size: 16px;
- color: #ff4d4f;
- background: linear-gradient(
- 135deg,
- rgba(255, 77, 79, 0.1) 0%,
- rgba(255, 77, 79, 0.05) 100%
- );
- width: 28px;
- height: 28px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- position: relative;
- }
-
- .add-approver-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
- border: 2px dashed #006cfb;
- border-radius: 16px;
- padding: 20px;
- color: #006cfb;
- font-size: 14px;
- position: relative;
- transition: all 0.3s ease;
-
- &::before {
- content: "";
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 32px;
- height: 32px;
- border: 2px solid #006cfb;
- border-radius: 50%;
- opacity: 0;
- transition: all 0.3s ease;
- }
- }
-
- .delete-step-btn {
- color: #ff4d4f;
- font-size: 12px;
- background: linear-gradient(
- 135deg,
- rgba(255, 77, 79, 0.1) 0%,
- rgba(255, 77, 79, 0.05) 100%
- );
- padding: 6px 12px;
- border-radius: 12px;
- display: inline-block;
- position: relative;
- transition: all 0.3s ease;
-
- &::before {
- content: "";
- position: absolute;
- left: 6px;
- top: 50%;
- transform: translateY(-50%);
- width: 4px;
- height: 4px;
- background: #ff4d4f;
- border-radius: 50%;
- }
- }
-
- .step-line {
- display: none; // 闅愯棌鍘熸潵鐨勭嚎鏉★紝浣跨敤浼厓绱犱唬鏇�
- }
-
- .add-step-btn {
- display: flex;
- align-items: center;
- justify-content: center;
+ .account-detail {
+ background-color: #fff;
}
.footer-btns {
position: fixed;
@@ -809,121 +736,5 @@
background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
- }
-
- // 鍔ㄧ敾瀹氫箟
- @keyframes pulse {
- 0% {
- transform: scale(1);
- opacity: 1;
- }
- 50% {
- transform: scale(1.2);
- opacity: 0.7;
- }
- 100% {
- transform: scale(1);
- opacity: 1;
- }
- }
-
- @keyframes rotate {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
-
- @keyframes ripple {
- 0% {
- transform: translate(-50%, -50%) scale(0.8);
- opacity: 1;
- }
- 100% {
- transform: translate(-50%, -50%) scale(1.6);
- opacity: 0;
- }
- }
-
- /* 濡傛灉宸叉湁 .step-line锛岃繖閲屾洿绮惧噯瀹氫綅鍒板乏渚т笌灏忓渾鐐瑰榻� */
- .step-line {
- position: absolute;
- left: 4px;
- top: 48px;
- width: 2px;
- height: calc(100% - 48px);
- background: #e5e7eb;
- }
-
- .approver-container {
- display: flex;
- align-items: center;
- background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
- border-radius: 16px;
- gap: 12px;
- padding: 10px 0;
- background: transparent;
- border: none;
- box-shadow: none;
- }
-
- .approver-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 8px 10px;
- background: transparent;
- border: none;
- box-shadow: none;
- border-radius: 0;
- }
-
- .approver-avatar {
- position: relative;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: #f3f4f6;
- border: 2px solid #e5e7eb;
- display: flex;
- align-items: center;
- justify-content: center;
- animation: none; /* 绂佺敤鏃嬭浆绛夊姩鐢伙紝鍥炲綊绠�娲� */
- }
-
- .avatar-text {
- font-size: 14px;
- color: #374151;
- font-weight: 600;
- }
-
- .add-approver-btn {
- display: flex;
- align-items: center;
- gap: 8px;
- background: transparent;
- border: none;
- box-shadow: none;
- padding: 0;
- }
-
- .add-approver-btn .add-circle {
- width: 40px;
- height: 40px;
- border: 2px dashed #a0aec0;
- border-radius: 50%;
- color: #6b7280;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 22px;
- line-height: 1;
- }
-
- .add-approver-btn .add-label {
- color: #3b82f6;
- font-size: 14px;
}
</style>
\ No newline at end of file
diff --git a/src/pages/cooperativeOffice/collaborativeApproval/index.vue b/src/pages/cooperativeOffice/collaborativeApproval/index.vue
index bc69a5f..910cdc3 100644
--- a/src/pages/cooperativeOffice/collaborativeApproval/index.vue
+++ b/src/pages/cooperativeOffice/collaborativeApproval/index.vue
@@ -97,13 +97,20 @@
</view>
<view class="detail-row">
<view class="actions">
- <!-- <u-button type="primary"
+ <u-button type="primary"
size="small"
class="action-btn edit"
- :disabled="item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8"
+ v-if="!(item.approveStatus == 2 || item.approveStatus == 1 || item.approveStatus == 4 || item.approveStatus == 8 || item.approveType == 5 || item.approveType == 6 || item.approveType == 7)"
@click="handleItemClick(item)">
缂栬緫
- </u-button> -->
+ </u-button>
+ <u-button type="info"
+ v-if="item.approveType == 5 || item.approveType == 6 || item.approveType == 7"
+ size="small"
+ class="action-btn detail"
+ @click="handleDetailClick(item)">
+ 璇︽儏
+ </u-button>
<u-button type="success"
size="small"
class="action-btn approve"
@@ -123,13 +130,13 @@
<text>鏆傛棤瀹℃壒鏁版嵁</text>
</view>
<!-- 娴姩鎿嶄綔鎸夐挳 -->
- <!-- <view class="fab-button"
+ <view class="fab-button"
v-if="props.approveType != 5 && props.approveType != 6 && props.approveType != 7"
@click="handleAdd">
<up-icon name="plus"
size="24"
color="#ffffff"></up-icon>
- </view> -->
+ </view>
</view>
</template>
@@ -262,6 +269,17 @@
});
};
+ // 鏌ョ湅璇︽儏
+ const handleDetailClick = item => {
+ uni.setStorageSync("invoiceLedgerEditRow", JSON.stringify(item));
+ uni.setStorageSync("operationType", "detail");
+ uni.setStorageSync("approveId", item.approveId);
+ uni.setStorageSync("approveType", props.approveType);
+ uni.navigateTo({
+ url: "/pages/cooperativeOffice/collaborativeApproval/detail",
+ });
+ };
+
// 娣诲姞鏂拌褰�
const handleAdd = () => {
uni.setStorageSync("operationType", "add");
diff --git a/src/pages/equipmentManagement/repair/add.vue b/src/pages/equipmentManagement/repair/add.vue
index 71de940..73c4eba 100644
--- a/src/pages/equipmentManagement/repair/add.vue
+++ b/src/pages/equipmentManagement/repair/add.vue
@@ -69,6 +69,20 @@
placeholder="璇疯緭鍏ユ姤淇汉"
clearable />
</u-form-item>
+ <u-form-item label="缁翠慨浜�"
+ prop="maintenanceName"
+ border-bottom>
+ <u-input v-model="form.maintenanceName"
+ placeholder="璇疯緭鍏ョ淮淇汉"
+ clearable />
+ </u-form-item>
+ <u-form-item label="缁翠慨椤圭洰"
+ prop="machineryCategory"
+ border-bottom>
+ <u-input v-model="form.machineryCategory"
+ placeholder="璇疯緭鍏ョ淮淇」鐩�"
+ clearable />
+ </u-form-item>
<u-form-item label="鏁呴殰鐜拌薄"
prop="remark"
required
@@ -79,6 +93,11 @@
clearable
count
maxlength="200" />
+ </u-form-item>
+ <u-form-item label="鍥剧墖闄勪欢"
+ prop="storageBlobDTOs"
+ border-bottom>
+ <CommonUpload v-model="form.storageBlobDTOs" />
</u-form-item>
</u-cell-group>
<!-- 鎻愪氦鎸夐挳 -->
@@ -108,8 +127,9 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from "vue";
- import { onShow } from "@dcloudio/uni-app";
+ import { onShow, onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
import {
addRepair,
@@ -132,10 +152,18 @@
// 琛ㄥ崟寮曠敤
const formRef = ref(null);
const operationType = ref("add");
+ const repairId = ref("");
const loading = ref(false);
const showDevice = ref(false);
const showDate = ref(false);
const pickerDateValue = ref(Date.now());
+
+ onLoad(options => {
+ if (options.id) {
+ repairId.value = options.id;
+ }
+ getPageParams();
+ });
// 璁惧閫夐」
const deviceOptions = ref([]);
@@ -169,7 +197,10 @@
deviceModel: undefined, // 瑙勬牸鍨嬪彿
repairTime: dayjs().format("YYYY-MM-DD"), // 鎶ヤ慨鏃ユ湡
repairName: undefined, // 鎶ヤ慨浜�
+ maintenanceName: undefined, // 缁翠慨浜�
+ machineryCategory: undefined, // 缁翠慨椤圭洰
remark: undefined, // 鏁呴殰鐜拌薄
+ storageBlobDTOs: [], // 鍥剧墖闄勪欢
});
// 鎶ヤ慨鐘舵�侀�夐」
@@ -221,7 +252,10 @@
form.value.deviceModel = data.deviceModel;
form.value.repairTime = dayjs(data.repairTime).format("YYYY-MM-DD");
form.value.repairName = data.repairName;
+ form.value.maintenanceName = data.maintenanceName;
+ form.value.machineryCategory = data.machineryCategory;
form.value.remark = data.remark;
+ form.value.storageBlobDTOs = data.storageBlobVOs || [];
repairStatusText.value =
repairStatusOptions.value.find(item => item.value == data.status)
?.name || "";
@@ -328,14 +362,12 @@
};
onShow(() => {
- // 椤甸潰鏄剧ず鏃惰幏鍙栧弬鏁�
- getPageParams();
+ // 椤甸潰鏄剧ず鏃堕�昏緫
});
onMounted(() => {
- // 椤甸潰鍔犺浇鏃惰幏鍙栬澶囧垪琛ㄥ拰鍙傛暟
+ // 椤甸潰鍔犺浇鏃惰幏鍙栬澶囧垪琛�
loadDeviceName();
- getPageParams();
});
// 缁勪欢鍗歌浇鏃舵竻鐞嗗畾鏃跺櫒
@@ -375,7 +407,6 @@
// 鍑嗗鎻愪氦鏁版嵁
const submitData = { ...form.value };
-
const { code } = id
? await editRepair({ id: id, ...submitData })
: await addRepair(submitData);
@@ -396,21 +427,15 @@
// 杩斿洖涓婁竴椤�
const goBack = () => {
- uni.removeStorageSync("repairId");
uni.navigateBack();
};
// 鑾峰彇椤甸潰鍙傛暟
const getPageParams = () => {
- // 浣跨敤uni.getStorageSync鑾峰彇id
- const id = uni.getStorageSync("repairId");
-
// 鏍规嵁鏄惁鏈塱d鍙傛暟鏉ュ垽鏂槸鏂板杩樻槸缂栬緫
- if (id) {
+ if (repairId.value) {
// 缂栬緫妯″紡锛岃幏鍙栬鎯�
- loadForm(id);
- // 鍙�夛細鑾峰彇鍚庢竻闄ゅ瓨鍌ㄧ殑id锛岄伩鍏嶅奖鍝嶅悗缁搷浣�
- uni.removeStorageSync("repairId");
+ loadForm(repairId.value);
} else {
// 鏂板妯″紡
loadForm();
@@ -419,9 +444,7 @@
// 鑾峰彇椤甸潰ID
const getPageId = () => {
- // 浣跨敤uni.getStorageSync鑾峰彇id
- const id = uni.getStorageSync("repairId");
- return id;
+ return repairId.value;
};
</script>
diff --git a/src/pages/equipmentManagement/repair/index.vue b/src/pages/equipmentManagement/repair/index.vue
index e280595..8c41ab1 100644
--- a/src/pages/equipmentManagement/repair/index.vue
+++ b/src/pages/equipmentManagement/repair/index.vue
@@ -58,12 +58,16 @@
<text class="detail-value">{{ item.repairName || '-' }}</text>
</view>
<view class="detail-row">
- <text class="detail-label">鏁呴殰鐜拌薄</text>
- <text class="detail-value">{{ item.remark || '-' }}</text>
- </view>
- <view class="detail-row">
<text class="detail-label">缁翠慨浜�</text>
<text class="detail-value">{{ item.maintenanceName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">缁翠慨椤圭洰</text>
+ <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏁呴殰鐜拌薄</text>
+ <text class="detail-value">{{ item.remark || '-' }}</text>
</view>
<view class="detail-row">
<text class="detail-label">缁翠慨缁撴灉</text>
@@ -208,9 +212,9 @@
const edit = id => {
if (!id) return;
// 浣跨敤uni.setStorageSync瀛樺偍id
- uni.setStorageSync("repairId", id);
+ // uni.setStorageSync("repairId", id);
uni.navigateTo({
- url: "/pages/equipmentManagement/repair/add",
+ url: "/pages/equipmentManagement/repair/add?id=" + id,
});
};
diff --git a/src/pages/equipmentManagement/upkeep/add.vue b/src/pages/equipmentManagement/upkeep/add.vue
index 8dedb1b..5f3bd40 100644
--- a/src/pages/equipmentManagement/upkeep/add.vue
+++ b/src/pages/equipmentManagement/upkeep/add.vue
@@ -1,393 +1,444 @@
<template>
- <view class="upkeep-add">
- <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
- <PageHeader :title="operationType === 'edit' ? '缂栬緫淇濆吇璁″垝' : '鏂板淇濆吇璁″垝'" @back="goBack" />
-
- <!-- 琛ㄥ崟鍐呭 -->
- <u-form ref="formRef" :model="form" :rules="formRules" label-width="110px">
- <!-- 鍩烘湰淇℃伅 -->
- <u-form-item label="璁惧鍚嶇О" prop="deviceNameText" required border-bottom>
- <u-input
- v-model="form.deviceNameText"
- placeholder="璇烽�夋嫨璁惧鍚嶇О"
- readonly
- @click="showDevicePicker"
- clearable
- />
- <template #right>
- <u-icon name="scan" @click="startScan" class="scan-icon" />
- </template>
- </u-form-item>
-
- <u-form-item label="瑙勬牸鍨嬪彿" prop="deviceModel" border-bottom>
- <u-input
- v-model="form.deviceModel"
- placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
- readonly
- clearable
- />
- </u-form-item>
-
- <u-form-item label="璁″垝淇濆吇鏃ユ湡" prop="maintenancePlanTime" required border-bottom>
- <u-input
- v-model="form.maintenancePlanTime"
- placeholder="璇烽�夋嫨璁″垝淇濆吇鏃ユ湡"
- readonly
- @click="showDatePicker"
- clearable
- />
- <template #right>
- <u-icon name="arrow-right" @click="showDatePicker" />
- </template>
- </u-form-item>
-
- <!-- 鎻愪氦鎸夐挳 -->
- <view class="footer-btns">
- <u-button class="cancel-btn" @click="goBack">鍙栨秷</u-button>
- <u-button class="save-btn" @click="sendForm" :loading="loading">淇濆瓨</u-button>
- </view>
- </u-form>
-
- <!-- 璁惧閫夋嫨鍣� -->
- <up-action-sheet
- :show="showDevice"
- :actions="deviceActions"
- title="閫夋嫨璁惧"
- @select="onDeviceConfirm"
- @close="showDevice = false"
- />
-<up-datetime-picker
- :show="showDate"
- v-model="pickerDateValue"
- @confirm="onDateConfirm"
- @cancel="showDate = false"
- mode="date"
- />
-
- </view>
+ <view class="upkeep-add">
+ <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
+ <PageHeader :title="operationType === 'edit' ? '缂栬緫淇濆吇璁″垝' : '鏂板淇濆吇璁″垝'"
+ @back="goBack" />
+ <!-- 琛ㄥ崟鍐呭 -->
+ <u-form ref="formRef"
+ :model="form"
+ :rules="formRules"
+ label-width="110px">
+ <!-- 鍩烘湰淇℃伅 -->
+ <u-form-item label="璁惧鍚嶇О"
+ prop="deviceNameText"
+ required
+ border-bottom>
+ <u-input v-model="form.deviceNameText"
+ placeholder="璇烽�夋嫨璁惧鍚嶇О"
+ readonly
+ @click="showDevicePicker"
+ clearable />
+ <template #right>
+ <u-icon name="scan"
+ @click="startScan"
+ class="scan-icon" />
+ </template>
+ </u-form-item>
+ <u-form-item label="瑙勬牸鍨嬪彿"
+ prop="deviceModel"
+ border-bottom>
+ <u-input v-model="form.deviceModel"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ readonly
+ clearable />
+ </u-form-item>
+ <u-form-item label="璁″垝淇濆吇鏃ユ湡"
+ prop="maintenancePlanTime"
+ required
+ border-bottom>
+ <u-input v-model="form.maintenancePlanTime"
+ placeholder="璇烽�夋嫨璁″垝淇濆吇鏃ユ湡"
+ readonly
+ @click="showDatePicker"
+ clearable />
+ <template #right>
+ <u-icon name="arrow-right"
+ @click="showDatePicker" />
+ </template>
+ </u-form-item>
+ <u-form-item label="淇濆吇浜�"
+ prop="maintenancePerson"
+ border-bottom>
+ <u-input v-model="form.maintenancePerson"
+ placeholder="璇疯緭鍏ヤ繚鍏讳汉"
+ clearable />
+ </u-form-item>
+ <u-form-item label="淇濆吇椤圭洰"
+ prop="machineryCategory"
+ border-bottom>
+ <u-input v-model="form.machineryCategory"
+ placeholder="璇疯緭鍏ヤ繚鍏婚」鐩�"
+ clearable />
+ </u-form-item>
+ <u-form-item label="闄勪欢鍥剧墖"
+ prop="storageBlobDTOs"
+ border-bottom>
+ <CommonUpload v-model="form.storageBlobDTOs" />
+ </u-form-item>
+ <!-- 鎻愪氦鎸夐挳 -->
+ <view class="footer-btns">
+ <u-button class="cancel-btn"
+ @click="goBack">鍙栨秷</u-button>
+ <u-button class="save-btn"
+ @click="sendForm"
+ :loading="loading">淇濆瓨</u-button>
+ </view>
+ </u-form>
+ <!-- 璁惧閫夋嫨鍣� -->
+ <up-action-sheet :show="showDevice"
+ :actions="deviceActions"
+ title="閫夋嫨璁惧"
+ @select="onDeviceConfirm"
+ @close="showDevice = false" />
+ <up-datetime-picker :show="showDate"
+ v-model="pickerDateValue"
+ @confirm="onDateConfirm"
+ @cancel="showDate = false"
+ mode="date" />
+ </view>
</template>
<script setup>
-import { ref, computed, onMounted, onUnmounted } from 'vue';
-import { onShow } from '@dcloudio/uni-app';
-import PageHeader from '@/components/PageHeader.vue';
-import { getDeviceLedger } from '@/api/equipmentManagement/ledger';
-import { addUpkeep, editUpkeep, getUpkeepById } from '@/api/equipmentManagement/upkeep';
-import dayjs from "dayjs";
-import { formatDateToYMD } from '@/utils/ruoyi';
+ import { ref, computed, onMounted, onUnmounted } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+ import {
+ addUpkeep,
+ editUpkeep,
+ getUpkeepById,
+ } from "@/api/equipmentManagement/upkeep";
+ import dayjs from "dayjs";
+ import { formatDateToYMD } from "@/utils/ruoyi";
-defineOptions({
- name: "璁惧淇濆吇璁″垝琛ㄥ崟",
-});
-const showToast = (message) => {
- uni.showToast({
- title: message,
- icon: 'none'
- })
-}
+ defineOptions({
+ name: "璁惧淇濆吇璁″垝琛ㄥ崟",
+ });
+ const showToast = message => {
+ uni.showToast({
+ title: message,
+ icon: "none",
+ });
+ };
-// 琛ㄥ崟寮曠敤
-const formRef = ref(null);
-const operationType = ref('add');
-const loading = ref(false);
-const showDevice = ref(false);
-const showDate = ref(false);
-const pickerDateValue = ref(Date.now());
-const currentDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
+ // 琛ㄥ崟寮曠敤
+ const formRef = ref(null);
+ const operationType = ref("add");
+ const loading = ref(false);
+ const showDevice = ref(false);
+ const showDate = ref(false);
+ const pickerDateValue = ref(Date.now());
+ const currentDate = ref([
+ new Date().getFullYear(),
+ new Date().getMonth() + 1,
+ new Date().getDate(),
+ ]);
-// 璁惧閫夐」
-const deviceOptions = ref([]);
-const deviceNameText = ref('');
-// 杞崲涓� action-sheet 闇�瑕佺殑鏍煎紡
-const deviceActions = computed(() => {
- return deviceOptions.value.map(item => ({
- text: item.deviceName,
- value: item.id,
- data: item
- }));
-});
+ // 璁惧閫夐」
+ const deviceOptions = ref([]);
+ const deviceNameText = ref("");
+ // 杞崲涓� action-sheet 闇�瑕佺殑鏍煎紡
+ const deviceActions = computed(() => {
+ return deviceOptions.value.map(item => ({
+ text: item.deviceName,
+ value: item.id,
+ data: item,
+ }));
+ });
-// 鎵爜鐩稿叧鐘舵��
-const isScanning = ref(false);
-const scanTimer = ref(null);
+ // 鎵爜鐩稿叧鐘舵��
+ const isScanning = ref(false);
+ const scanTimer = ref(null);
-// 琛ㄥ崟楠岃瘉瑙勫垯
-const formRules = {
- deviceLedgerId: [{ required: true, trigger: "change", message: "璇烽�夋嫨璁惧鍚嶇О" }],
- maintenancePlanTime: [{ required: true, trigger: "change", message: "璇烽�夋嫨璁″垝淇濆吇鏃ユ湡" }],
-};
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const formRules = {
+ deviceLedgerId: [
+ { required: true, trigger: "change", message: "璇烽�夋嫨璁惧鍚嶇О" },
+ ],
+ maintenancePlanTime: [
+ { required: true, trigger: "change", message: "璇烽�夋嫨璁″垝淇濆吇鏃ユ湡" },
+ ],
+ };
-// 浣跨敤 ref 澹版槑琛ㄥ崟鏁版嵁
-const form = ref({
- deviceLedgerId: undefined, // 璁惧ID
- deviceModel: undefined, // 瑙勬牸鍨嬪彿
- maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // 璁″垝淇濆吇鏃ユ湡
-});
+ // 浣跨敤 ref 澹版槑琛ㄥ崟鏁版嵁
+ const form = ref({
+ deviceLedgerId: undefined, // 璁惧ID
+ deviceModel: undefined, // 瑙勬牸鍨嬪彿
+ maintenancePlanTime: dayjs().format("YYYY-MM-DD"), // 璁″垝淇濆吇鏃ユ湡
+ maintenancePerson: undefined, // 淇濆吇浜�
+ machineryCategory: undefined, // 淇濆吇椤圭洰
+ storageBlobDTOs: [], // 闄勪欢鍥剧墖
+ });
-// 鍔犺浇璁惧鍒楄〃
-const loadDeviceName = async () => {
- try {
- const { data } = await getDeviceLedger();
- deviceOptions.value = data || [];
- } catch (e) {
- showToast('鑾峰彇璁惧鍒楄〃澶辫触');
- }
-};
+ // 鍔犺浇璁惧鍒楄〃
+ const loadDeviceName = async () => {
+ try {
+ const { data } = await getDeviceLedger();
+ deviceOptions.value = data || [];
+ } catch (e) {
+ showToast("鑾峰彇璁惧鍒楄〃澶辫触");
+ }
+ };
-// 鍔犺浇琛ㄥ崟鏁版嵁锛堢紪杈戞ā寮忥級
-const loadForm = async (id) => {
- if (id) {
- operationType.value = 'edit';
- try {
- const { code, data } = await getUpkeepById(id);
- if (code == 200) {
- form.value.deviceLedgerId = data.deviceLedgerId;
- form.value.deviceModel = data.deviceModel;
- form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format("YYYY-MM-DD");
- // 璁剧疆璁惧鍚嶇О鏄剧ず
- const device = deviceOptions.value.find(item => item.id === data.deviceLedgerId);
- if (device) {
- form.value.deviceNameText = device.deviceName;
- }
- }
- } catch (e) {
- showToast('鑾峰彇璇︽儏澶辫触');
- }
- } else {
- // 鏂板妯″紡
- operationType.value = 'add';
- }
-};
+ // 鍔犺浇琛ㄥ崟鏁版嵁锛堢紪杈戞ā寮忥級
+ const loadForm = async id => {
+ if (id) {
+ operationType.value = "edit";
+ try {
+ const { code, data } = await getUpkeepById(id);
+ if (code == 200) {
+ form.value.deviceLedgerId = data.deviceLedgerId;
+ form.value.deviceModel = data.deviceModel;
+ form.value.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
+ "YYYY-MM-DD"
+ );
+ form.value.maintenancePerson = data.maintenancePerson;
+ form.value.machineryCategory = data.machineryCategory;
+ form.value.storageBlobDTOs = data.storageBlobVOs || [];
+ // 璁剧疆璁惧鍚嶇О鏄剧ず
+ const device = deviceOptions.value.find(
+ item => item.id === data.deviceLedgerId
+ );
+ if (device) {
+ form.value.deviceNameText = device.deviceName;
+ }
+ }
+ } catch (e) {
+ showToast("鑾峰彇璇︽儏澶辫触");
+ }
+ } else {
+ // 鏂板妯″紡
+ operationType.value = "add";
+ }
+ };
-// 鎵弿浜岀淮鐮佸姛鑳�
-const startScan = () => {
- if (isScanning.value) {
- showToast('姝e湪鎵弿涓紝璇风◢鍊�...');
- return;
- }
-
- // 璋冪敤uni-app鐨勬壂鐮丄PI
- uni.scanCode({
- scanType: ['qrCode', 'barCode'],
- success: (res) => {
- handleScanResult(res.result);
- },
- fail: (err) => {
- console.error('鎵爜澶辫触:', err);
- showToast('鎵爜澶辫触锛岃閲嶈瘯');
- }
- });
-};
+ // 鎵弿浜岀淮鐮佸姛鑳�
+ const startScan = () => {
+ if (isScanning.value) {
+ showToast("姝e湪鎵弿涓紝璇风◢鍊�...");
+ return;
+ }
-// 澶勭悊鎵爜缁撴灉
-const handleScanResult = (scanResult) => {
- if (!scanResult) {
- showToast('鎵爜缁撴灉涓虹┖');
- return;
- }
-
- isScanning.value = true;
- showToast('鎵爜鎴愬姛');
-
- // 3绉掑悗澶勭悊鎵爜缁撴灉
- scanTimer.value = setTimeout(() => {
- processScanResult(scanResult);
- isScanning.value = false;
- }, 1000);
-};
-function getDeviceIdByRegExp(url) {
- // 鍖归厤deviceId=鍚庨潰鐨勬暟瀛�
- const reg = /deviceId=(\d+)/;
- const match = url.match(reg);
- // 濡傛灉鍖归厤鍒扮粨鏋滐紝杩斿洖鏁板瓧绫诲瀷锛屽惁鍒欒繑鍥瀗ull
- return match ? Number(match[1]) : null;
-}
-// 澶勭悊鎵爜缁撴灉骞跺尮閰嶈澶�
-const processScanResult = (scanResult) => {
- const deviceId = getDeviceIdByRegExp(scanResult);
- const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
-
- if (matchedDevice) {
- // 鎵惧埌鍖归厤鐨勮澶囷紝鑷姩濉厖
- form.value.deviceLedgerId = matchedDevice.id;
- form.value.deviceNameText = matchedDevice.deviceName;
- form.value.deviceModel = matchedDevice.deviceModel;
- showToast('璁惧淇℃伅宸茶嚜鍔ㄥ~鍏�');
- } else {
- // 鏈壘鍒板尮閰嶇殑璁惧
- showToast('鏈壘鍒板尮閰嶇殑璁惧锛岃鎵嬪姩閫夋嫨');
- }
-};
+ // 璋冪敤uni-app鐨勬壂鐮丄PI
+ uni.scanCode({
+ scanType: ["qrCode", "barCode"],
+ success: res => {
+ handleScanResult(res.result);
+ },
+ fail: err => {
+ console.error("鎵爜澶辫触:", err);
+ showToast("鎵爜澶辫触锛岃閲嶈瘯");
+ },
+ });
+ };
-// 鏄剧ず璁惧閫夋嫨鍣�
-const showDevicePicker = () => {
- showDevice.value = true;
-};
+ // 澶勭悊鎵爜缁撴灉
+ const handleScanResult = scanResult => {
+ if (!scanResult) {
+ showToast("鎵爜缁撴灉涓虹┖");
+ return;
+ }
-// 纭璁惧閫夋嫨
-const onDeviceConfirm = (selected) => {
- // selected 杩斿洖鐨勬槸閫変腑椤�
- form.value.deviceLedgerId = selected.value;
- form.value.deviceNameText = selected.name;
- const selectedDevice = deviceOptions.value.find(item => item.id === selected.value);
- if (selectedDevice) {
- form.value.deviceModel = selectedDevice.deviceModel;
- }
- showDevice.value = false;
-};
+ isScanning.value = true;
+ showToast("鎵爜鎴愬姛");
-// 鏄剧ず鏃ユ湡閫夋嫨鍣�
-const showDatePicker = () => {
- showDate.value = true;
-};
+ // 3绉掑悗澶勭悊鎵爜缁撴灉
+ scanTimer.value = setTimeout(() => {
+ processScanResult(scanResult);
+ isScanning.value = false;
+ }, 1000);
+ };
+ function getDeviceIdByRegExp(url) {
+ // 鍖归厤deviceId=鍚庨潰鐨勬暟瀛�
+ const reg = /deviceId=(\d+)/;
+ const match = url.match(reg);
+ // 濡傛灉鍖归厤鍒扮粨鏋滐紝杩斿洖鏁板瓧绫诲瀷锛屽惁鍒欒繑鍥瀗ull
+ return match ? Number(match[1]) : null;
+ }
+ // 澶勭悊鎵爜缁撴灉骞跺尮閰嶈澶�
+ const processScanResult = scanResult => {
+ const deviceId = getDeviceIdByRegExp(scanResult);
+ const matchedDevice = deviceOptions.value.find(item => item.id == deviceId);
-// 纭鏃ユ湡閫夋嫨
-const onDateConfirm = (e) => {
- form.value.maintenancePlanTime = formatDateToYMD(e.value);
- showDate.value = false;
-};
+ if (matchedDevice) {
+ // 鎵惧埌鍖归厤鐨勮澶囷紝鑷姩濉厖
+ form.value.deviceLedgerId = matchedDevice.id;
+ form.value.deviceNameText = matchedDevice.deviceName;
+ form.value.deviceModel = matchedDevice.deviceModel;
+ showToast("璁惧淇℃伅宸茶嚜鍔ㄥ~鍏�");
+ } else {
+ // 鏈壘鍒板尮閰嶇殑璁惧
+ showToast("鏈壘鍒板尮閰嶇殑璁惧锛岃鎵嬪姩閫夋嫨");
+ }
+ };
-onShow(() => {
- // 椤甸潰鏄剧ず鏃惰幏鍙栧弬鏁�
- getPageParams();
-});
+ // 鏄剧ず璁惧閫夋嫨鍣�
+ const showDevicePicker = () => {
+ showDevice.value = true;
+ };
-onMounted(() => {
- // 椤甸潰鍔犺浇鏃惰幏鍙栬澶囧垪琛ㄥ拰鍙傛暟
- loadDeviceName();
- getPageParams();
-});
+ // 纭璁惧閫夋嫨
+ const onDeviceConfirm = selected => {
+ // selected 杩斿洖鐨勬槸閫変腑椤�
+ form.value.deviceLedgerId = selected.value;
+ form.value.deviceNameText = selected.name;
+ const selectedDevice = deviceOptions.value.find(
+ item => item.id === selected.value
+ );
+ if (selectedDevice) {
+ form.value.deviceModel = selectedDevice.deviceModel;
+ }
+ showDevice.value = false;
+ };
-// 缁勪欢鍗歌浇鏃舵竻鐞嗗畾鏃跺櫒
-onUnmounted(() => {
- if (scanTimer.value) {
- clearTimeout(scanTimer.value);
- }
-});
+ // 鏄剧ず鏃ユ湡閫夋嫨鍣�
+ const showDatePicker = () => {
+ showDate.value = true;
+ };
-// 鎻愪氦琛ㄥ崟
-const sendForm = async () => {
- try {
- // 鎵嬪姩楠岃瘉琛ㄥ崟
- const valid = await formRef.value.validate();
- if (!valid) return;
-
- loading.value = true;
- const id = getPageId();
-
- // 鍑嗗鎻愪氦鏁版嵁
- const submitData = { ...form.value };
- // 纭繚鏃ユ湡鏍煎紡姝g‘
- if (submitData.maintenancePlanTime && !submitData.maintenancePlanTime.includes(':')) {
- submitData.maintenancePlanTime = submitData.maintenancePlanTime + ' 00:00:00';
- }
-
- const { code } = id
- ? await editUpkeep({ id: id, ...submitData })
- : await addUpkeep(submitData);
-
- if (code == 200) {
- showToast(`${id ? "缂栬緫" : "鏂板"}璁″垝鎴愬姛`);
- setTimeout(() => {
- uni.navigateBack();
- }, 1500);
- } else {
- loading.value = false;
- }
- } catch (e) {
- loading.value = false;
- showToast('琛ㄥ崟楠岃瘉澶辫触');
- }
-};
+ // 纭鏃ユ湡閫夋嫨
+ const onDateConfirm = e => {
+ form.value.maintenancePlanTime = formatDateToYMD(e.value);
+ showDate.value = false;
+ };
-// 杩斿洖涓婁竴椤�
-const goBack = () => {
- // 娓呴櫎瀛樺偍鐨刬d
- uni.removeStorageSync('repairId');
- uni.navigateBack();
-};
+ onShow(() => {
+ // 椤甸潰鏄剧ず鏃惰幏鍙栧弬鏁�
+ getPageParams();
+ });
-// 鑾峰彇椤甸潰鍙傛暟
-const getPageParams = () => {
- // 浠庢湰鍦板瓨鍌ㄨ幏鍙杋d
- const id = uni.getStorageSync('repairId');
-
- // 鏍规嵁鏄惁鏈塱d鍙傛暟鏉ュ垽鏂槸鏂板杩樻槸缂栬緫
- if (id) {
- // 缂栬緫妯″紡锛岃幏鍙栬鎯�
- loadForm(id);
- } else {
- // 鏂板妯″紡
- loadForm();
- }
-};
+ onMounted(() => {
+ // 椤甸潰鍔犺浇鏃惰幏鍙栬澶囧垪琛ㄥ拰鍙傛暟
+ loadDeviceName();
+ getPageParams();
+ });
-// 鑾峰彇椤甸潰ID
-const getPageId = () => {
- // 浠庢湰鍦板瓨鍌ㄨ幏鍙杋d
- return uni.getStorageSync('repairId');
-};
+ // 缁勪欢鍗歌浇鏃舵竻鐞嗗畾鏃跺櫒
+ onUnmounted(() => {
+ if (scanTimer.value) {
+ clearTimeout(scanTimer.value);
+ }
+ });
+
+ // 鎻愪氦琛ㄥ崟
+ const sendForm = async () => {
+ try {
+ // 鎵嬪姩楠岃瘉琛ㄥ崟
+ const valid = await formRef.value.validate();
+ if (!valid) return;
+
+ loading.value = true;
+ const id = getPageId();
+
+ // 鍑嗗鎻愪氦鏁版嵁
+ const submitData = { ...form.value, status: 0 };
+
+ // 纭繚鏃ユ湡鏍煎紡姝g‘
+ if (
+ submitData.maintenancePlanTime &&
+ !submitData.maintenancePlanTime.includes(":")
+ ) {
+ submitData.maintenancePlanTime =
+ submitData.maintenancePlanTime + " 00:00:00";
+ }
+
+ const { code } = id
+ ? await editUpkeep({ id: id, ...submitData })
+ : await addUpkeep(submitData);
+
+ if (code == 200) {
+ showToast(`${id ? "缂栬緫" : "鏂板"}璁″垝鎴愬姛`);
+ setTimeout(() => {
+ uni.navigateBack();
+ }, 1500);
+ } else {
+ loading.value = false;
+ }
+ } catch (e) {
+ loading.value = false;
+ showToast("琛ㄥ崟楠岃瘉澶辫触");
+ }
+ };
+
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ // 娓呴櫎瀛樺偍鐨刬d
+ uni.removeStorageSync("repairId");
+ uni.navigateBack();
+ };
+
+ // 鑾峰彇椤甸潰鍙傛暟
+ const getPageParams = () => {
+ // 浠庢湰鍦板瓨鍌ㄨ幏鍙杋d
+ const id = uni.getStorageSync("repairId");
+
+ // 鏍规嵁鏄惁鏈塱d鍙傛暟鏉ュ垽鏂槸鏂板杩樻槸缂栬緫
+ if (id) {
+ // 缂栬緫妯″紡锛岃幏鍙栬鎯�
+ loadForm(id);
+ } else {
+ // 鏂板妯″紡
+ loadForm();
+ }
+ };
+
+ // 鑾峰彇椤甸潰ID
+ const getPageId = () => {
+ // 浠庢湰鍦板瓨鍌ㄨ幏鍙杋d
+ return uni.getStorageSync("repairId");
+ };
</script>
<style scoped lang="scss">
-@import '@/static/scss/form-common.scss';
-.upkeep-add {
- min-height: 100vh;
- background: #f8f9fa;
- padding-bottom: 5rem;
-}
+ @import "@/static/scss/form-common.scss";
+ .upkeep-add {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 5rem;
+ }
-.footer-btns {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
- background: #fff;
- display: flex;
- justify-content: space-around;
- align-items: center;
- padding: 0.75rem 0;
- box-shadow: 0 -0.125rem 0.5rem rgba(0,0,0,0.05);
- z-index: 1000;
-}
+ .footer-btns {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ padding: 0.75rem 0;
+ box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
+ z-index: 1000;
+ }
-.cancel-btn {
- font-weight: 400;
- font-size: 1rem;
- color: #FFFFFF;
- width: 6.375rem;
- background: #C7C9CC;
- box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
- border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
-}
+ .cancel-btn {
+ font-weight: 400;
+ font-size: 1rem;
+ color: #ffffff;
+ width: 6.375rem;
+ background: #c7c9cc;
+ box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
+ border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
+ }
-.save-btn {
- font-weight: 400;
- font-size: 1rem;
- color: #FFFFFF;
- width: 14rem;
- background: linear-gradient( 140deg, #00BAFF 0%, #006CFB 100%);
- box-shadow: 0 0.25rem 0.625rem 0 rgba(3,88,185,0.2);
- border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
-}
+ .save-btn {
+ font-weight: 400;
+ font-size: 1rem;
+ color: #ffffff;
+ width: 14rem;
+ background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
+ box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
+ border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
+ }
-// 鍝嶅簲寮忚皟鏁�
-@media (max-width: 768px) {
- .submit-section {
- padding: 12px;
- }
-}
+ // 鍝嶅簲寮忚皟鏁�
+ @media (max-width: 768px) {
+ .submit-section {
+ padding: 12px;
+ }
+ }
-.tip-text {
- padding: 4px 16px 0 16px;
- font-size: 12px;
- color: #888;
-}
+ .tip-text {
+ padding: 4px 16px 0 16px;
+ font-size: 12px;
+ color: #888;
+ }
-.scan-icon {
- color: #1989fa;
- font-size: 18px;
- margin-left: 8px;
- cursor: pointer;
-}
+ .scan-icon {
+ color: #1989fa;
+ font-size: 18px;
+ margin-left: 8px;
+ cursor: pointer;
+ }
</style>
\ No newline at end of file
diff --git a/src/pages/equipmentManagement/upkeep/fileList.vue b/src/pages/equipmentManagement/upkeep/fileList.vue
index b4d4b7f..1680fcb 100644
--- a/src/pages/equipmentManagement/upkeep/fileList.vue
+++ b/src/pages/equipmentManagement/upkeep/fileList.vue
@@ -8,7 +8,7 @@
<view v-if="fileList.length > 0"
class="file-list">
<view v-for="(file, index) in fileList"
- :key="file.id || index"
+ :key="file.storageAttachmentId || file.id || index"
class="file-item">
<!-- 鏂囦欢鍥炬爣 -->
<!-- <view class="file-icon"
@@ -19,7 +19,7 @@
</view> -->
<!-- 鏂囦欢淇℃伅 -->
<view class="file-info">
- <text class="file-name">{{ file.name }}</text>
+ <text class="file-name">{{ file.originalFilename || file.name }}</text>
<!-- <text class="file-meta">{{ formatFileSize(file.fileSize) }} 路 {{ file.uploadTime || file.createTime }}</text> -->
</view>
<!-- 鎿嶄綔鎸夐挳 -->
@@ -65,15 +65,16 @@
<script setup>
import { ref, onMounted } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import config from "@/config";
import { getToken } from "@/utils/auth";
// import { saveAs } from "file-saver";
import {
- listMaintenanceTaskFiles,
- addMaintenanceTaskFile,
- delMaintenanceTaskFile,
- } from "@/api/equipmentManagement/upkeep";
+ attachmentList,
+ createAttachment,
+ deleteAttachment,
+ } from "@/api/basicData/storageAttachment";
import { blobValidate } from "@/utils/ruoyi";
// 闄勪欢鍒楄〃
@@ -214,21 +215,27 @@
// const fileType = fileName.split(".").pop();
// 3. 鏋勯�犱繚瀛樻枃浠朵俊鎭殑鍙傛暟
const saveData = {
- name: fileName,
- deviceMaintenanceId: upkeepId.value,
- url: res.data.tempPath || "",
+ application: "file",
+ recordType: recordType.value,
+ recordId: upkeepId.value,
+ storageBlobDTOs: [
+ {
+ name: fileName,
+ url:
+ res.data.url ||
+ res.data.previewURL ||
+ res.data.tempPath ||
+ "",
+ ...res.data,
+ },
+ ],
};
console.log(saveData, "淇濆瓨鏂囦欢淇℃伅鍙傛暟");
- // 4. 璋冪敤 addRuleFile 鎺ュ彛淇濆瓨鏂囦欢淇℃伅
- addMaintenanceTaskFile(saveData)
+ // 4. 璋冪敤 createAttachment 鎺ュ彛淇濆瓨鏂囦欢淇℃伅
+ createAttachment(saveData)
.then(addRes => {
if (addRes.code === 200) {
- // 5. 娣诲姞鍒版枃浠跺垪琛�
- const newFile = {
- ...addRes.data,
- uploadTime: new Date().toLocaleString(),
- };
- // fileList.value.push(newFile);
+ // 5. 鍒锋柊鍒楄〃
getFileList();
showToast("涓婁紶鎴愬姛");
} else {
@@ -257,20 +264,32 @@
};
// 涓嬭浇鏂囦欢
const downloadFile = file => {
- var url =
- config.baseUrl +
- "/common/download?fileName=" +
- encodeURIComponent(file.url) +
- "&delete=true";
- console.log(url, "url");
+ let url = file.downloadURL || file.previewURL || file.url;
+ if (!url) {
+ showToast("鏂囦欢鍦板潃鏃犳晥");
+ return;
+ }
+
+ // 濡傛灉涓嶆槸瀹屾暣鐨刄RL锛屽垯鎷兼帴
+ if (!url.startsWith("http")) {
+ url =
+ config.baseUrl +
+ "/common/download?fileName=" +
+ encodeURIComponent(url) +
+ "&delete=true";
+ }
+
+ console.log(url, "涓嬭浇鍦板潃");
+
+ uni.showLoading({ title: "姝e湪涓嬭浇...", mask: true });
uni
.downloadFile({
url: url,
- responseType: "blob",
header: { Authorization: "Bearer " + getToken() },
})
.then(res => {
+ uni.hideLoading();
let osType = uni.getStorageSync("deviceInfo").osName;
let filePath = res.tempFilePath;
if (osType === "ios") {
@@ -280,7 +299,6 @@
success: res => {},
fail: err => {
console.log("uni.openDocument--fail");
- reject(err);
},
});
} else {
@@ -290,10 +308,8 @@
uni.showToast({
icon: "none",
mask: true,
- title:
- "鏂囦欢宸蹭繚瀛橈細Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
- fileRes.savedFilePath, //淇濆瓨璺緞
- duration: 3000,
+ title: "鏂囦欢宸蹭笅杞藉苟灏濊瘯鎵撳紑",
+ duration: 2000,
});
setTimeout(() => {
//鎵撳紑鏂囨。鏌ョ湅
@@ -305,24 +321,12 @@
},
fail: err => {
console.log("uni.save--fail");
- reject(err);
},
});
}
- // const isBlob = blobValidate(res.data);
- // if (isBlob) {
- // const blob = new Blob([res.data], { type: "text/plain" });
- // const url = URL.createObjectURL(blob);
- // const downloadLink = document.getElementById("downloadLink");
- // downloadLink.href = url;
- // downloadLink.download = file.name;
- // downloadLink.click();
- // showToast("涓嬭浇鎴愬姛");
- // } else {
- // showToast("涓嬭浇澶辫触");
- // }
})
.catch(err => {
+ uni.hideLoading();
console.error("涓嬭浇澶辫触:", err);
showToast("涓嬭浇澶辫触");
});
@@ -335,7 +339,7 @@
content: `纭畾瑕佸垹闄ら檮浠� "${file.name}" 鍚楋紵`,
success: res => {
if (res.confirm) {
- deleteFile(file.id, index);
+ deleteFile(file.storageAttachmentId || file.id, index);
}
},
});
@@ -348,7 +352,7 @@
mask: true,
});
- delMaintenanceTaskFile([fileId])
+ deleteAttachment([fileId])
.then(res => {
uni.hideLoading();
if (res.code === 200) {
@@ -372,37 +376,48 @@
icon: "none",
});
};
- const rulesRegulationsManagementId = ref("");
const upkeepId = ref("");
+ const recordType = ref("");
+
+ // 椤甸潰鍔犺浇鏃惰幏鍙栧弬鏁�
+ onLoad(options => {
+ if (options.recordId) {
+ upkeepId.value = options.recordId;
+ } else {
+ upkeepId.value = uni.getStorageSync("upkeepId");
+ }
+
+ if (options.recordType) {
+ recordType.value = options.recordType;
+ } else {
+ recordType.value = "device_maintenance"; // 榛樿鍏煎
+ }
+
+ getFileList();
+ });
+
// 椤甸潰鍔犺浇鏃�
onMounted(() => {
- // 浠� API 鑾峰彇闄勪欢鍒楄〃
-
- // 浠庢湰鍦板瓨鍌ㄨ幏鍙� rulesRegulationsManagementId
- rulesRegulationsManagementId.value = uni.getStorageSync(
- "rulesRegulationsManagement"
- );
- upkeepId.value = uni.getStorageSync("upkeepId");
- getFileList();
+ // getFileList(); // onLoad 涓凡缁忚皟鐢ㄤ簡
});
// 鑾峰彇闄勪欢鍒楄〃
const getFileList = () => {
+ if (!upkeepId.value) return;
+
uni.showLoading({
title: "鍔犺浇涓�...",
mask: true,
});
- listMaintenanceTaskFiles({
- current: 1,
- size: 100,
- deviceMaintenanceId: upkeepId.value,
- rulesRegulationsManagementId: upkeepId.value,
+ attachmentList({
+ recordType: recordType.value,
+ recordId: upkeepId.value,
})
.then(res => {
uni.hideLoading();
if (res.code === 200) {
- fileList.value = res.data.records || [];
+ fileList.value = res.data || [];
} else {
showToast("鑾峰彇闄勪欢鍒楄〃澶辫触");
}
diff --git a/src/pages/equipmentManagement/upkeep/index.vue b/src/pages/equipmentManagement/upkeep/index.vue
index cca1b04..671bc65 100644
--- a/src/pages/equipmentManagement/upkeep/index.vue
+++ b/src/pages/equipmentManagement/upkeep/index.vue
@@ -63,6 +63,14 @@
<text class="detail-value">{{ formatDateTime(item.createTime) || '-' }}</text>
</view>
<view class="detail-row">
+ <text class="detail-label">淇濆吇浜�</text>
+ <text class="detail-value">{{ item.maintenancePerson || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">淇濆吇椤圭洰</text>
+ <text class="detail-value">{{ item.machineryCategory || '-' }}</text>
+ </view>
+ <view class="detail-row">
<text class="detail-label">瀹為檯淇濆吇浜�</text>
<text class="detail-value">{{ item.maintenanceActuallyName || '-' }}</text>
</view>
@@ -72,7 +80,8 @@
</view>
<view class="detail-row">
<text class="detail-label">淇濆吇缁撴灉</text>
- <view class="detail-value">
+ <text class="detail-value">{{ item.maintenanceResult || '-' }}</text>
+ <!-- <view class="detail-value">
<u-tag v-if="item.maintenanceResult === 1"
type="success"
size="mini">
@@ -84,7 +93,7 @@
缁翠慨
</u-tag>
<text v-if="item.maintenanceResult === undefined || item.maintenanceResult === null">-</text>
- </view>
+ </view> -->
</view>
</view>
<!-- 鎸夐挳鍖哄煙 -->
@@ -198,10 +207,8 @@
};
// 鏂板闄勪欢 - 璺宠浆鍒伴檮浠堕〉闈�
const addFile = id => {
- // 浣跨敤鏈湴瀛樺偍浼犻�抜d
- uni.setStorageSync("upkeepId", id);
uni.navigateTo({
- url: "/pages/equipmentManagement/upkeep/fileList",
+ url: `/pages/equipmentManagement/upkeep/fileList?recordId=${id}&recordType=device_maintenance`,
});
};
diff --git a/src/pages/equipmentManagement/upkeep/maintain.vue b/src/pages/equipmentManagement/upkeep/maintain.vue
index f86006c..3abfc58 100644
--- a/src/pages/equipmentManagement/upkeep/maintain.vue
+++ b/src/pages/equipmentManagement/upkeep/maintain.vue
@@ -100,81 +100,9 @@
<!-- 涓婁紶闄勪欢 -->
<u-form-item v-if="form.status == '1'"
label="淇濆吇闄勪欢"
+ prop="storageBlobDTOs"
border-bottom>
- <view class="simple-upload-area">
- <view class="upload-buttons">
- <u-button type="primary"
- @click="chooseMedia('image')"
- :loading="uploading"
- :disabled="uploadFiles.length >= uploadConfig.limit"
- :customStyle="{ marginRight: '10px', flex: 1 }">
- <u-icon name="camera"
- size="18"
- color="#fff"
- style="margin-right: 5px;"></u-icon>
- {{ uploading ? '涓婁紶涓�...' : '鎷嶇収' }}
- </u-button>
- <!-- <u-button type="success"
- @click="chooseMedia('video')"
- :loading="uploading"
- :disabled="uploadFiles.length >= uploadConfig.limit"
- :customStyle="{ flex: 1 }">
- <uni-icons type="videocam"
- name="videocam"
- size="18"
- color="#fff"
- style="margin-right: 5px;"></uni-icons>
- {{ uploading ? '涓婁紶涓�...' : '鎷嶈棰�' }}
- </u-button> -->
- </view>
- <!-- 涓婁紶杩涘害 -->
- <view v-if="uploading"
- class="upload-progress">
- <u-line-progress :percentage="uploadProgress"
- :showText="true"
- activeColor="#409eff"></u-line-progress>
- </view>
- <!-- 涓婁紶鐨勬枃浠跺垪琛� -->
- <view v-if="uploadFiles.length > 0"
- class="file-list">
- <view v-for="(file, index) in uploadFiles"
- :key="index"
- class="file-item">
- <view class="file-preview-container">
- <!-- {{formatFileUrl(file.url)}} -->
- <image v-if="file.type === 'image' || isImageFile(file)"
- :src="formatFileUrl(file.url || file.tempFilePath || file.path || file.downloadUrl)"
- class="file-preview"
- mode="aspectFill" />
- <view v-else-if="file.type === 'video'"
- class="video-preview">
- <uni-icons type="videocam"
- name="videocam"
- size="18"
- color="#fff"
- style="margin-right: 5px;"></uni-icons>
- <text class="video-text">瑙嗛</text>
- </view>
- <!-- 鍒犻櫎鎸夐挳 -->
- <view class="delete-btn"
- @click="removeFile(index)">
- <u-icon name="close"
- size="12"
- color="#fff"></u-icon>
- </view>
- </view>
- <view class="file-info">
- <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '鍥剧墖' : '瑙嗛')
- }}</text>
- <text class="file-size">{{ formatFileSize(file.size) }}</text>
- </view>
- </view>
- </view>
- <view v-if="uploadFiles.length === 0"
- class="empty-state">
- <text>璇烽�夋嫨瑕佷笂浼犵殑淇濆吇鍥剧墖</text>
- </view>
- </view>
+ <CommonUpload v-model="form.storageBlobDTOs" />
</u-form-item>
<!-- 鎻愪氦鎸夐挳 -->
<view class="footer-btns">
@@ -235,6 +163,7 @@
import { ref, onMounted, reactive } from "vue";
import { onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
import { addMaintenance } from "@/api/equipmentManagement/upkeep";
import { getSparePartsList } from "@/api/equipmentManagement/repair";
import useUserStore from "@/store/modules/user";
@@ -275,7 +204,6 @@
const sparePartsQtyRaw = ref("");
// 鏂囦欢涓婁紶鐩稿叧
- const uploadFiles = ref([]);
const uploading = ref(false);
const uploadProgress = ref(0);
const number = ref(0);
@@ -316,6 +244,7 @@
maintenanceResult: undefined, // 淇濆吇缁撴灉
maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 瀹為檯淇濆吇鏃ユ湡锛堝彧鏄剧ず鏃ユ湡锛�
sparePartsIds: undefined, // 璁惧澶囦欢ID
+ storageBlobDTOs: [], // 淇濆吇闄勪欢
});
// 娓呴櫎琛ㄥ崟鏍¢獙鐘舵��
@@ -330,6 +259,7 @@
maintenanceResult: undefined,
maintenanceActuallyTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
sparePartsIds: [],
+ storageBlobDTOs: [],
};
maintenancestatusText.value = "";
selectedSpareParts.value = [];
@@ -374,7 +304,11 @@
} else if (form.value.maintenanceResult === undefined) {
isValid = false;
errorMessage = "璇烽�夋嫨淇濆吇缁撴灉";
- } else if (uploadFiles.value.length === 0 && form.value.status == "1") {
+ } else if (
+ (!form.value.storageBlobDTOs ||
+ form.value.storageBlobDTOs.length === 0) &&
+ form.value.status == "1"
+ ) {
isValid = false;
errorMessage = "璇蜂笂浼犱繚鍏荤収鐗�";
}
@@ -436,7 +370,6 @@
const submitData = {
...form.value,
- imagesFile: form.value.status == "1" ? uploadFiles.value : [],
sparePartsIds: spareIds.length ? spareIds.join(",") : "",
sparePartsQty: spareIds.length
? spareIds.map(pid => sparePartQtyMap?.[pid] ?? 1).join(",")
@@ -605,7 +538,7 @@
// 閲嶇疆閫夋嫨鐨勫浠�
selectedSpareParts.value = [];
// 閲嶇疆涓婁紶鐨勬枃浠�
- uploadFiles.value = [];
+ form.value.storageBlobDTOs = [];
uploading.value = false;
uploadProgress.value = 0;
maintenancestatusText.value = "";
@@ -655,8 +588,10 @@
sparePartsIds.value = itemData.sparePartsIds;
// 濉厖闄勪欢鏁版嵁
- if (itemData.files && itemData.files.length > 0) {
- uploadFiles.value = itemData.files.map(file => ({
+ if (itemData.storageBlobVOs && itemData.storageBlobVOs.length > 0) {
+ form.value.storageBlobDTOs = itemData.storageBlobVOs;
+ } else if (itemData.files && itemData.files.length > 0) {
+ form.value.storageBlobDTOs = itemData.files.map(file => ({
id: file.id,
name: file.name || file.bucketFilename || file.originalFilename,
url: file.url || file.downloadUrl,
@@ -668,7 +603,7 @@
size: file.size || file.byteSize,
}));
} else if (itemData.uploadFiles && itemData.uploadFiles.length > 0) {
- uploadFiles.value = itemData.uploadFiles.map(file => ({
+ form.value.storageBlobDTOs = itemData.uploadFiles.map(file => ({
id: file.id,
name: file.name || file.bucketFilename || file.originalFilename,
url: file.url || file.downloadUrl || file.tempFilePath || file.path,
diff --git a/src/pages/fileManagement/borrow/edit.vue b/src/pages/fileManagement/borrow/edit.vue
new file mode 100644
index 0000000..1338a6f
--- /dev/null
+++ b/src/pages/fileManagement/borrow/edit.vue
@@ -0,0 +1,333 @@
+<template>
+ <view class="borrow-edit">
+ <PageHeader :title="pageTitle" @back="goBack" />
+
+ <up-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="110"
+ >
+ <up-form-item label="鍊熼槄浜�" prop="borrower" required>
+ <up-input
+ v-model="form.borrower"
+ placeholder="璇疯緭鍏ュ�熼槄浜�"
+ clearable
+ :disabled="isReturned"
+ />
+ </up-form-item>
+
+ <up-form-item label="鍊熼槄涔︾睄" prop="documentationId" required>
+ <up-input
+ v-model="displayDocName"
+ placeholder="璇烽�夋嫨鍊熼槄涔︾睄"
+ readonly
+ :disabled="isEdit"
+ @click="!isEdit && (showDocPicker = true)"
+ />
+ <template #right>
+ <up-icon v-if="!isEdit" name="arrow-right" @click="showDocPicker = true"></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="鍊熼槄鏃ユ湡" prop="borrowDate" required>
+ <up-input
+ v-model="form.borrowDate"
+ placeholder="璇烽�夋嫨鍊熼槄鏃ユ湡"
+ readonly
+ @click="!isReturned && (showBorrowDatePicker = true)"
+ :disabled="isReturned"
+ />
+ <template #right>
+ <up-icon name="arrow-right" @click="!isReturned && (showBorrowDatePicker = true)"></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="搴斿綊杩樻棩鏈�" prop="dueReturnDate" required>
+ <up-input
+ v-model="form.dueReturnDate"
+ placeholder="璇烽�夋嫨搴斿綊杩樻棩鏈�"
+ readonly
+ @click="!isReturned && (showDueDatePicker = true)"
+ :disabled="isReturned"
+ />
+ <template #right>
+ <up-icon name="arrow-right" @click="!isReturned && (showDueDatePicker = true)"></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="鍊熼槄鐩殑" prop="borrowPurpose" required>
+ <up-input
+ v-model="form.borrowPurpose"
+ placeholder="璇疯緭鍏ュ�熼槄鐩殑"
+ clearable
+ :disabled="isReturned"
+ />
+ </up-form-item>
+
+ <up-form-item label="澶囨敞">
+ <up-textarea
+ v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�"
+ height="80"
+ border="none"
+ :disabled="isReturned"
+ />
+ </up-form-item>
+ </up-form>
+
+ <FooterButtons
+ v-if="!isReturned"
+ :loading="loading"
+ :confirmText="isEdit ? '淇濆瓨' : '鏂板'"
+ @cancel="goBack"
+ @confirm="handleSubmit"
+ />
+
+ <!-- 鍊熼槄鏃ユ湡閫夋嫨鍣� -->
+ <up-popup :show="showBorrowDatePicker" mode="bottom" @close="showBorrowDatePicker = false">
+ <up-datetime-picker
+ :show="true"
+ v-model="borrowDateValue"
+ @confirm="onBorrowDateConfirm"
+ @cancel="showBorrowDatePicker = false"
+ mode="date"
+ />
+ </up-popup>
+
+ <!-- 搴斿綊杩樻棩鏈熼�夋嫨鍣� -->
+ <up-popup :show="showDueDatePicker" mode="bottom" @close="showDueDatePicker = false">
+ <up-datetime-picker
+ :show="true"
+ v-model="dueReturnDateValue"
+ @confirm="onDueDateConfirm"
+ @cancel="showDueDatePicker = false"
+ mode="date"
+ />
+ </up-popup>
+
+ <!-- 鏂囨。閫夋嫨鍣� -->
+ <up-action-sheet
+ :show="showDocPicker"
+ :actions="documentOptions"
+ title="閫夋嫨鍊熼槄涔︾睄"
+ @select="onDocSelect"
+ @close="showDocPicker = false"
+ />
+ </view>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import FooterButtons from "@/components/FooterButtons.vue";
+import { addBorrow, updateBorrow, getDocumentList } from "@/api/fileManagement/borrow";
+
+const formRef = ref();
+const loading = ref(false);
+const borrowId = ref("");
+const isEdit = ref(false);
+
+// 寮圭獥鏄剧ず鐘舵��
+const showDocPicker = ref(false);
+const showBorrowDatePicker = ref(false);
+const showDueDatePicker = ref(false);
+
+// 鏁版嵁
+const documentList = ref([]);
+const borrowDateValue = ref(Date.now());
+const dueReturnDateValue = ref(Date.now());
+const displayDocName = ref(""); // 鐢ㄤ簬鏄剧ず鐨勬枃妗e悕绉�
+
+const form = ref({
+ id: "",
+ documentationId: "",
+ borrower: "",
+ borrowPurpose: "",
+ borrowDate: "",
+ dueReturnDate: "",
+ remark: "",
+ borrowStatus: "",
+});
+
+const rules = {
+ borrower: [{ required: true, message: "璇疯緭鍏ュ�熼槄浜�", trigger: "blur" }],
+ documentationId: [{ required: true, message: "璇烽�夋嫨鍊熼槄涔︾睄", trigger: "change" }],
+ borrowPurpose: [{ required: true, message: "璇疯緭鍏ュ�熼槄鐩殑", trigger: "blur" }],
+ borrowDate: [{ required: true, message: "璇烽�夋嫨鍊熼槄鏃ユ湡", trigger: "change" }],
+ dueReturnDate: [{ required: true, message: "璇烽�夋嫨搴斿綊杩樻棩鏈�", trigger: "change" }],
+};
+
+// 椤甸潰鏍囬
+const pageTitle = computed(() => {
+ if (isEdit.value) {
+ return form.value.borrowStatus === "褰掕繕" ? "鍊熼槄璇︽儏" : "缂栬緫鍊熼槄";
+ }
+ return "鏂板鍊熼槄";
+});
+
+// 鏄惁宸插綊杩�
+const isReturned = computed(() => {
+ return form.value.borrowStatus === "褰掕繕";
+});
+
+// 鏂囨。閫夐」锛堜粎鏂板妯″紡浣跨敤锛�
+const documentOptions = computed(() => {
+ return documentList.value.map((item) => ({
+ name: item.docName || item.name,
+ id: item.id,
+ }));
+});
+
+// 杩斿洖涓婁竴椤�
+const goBack = () => {
+ uni.removeStorageSync("borrowEditData");
+ uni.navigateBack();
+};
+
+// 鍔犺浇鏂囨。鍒楄〃锛堜粎鏂板妯″紡闇�瑕侊級
+const loadDocumentList = async () => {
+ try {
+ const res = await getDocumentList();
+ if (res.code === 200) {
+ documentList.value = res.data || [];
+ }
+ } catch (error) {
+ console.error("鑾峰彇鏂囨。鍒楄〃澶辫触", error);
+ }
+};
+
+// 鏂囨。閫夋嫨纭
+const onDocSelect = (e) => {
+ form.value.documentationId = e.id;
+ displayDocName.value = e.name;
+ showDocPicker.value = false;
+};
+
+// 鍊熼槄鏃ユ湡纭
+const onBorrowDateConfirm = (e) => {
+ const date = new Date(e.value);
+ form.value.borrowDate = formatDate(date);
+ showBorrowDatePicker.value = false;
+};
+
+// 搴斿綊杩樻棩鏈熺‘璁�
+const onDueDateConfirm = (e) => {
+ const date = new Date(e.value);
+ form.value.dueReturnDate = formatDate(date);
+ showDueDatePicker.value = false;
+};
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+};
+
+// 鎻愪氦琛ㄥ崟
+const handleSubmit = () => {
+ // 濡傛灉宸插綊杩橈紝绂佹鎻愪氦
+ if (isReturned.value) {
+ uni.showToast({ title: "宸插綊杩樼殑鍊熼槄璁板綍涓嶈兘缂栬緫", icon: "none" });
+ return;
+ }
+
+ formRef.value
+ .validate()
+ .then(async () => {
+ try {
+ loading.value = true;
+
+ if (isEdit.value) {
+ // 缂栬緫妯″紡
+ const res = await updateBorrow({
+ id: form.value.id,
+ borrower: form.value.borrower,
+ borrowPurpose: form.value.borrowPurpose,
+ borrowDate: form.value.borrowDate,
+ dueReturnDate: form.value.dueReturnDate,
+ remark: form.value.remark,
+ });
+
+ if (res.code === 200) {
+ uni.showToast({ title: "缂栬緫鎴愬姛", icon: "success" });
+ setTimeout(() => {
+ goBack();
+ }, 1500);
+ } else {
+ uni.showToast({ title: res.msg || "缂栬緫澶辫触", icon: "none" });
+ }
+ } else {
+ // 鏂板妯″紡
+ const res = await addBorrow({
+ documentationId: form.value.documentationId,
+ borrower: form.value.borrower,
+ borrowPurpose: form.value.borrowPurpose,
+ borrowDate: form.value.borrowDate,
+ dueReturnDate: form.value.dueReturnDate,
+ borrowStatus: "鍊熼槄",
+ remark: form.value.remark,
+ });
+
+ if (res.code === 200) {
+ uni.showToast({ title: "鏂板鎴愬姛", icon: "success" });
+ setTimeout(() => {
+ goBack();
+ }, 1500);
+ } else {
+ uni.showToast({ title: res.msg || "鏂板澶辫触", icon: "none" });
+ }
+ }
+ } catch (error) {
+ uni.showToast({ title: "鎿嶄綔澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ })
+ .catch(() => {
+ // 楠岃瘉澶辫触
+ });
+};
+
+// 椤甸潰鍔犺浇
+onLoad((options) => {
+ if (options.id) {
+ // 缂栬緫妯″紡
+ isEdit.value = true;
+ borrowId.value = options.id;
+
+ // 浠� storage 鑾峰彇缂栬緫鏁版嵁
+ const editDataStr = uni.getStorageSync("borrowEditData");
+ if (editDataStr) {
+ try {
+ const data = JSON.parse(editDataStr);
+ Object.assign(form.value, data);
+ borrowDateValue.value = new Date(data.borrowDate).getTime();
+ dueReturnDateValue.value = new Date(data.dueReturnDate).getTime();
+ // 鐩存帴浣跨敤浼犻�掔殑鏂囨。鍚嶇О鏄剧ず锛屽皾璇曞涓彲鑳界殑瀛楁鍚�
+ displayDocName.value = data.docName || data.documentationName || data.fileName || data.name || "";
+ } catch (e) {
+ console.error("瑙f瀽缂栬緫鏁版嵁澶辫触", e);
+ }
+ }
+ } else {
+ // 鏂板妯″紡锛屽姞杞芥枃妗e垪琛ㄥ苟璁剧疆榛樿鏃ユ湡
+ loadDocumentList();
+ const today = new Date();
+ form.value.borrowDate = formatDate(today);
+ borrowDateValue.value = today.getTime();
+ }
+});
+</script>
+
+<style lang="scss">
+@import "@/static/scss/form-common.scss";
+
+.borrow-edit {
+ min-height: 100vh;
+ background: #f5f5f5;
+}
+</style>
diff --git a/src/pages/fileManagement/borrow/index.vue b/src/pages/fileManagement/borrow/index.vue
new file mode 100644
index 0000000..b051137
--- /dev/null
+++ b/src/pages/fileManagement/borrow/index.vue
@@ -0,0 +1,262 @@
+<template>
+ <view class="sales-account">
+ <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
+ <PageHeader title="鍊熼槄绠$悊" @back="goBack" />
+
+ <!-- 鎼滅储鍜岀瓫閫夊尯鍩� -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input
+ class="search-text"
+ placeholder="璇疯緭鍏ュ�熼槄浜烘悳绱�"
+ v-model="searchForm.borrower"
+ @change="getList"
+ clearable
+ />
+ </view>
+ <view class="filter-button" @click="getList">
+ <up-icon name="search" size="24" color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍊熼槄鍒楄〃 -->
+ <view class="ledger-list" v-if="borrowList.length > 0">
+ <view v-for="(item, index) in borrowList" :key="index">
+ <view class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.docName || '-' }}</text>
+ </view>
+ <view class="item-tag" :class="getStatusClass(item.borrowStatus)">
+ <text class="tag-text">{{ item.borrowStatus }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍊熼槄浜�</text>
+ <text class="detail-value">{{ item.borrower || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍊熼槄鐩殑</text>
+ <text class="detail-value">{{ item.borrowPurpose || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍊熼槄鏃ユ湡</text>
+ <text class="detail-value">{{ item.borrowDate || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴斿綊杩樻棩鏈�</text>
+ <text class="detail-value">{{ item.dueReturnDate || '-' }}</text>
+ </view>
+ <view class="detail-row" v-if="item.remark">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="detail-buttons">
+ <u-button
+ v-if="item.borrowStatus !== '褰掕繕'"
+ class="detail-button"
+ size="small"
+ type="primary"
+ @click.stop="goEdit(item)"
+ >
+ 缂栬緫
+ </u-button>
+ <u-button
+ v-if="item.borrowStatus !== '褰掕繕'"
+ class="detail-button"
+ size="small"
+ type="error"
+ plain
+ @click.stop="handleDelete(item)"
+ >
+ 鍒犻櫎
+ </u-button>
+ <u-button
+ v-if="item.borrowStatus === '褰掕繕'"
+ class="detail-button"
+ size="small"
+ type="primary"
+ plain
+ @click.stop="goView(item)"
+ >
+ 鏌ョ湅
+ </u-button>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <view v-else class="no-data">
+ <text>鏆傛棤鍊熼槄璁板綍</text>
+ </view>
+
+ <!-- 娴姩鎿嶄綔鎸夐挳 -->
+ <view class="fab-button" @click="goAdd">
+ <up-icon name="plus" size="24" color="#ffffff"></up-icon>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref, reactive } from "vue";
+import { onShow } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import { getBorrowList, deleteBorrow } from "@/api/fileManagement/borrow";
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ borrower: "",
+});
+
+// 鍊熼槄鍒楄〃鏁版嵁
+const borrowList = ref([]);
+
+// 鍒嗛〉鐩稿叧
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+// 杩斿洖涓婁竴椤�
+const goBack = () => {
+ uni.navigateBack();
+};
+
+// 鑾峰彇鐘舵�佹牱寮忕被
+const getStatusClass = (status) => {
+ if (status === "褰掕繕") return "tag-success";
+ if (status === "鍊熼槄") return "tag-warning";
+ return "tag-default";
+};
+
+// 鍔犺浇鍊熼槄鍒楄〃
+const getList = async () => {
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+
+ const query = {
+ page: -1,
+ size: -1,
+ borrower: searchForm.borrower || undefined,
+ };
+
+ try {
+ const res = await getBorrowList(query);
+ if (res.code === 200) {
+ borrowList.value = res.data.records || [];
+ pagination.total = res.data.total || 0;
+ } else {
+ uni.showToast({ title: res.msg || "鑾峰彇鍊熼槄鍒楄〃澶辫触", icon: "none" });
+ borrowList.value = [];
+ }
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇鍊熼槄鍒楄〃澶辫触", icon: "none" });
+ borrowList.value = [];
+ } finally {
+ uni.hideLoading();
+ }
+};
+
+// 璺宠浆鍒版柊澧為〉闈�
+const goAdd = () => {
+ uni.navigateTo({
+ url: "/pages/fileManagement/borrow/edit",
+ });
+};
+
+// 璺宠浆鍒扮紪杈戦〉闈�
+const goEdit = (item) => {
+ uni.setStorageSync("borrowEditData", JSON.stringify(item));
+ uni.navigateTo({
+ url: `/pages/fileManagement/borrow/edit?id=${item.id}`,
+ });
+};
+
+// 璺宠浆鍒版煡鐪嬮〉闈紙宸插綊杩樿褰曪級
+const goView = (item) => {
+ uni.setStorageSync("borrowEditData", JSON.stringify(item));
+ uni.navigateTo({
+ url: `/pages/fileManagement/borrow/edit?id=${item.id}`,
+ });
+};
+
+// 鍒犻櫎
+const handleDelete = (row) => {
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�",
+ confirmText: "纭",
+ cancelText: "鍙栨秷",
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ uni.showLoading({ title: "鍒犻櫎涓�...", mask: true });
+ const result = await deleteBorrow([row.id]);
+ if (result.code === 200) {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ } else {
+ uni.showToast({ title: result.msg || "鍒犻櫎澶辫触", icon: "none" });
+ }
+ } catch (error) {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ } finally {
+ uni.hideLoading();
+ }
+ }
+ },
+ });
+};
+
+onShow(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/styles/sales-common.scss";
+
+// 鏍囩鏍峰紡
+.item-tag {
+ border-radius: 4px;
+ padding: 2px 8px;
+
+ &.tag-success {
+ background: #4caf50;
+ }
+
+ &.tag-warning {
+ background: #ff9800;
+ }
+
+ &.tag-default {
+ background: #9e9e9e;
+ }
+}
+
+.tag-text {
+ font-size: 11px;
+ color: #ffffff;
+ font-weight: 500;
+}
+
+// 鎸夐挳鏍峰紡
+.detail-buttons {
+ padding: 12px 0;
+ display: flex;
+ gap: 12px;
+}
+
+.detail-button {
+ flex: 1;
+}
+</style>
diff --git a/src/pages/fileManagement/return/edit.vue b/src/pages/fileManagement/return/edit.vue
new file mode 100644
index 0000000..e0f9e86
--- /dev/null
+++ b/src/pages/fileManagement/return/edit.vue
@@ -0,0 +1,313 @@
+<template>
+ <view class="return-edit">
+ <PageHeader :title="pageTitle" @back="goBack" />
+
+ <up-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="110"
+ >
+ <up-form-item label="鏂囨。" prop="borrowId" required>
+ <up-input
+ v-model="displayDocName"
+ placeholder="璇烽�夋嫨鏂囨。"
+ readonly
+ :disabled="isEdit"
+ @click="!isEdit && (showDocPicker = true)"
+ />
+ <template #right>
+ <up-icon v-if="!isEdit" name="arrow-right" @click="showDocPicker = true"></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="鍊熼槄浜�" prop="borrower">
+ <up-input
+ v-model="form.borrower"
+ placeholder="閫夋嫨鏂囨。鍚庤嚜鍔ㄥ甫鍑�"
+ disabled
+ />
+ </up-form-item>
+
+ <up-form-item label="褰掕繕浜�" prop="returner" required>
+ <up-input
+ v-model="form.returner"
+ placeholder="璇疯緭鍏ュ綊杩樹汉"
+ clearable
+ :disabled="isReturned"
+ />
+ </up-form-item>
+
+ <up-form-item label="褰掕繕鏃ユ湡" prop="returnDate" required>
+ <up-input
+ v-model="form.returnDate"
+ placeholder="璇烽�夋嫨褰掕繕鏃ユ湡"
+ readonly
+ @click="!isReturned && (showReturnDatePicker = true)"
+ :disabled="isReturned"
+ />
+ <template #right>
+ <up-icon name="arrow-right" @click="!isReturned && (showReturnDatePicker = true)"></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="搴斿綊杩樻棩鏈�" prop="dueReturnDate">
+ <up-input
+ v-model="form.dueReturnDate"
+ placeholder="閫夋嫨鏂囨。鍚庤嚜鍔ㄥ甫鍑�"
+ disabled
+ />
+ </up-form-item>
+
+ <up-form-item label="澶囨敞璇存槑" prop="remark">
+ <up-textarea
+ v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉ㄨ鏄�"
+ height="80"
+ border="none"
+ :disabled="isReturned"
+ />
+ </up-form-item>
+ </up-form>
+
+ <FooterButtons
+ v-if="!isReturned"
+ :loading="loading"
+ :confirmText="isEdit ? '淇濆瓨' : '鏂板'"
+ @cancel="goBack"
+ @confirm="handleSubmit"
+ />
+
+ <!-- 鏂囨。閫夋嫨鍣� -->
+ <up-action-sheet
+ :show="showDocPicker"
+ :actions="documentOptions"
+ title="閫夋嫨鏂囨。"
+ @select="onDocSelect"
+ @close="showDocPicker = false"
+ />
+
+ <!-- 褰掕繕鏃ユ湡閫夋嫨鍣� -->
+ <up-popup :show="showReturnDatePicker" mode="bottom" @close="showReturnDatePicker = false">
+ <up-datetime-picker
+ :show="true"
+ v-model="returnDateValue"
+ @confirm="onReturnDateConfirm"
+ @cancel="showReturnDatePicker = false"
+ mode="date"
+ />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from "vue";
+import { onLoad } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import FooterButtons from "@/components/FooterButtons.vue";
+import { returnDocument, reventUpdate, getDocumentList } from "@/api/fileManagement/return";
+
+const formRef = ref();
+const loading = ref(false);
+const returnId = ref("");
+const isEdit = ref(false);
+
+// 寮圭獥鏄剧ず鐘舵��
+const showDocPicker = ref(false);
+const showReturnDatePicker = ref(false);
+
+// 鏁版嵁
+const documentList = ref([]);
+const returnDateValue = ref(Date.now());
+const displayDocName = ref(""); // 鐢ㄤ簬鏄剧ず鐨勬枃妗e悕绉�
+
+const form = ref({
+ id: "",
+ borrowId: "",
+ documentationId: "",
+ borrower: "",
+ returner: "",
+ borrowStatus: "",
+ returnDate: "",
+ dueReturnDate: "",
+ remark: "",
+});
+
+const rules = {
+ borrowId: [{ required: true, message: "璇烽�夋嫨鏂囨。", trigger: "change" }],
+ returner: [{ required: true, message: "璇疯緭鍏ュ綊杩樹汉", trigger: "blur" }],
+ returnDate: [{ required: true, message: "璇烽�夋嫨褰掕繕鏃ユ湡", trigger: "change" }],
+};
+
+// 椤甸潰鏍囬
+const pageTitle = computed(() => {
+ if (isEdit.value) {
+ return form.value.borrowStatus === "褰掕繕" ? "褰掕繕璇︽儏" : "缂栬緫褰掕繕";
+ }
+ return "鏂板褰掕繕";
+});
+
+// 鏄惁宸插綊杩�
+const isReturned = computed(() => {
+ return form.value.borrowStatus === "褰掕繕";
+});
+
+// 鏂囨。閫夐」锛堜粎鏂板妯″紡浣跨敤锛�
+const documentOptions = computed(() => {
+ return documentList.value.map((item) => ({
+ name: item.docName || item.name,
+ id: item.id,
+ borrower: item.borrower,
+ dueReturnDate: item.dueReturnDate,
+ }));
+});
+
+// 杩斿洖涓婁竴椤�
+const goBack = () => {
+ uni.removeStorageSync("returnEditData");
+ uni.navigateBack();
+};
+
+// 鍔犺浇鏂囨。鍒楄〃锛堜粎鏂板妯″紡闇�瑕侊級
+const loadDocumentList = async () => {
+ try {
+ const res = await getDocumentList();
+ if (res.code === 200) {
+ documentList.value = res.data || [];
+ }
+ } catch (error) {
+ console.error("鑾峰彇鏂囨。鍒楄〃澶辫触", error);
+ }
+};
+
+// 鏂囨。閫夋嫨纭
+const onDocSelect = (e) => {
+ form.value.borrowId = e.id;
+ form.value.documentationId = e.id;
+ displayDocName.value = e.name;
+ // 鑷姩甯﹀嚭鍊熼槄浜哄拰搴斿綊杩樻棩鏈�
+ form.value.borrower = e.borrower || "";
+ form.value.dueReturnDate = e.dueReturnDate || "";
+ showDocPicker.value = false;
+};
+
+// 褰掕繕鏃ユ湡纭
+const onReturnDateConfirm = (e) => {
+ const date = new Date(e.value);
+ form.value.returnDate = formatDate(date);
+ showReturnDatePicker.value = false;
+};
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+};
+
+// 鎻愪氦琛ㄥ崟
+const handleSubmit = () => {
+ // 濡傛灉宸插綊杩橈紝绂佹鎻愪氦
+ if (isReturned.value) {
+ uni.showToast({ title: "宸插綊杩樼殑璁板綍涓嶈兘缂栬緫", icon: "none" });
+ return;
+ }
+
+ formRef.value
+ .validate()
+ .then(async () => {
+ try {
+ loading.value = true;
+
+ if (isEdit.value) {
+ // 缂栬緫妯″紡
+ const res = await reventUpdate({
+ id: form.value.id,
+ documentationId: form.value.documentationId,
+ borrower: form.value.borrower,
+ returner: form.value.returner,
+ borrowStatus: form.value.borrowStatus,
+ returnDate: form.value.returnDate,
+ dueReturnDate: form.value.dueReturnDate,
+ remark: form.value.remark,
+ });
+
+ if (res.code === 200) {
+ uni.showToast({ title: "缂栬緫鎴愬姛", icon: "success" });
+ setTimeout(() => {
+ goBack();
+ }, 1500);
+ } else {
+ uni.showToast({ title: res.msg || "缂栬緫澶辫触", icon: "none" });
+ }
+ } else {
+ // 鏂板妯″紡
+ const res = await returnDocument({
+ borrowId: form.value.borrowId,
+ borrower: form.value.borrower,
+ returner: form.value.returner,
+ borrowStatus: "褰掕繕",
+ returnDate: form.value.returnDate,
+ dueReturnDate: form.value.dueReturnDate,
+ remark: form.value.remark,
+ });
+
+ if (res.code === 200) {
+ uni.showToast({ title: "鏂板鎴愬姛", icon: "success" });
+ setTimeout(() => {
+ goBack();
+ }, 1500);
+ } else {
+ uni.showToast({ title: res.msg || "鏂板澶辫触", icon: "none" });
+ }
+ }
+ } catch (error) {
+ uni.showToast({ title: "鎿嶄綔澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ })
+ .catch(() => {
+ // 楠岃瘉澶辫触
+ });
+};
+
+// 椤甸潰鍔犺浇
+onLoad((options) => {
+ if (options.id) {
+ // 缂栬緫妯″紡
+ isEdit.value = true;
+ returnId.value = options.id;
+
+ // 浠� storage 鑾峰彇缂栬緫鏁版嵁
+ const editDataStr = uni.getStorageSync("returnEditData");
+ if (editDataStr) {
+ try {
+ const data = JSON.parse(editDataStr);
+ Object.assign(form.value, data);
+ returnDateValue.value = new Date(data.returnDate).getTime();
+ // 鐩存帴浣跨敤浼犻�掔殑 docName 鏄剧ず
+ displayDocName.value = data.docName || "";
+ } catch (e) {
+ console.error("瑙f瀽缂栬緫鏁版嵁澶辫触", e);
+ }
+ }
+ } else {
+ // 鏂板妯″紡锛屽姞杞芥枃妗e垪琛ㄥ苟璁剧疆榛樿鏃ユ湡
+ loadDocumentList();
+ const today = new Date();
+ form.value.returnDate = formatDate(today);
+ returnDateValue.value = today.getTime();
+ }
+});
+</script>
+
+<style lang="scss">
+@import "@/static/scss/form-common.scss";
+
+.return-edit {
+ min-height: 100vh;
+ background: #f5f5f5;
+}
+</style>
diff --git a/src/pages/fileManagement/return/index.vue b/src/pages/fileManagement/return/index.vue
new file mode 100644
index 0000000..3c8aeb4
--- /dev/null
+++ b/src/pages/fileManagement/return/index.vue
@@ -0,0 +1,262 @@
+<template>
+ <view class="sales-account">
+ <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
+ <PageHeader title="褰掕繕绠$悊" @back="goBack" />
+
+ <!-- 鎼滅储鍜岀瓫閫夊尯鍩� -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input
+ class="search-text"
+ placeholder="璇疯緭鍏ュ�熼槄浜烘悳绱�"
+ v-model="searchForm.borrower"
+ @change="getList"
+ clearable
+ />
+ </view>
+ <view class="filter-button" @click="getList">
+ <up-icon name="search" size="24" color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <!-- 褰掕繕鍒楄〃 -->
+ <view class="ledger-list" v-if="returnList.length > 0">
+ <view v-for="(item, index) in returnList" :key="index">
+ <view class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.docName || '-' }}</text>
+ </view>
+ <view class="item-tag" :class="getStatusClass(item.borrowStatus)">
+ <text class="tag-text">{{ item.borrowStatus }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍊熼槄浜�</text>
+ <text class="detail-value">{{ item.borrower || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">褰掕繕浜�</text>
+ <text class="detail-value">{{ item.returner || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">褰掕繕鏃ユ湡</text>
+ <text class="detail-value">{{ item.returnDate || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴斿綊杩樻棩鏈�</text>
+ <text class="detail-value">{{ item.dueReturnDate || '-' }}</text>
+ </view>
+ <view class="detail-row" v-if="item.remark">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="detail-buttons">
+ <u-button
+ v-if="item.borrowStatus !== '褰掕繕'"
+ class="detail-button"
+ size="small"
+ type="primary"
+ @click.stop="goEdit(item)"
+ >
+ 缂栬緫
+ </u-button>
+ <u-button
+ v-if="item.borrowStatus !== '褰掕繕'"
+ class="detail-button"
+ size="small"
+ type="error"
+ plain
+ @click.stop="handleDelete(item)"
+ >
+ 鍒犻櫎
+ </u-button>
+ <u-button
+ v-if="item.borrowStatus === '褰掕繕'"
+ class="detail-button"
+ size="small"
+ type="primary"
+ plain
+ @click.stop="goView(item)"
+ >
+ 鏌ョ湅
+ </u-button>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <view v-else class="no-data">
+ <text>鏆傛棤褰掕繕璁板綍</text>
+ </view>
+
+ <!-- 娴姩鎿嶄綔鎸夐挳 -->
+ <view class="fab-button" @click="goAdd">
+ <up-icon name="plus" size="24" color="#ffffff"></up-icon>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref, reactive } from "vue";
+import { onShow } from "@dcloudio/uni-app";
+import PageHeader from "@/components/PageHeader.vue";
+import { getReturnListPage, deleteReturn } from "@/api/fileManagement/return";
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ borrower: "",
+});
+
+// 褰掕繕鍒楄〃鏁版嵁
+const returnList = ref([]);
+
+// 鍒嗛〉鐩稿叧
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+// 杩斿洖涓婁竴椤�
+const goBack = () => {
+ uni.navigateBack();
+};
+
+// 鑾峰彇鐘舵�佹牱寮忕被
+const getStatusClass = (status) => {
+ if (status === "褰掕繕") return "tag-success";
+ if (status === "鍊熼槄") return "tag-warning";
+ return "tag-default";
+};
+
+// 鍔犺浇褰掕繕鍒楄〃
+const getList = async () => {
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+
+ const query = {
+ page: -1,
+ size: -1,
+ borrower: searchForm.borrower || undefined,
+ };
+
+ try {
+ const res = await getReturnListPage(query);
+ if (res.code === 200) {
+ returnList.value = res.data.records || [];
+ pagination.total = res.data.total || 0;
+ } else {
+ uni.showToast({ title: res.msg || "鑾峰彇褰掕繕鍒楄〃澶辫触", icon: "none" });
+ returnList.value = [];
+ }
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇褰掕繕鍒楄〃澶辫触", icon: "none" });
+ returnList.value = [];
+ } finally {
+ uni.hideLoading();
+ }
+};
+
+// 璺宠浆鍒版柊澧為〉闈�
+const goAdd = () => {
+ uni.navigateTo({
+ url: "/pages/fileManagement/return/edit",
+ });
+};
+
+// 璺宠浆鍒扮紪杈戦〉闈�
+const goEdit = (item) => {
+ uni.setStorageSync("returnEditData", JSON.stringify(item));
+ uni.navigateTo({
+ url: `/pages/fileManagement/return/edit?id=${item.id}`,
+ });
+};
+
+// 璺宠浆鍒版煡鐪嬮〉闈紙宸插綊杩樿褰曪級
+const goView = (item) => {
+ uni.setStorageSync("returnEditData", JSON.stringify(item));
+ uni.navigateTo({
+ url: `/pages/fileManagement/return/edit?id=${item.id}`,
+ });
+};
+
+// 鍒犻櫎
+const handleDelete = (row) => {
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�",
+ confirmText: "纭",
+ cancelText: "鍙栨秷",
+ success: async (res) => {
+ if (res.confirm) {
+ try {
+ uni.showLoading({ title: "鍒犻櫎涓�...", mask: true });
+ const result = await deleteReturn([row.id]);
+ if (result.code === 200) {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ } else {
+ uni.showToast({ title: result.msg || "鍒犻櫎澶辫触", icon: "none" });
+ }
+ } catch (error) {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ } finally {
+ uni.hideLoading();
+ }
+ }
+ },
+ });
+};
+
+onShow(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/styles/sales-common.scss";
+
+// 鏍囩鏍峰紡
+.item-tag {
+ border-radius: 4px;
+ padding: 2px 8px;
+
+ &.tag-success {
+ background: #4caf50;
+ }
+
+ &.tag-warning {
+ background: #ff9800;
+ }
+
+ &.tag-default {
+ background: #9e9e9e;
+ }
+}
+
+.tag-text {
+ font-size: 11px;
+ color: #ffffff;
+ font-weight: 500;
+}
+
+// 鎸夐挳鏍峰紡
+.detail-buttons {
+ padding: 12px 0;
+ display: flex;
+ gap: 12px;
+}
+
+.detail-button {
+ flex: 1;
+}
+</style>
diff --git a/src/pages/index.vue b/src/pages/index.vue
index bf5ef0c..a1beef3 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -3,23 +3,42 @@
<scroll-view class="scroll" scroll-y>
<!-- 椤堕儴 Banner锛氭斁鍏ユ粴鍔ㄥ尯鍩燂紝闅忛〉闈竴璧锋粴鍔紝涓嶅浐瀹氬湪椤堕儴 -->
<view class="hero-section">
- <view class="bg-img">
- <view class="hero-content">
- <view class="hero-ornaments">
- <view class="hero-glow glow-left" />
- <view class="hero-glow glow-right" />
- <view class="hero-mist mist-top" />
- <view class="hero-mist mist-bottom" />
- <view class="hero-curve curve-main" />
- <view class="hero-curve curve-sub" />
+ <view class="hero-banner">
+ <view class="hero-top">
+ <view class="hero-copy">
+ <view class="hero-badge">缁忚惀鐪嬫澘</view>
+ <text class="hero-title">{{ heroTitle }}</text>
+ <text class="hero-subtitle">{{ heroSubtitle }}</text>
+ <view class="hero-meta">
+ <text
+ v-for="item in heroMetaItems"
+ :key="item"
+ class="hero-meta-item"
+ >
+ {{ item }}
+ </text>
+ </view>
+ </view>
+ <view class="hero-avatar">
+ <text class="hero-avatar-text">{{ heroInitial }}</text>
</view>
</view>
- <view class="hero-wave"></view>
+ <view class="hero-panels">
+ <view
+ v-for="item in heroMetrics"
+ :key="item.label"
+ class="hero-panel"
+ >
+ <text class="hero-panel-label">{{ item.label }}</text>
+ <text class="hero-panel-value">{{ item.value }}</text>
+ <text class="hero-panel-hint">{{ item.hint }}</text>
+ </view>
+ </view>
</view>
</view>
<!-- 蹇嵎鍏ュ彛 -->
- <view class="quick-section">
+ <view v-if="quickTools.length" class="quick-section">
<up-grid :border="false" col="4">
<up-grid-item
v-for="item in quickTools"
@@ -35,7 +54,7 @@
</view>
<!-- 鏁版嵁鎬昏 -->
- <view class="section">
+ <view v-if="hasOverviewSection" class="section">
<view class="section-header">
<view class="section-title">
<view class="title-bar" />
@@ -48,7 +67,7 @@
</view>
<view v-show="overviewExpanded" class="overview">
- <view class="overview-card sales">
+ <view v-if="canShowSalesOverview" class="overview-card sales">
<view class="card-left">
<text class="card-title">閿�鍞暟鎹�</text>
<view class="card-metrics">
@@ -64,7 +83,7 @@
</view>
</view>
- <view class="overview-card purchase">
+ <view v-if="canShowPurchaseOverview" class="overview-card purchase">
<view class="card-left">
<text class="card-title">閲囪喘鏁版嵁</text>
<view class="card-metrics">
@@ -80,7 +99,7 @@
</view>
</view>
- <view class="overview-card stock">
+ <view v-if="canShowStockOverview" class="overview-card stock">
<view class="card-left">
<text class="card-title">搴撳瓨鏁版嵁</text>
<view class="card-metrics">
@@ -99,7 +118,7 @@
</view>
<!-- 瀹㈡埛鍚堝悓閲戦鍒嗘瀽 -->
- <view class="section">
+ <view v-if="canShowContractAnalysis" class="section">
<view class="section-header">
<view class="section-title">
<view class="title-bar" />
@@ -197,12 +216,15 @@
import { analysisCustomerContractAmounts, getBusiness } from "@/api/viewIndex";
import { createVersionUpgradeChecker } from "@/utils/versionUpgrade";
import DownloadProgressMask from "@/components/DownloadProgressMask.vue";
+import useUserStore from "@/store/modules/user";
const imgNum1 = "/static/images/index/num1.png";
const imgNum2 = "/static/images/index/num2.png";
const imgNum3 = "/static/images/index/num3.png";
-const quickTools = [
+const userStore = useUserStore();
+
+const quickToolSource = [
{
label: "鐢熶骇鎶ュ伐",
icon: "/static/images/icon/shengchanbaogong.svg",
@@ -224,6 +246,8 @@
// route: "/pages/equipmentManagement/repair/index",
// },
];
+const quickTools = ref([...quickToolSource]);
+const allowedMenuTitles = ref(new Set());
const isCanvas2d = ref(false);
@@ -272,6 +296,125 @@
uni.showToast({ title: "鏇村鍔熻兘寰呮帴鍏�", icon: "none" });
}
+
+function filterQuickToolsByRoutes() {
+ const routers = userStore.routers || [];
+
+ if (!routers || routers.length === 0) {
+ allowedMenuTitles.value = new Set();
+ quickTools.value = [...quickToolSource];
+ return;
+ }
+
+ const titles = new Set();
+ const collectMenuTitles = (routes) => {
+ if (!Array.isArray(routes)) return;
+ routes.forEach((route) => {
+ if (route.meta && route.meta.title) {
+ titles.add(route.meta.title);
+ }
+ if (route.children && route.children.length > 0) {
+ collectMenuTitles(route.children);
+ }
+ });
+ };
+ collectMenuTitles(routers);
+ allowedMenuTitles.value = titles;
+
+ quickTools.value = quickToolSource.filter((item) =>
+ titles.has(item.label)
+ );
+}
+
+function hasAnyPermission(titles) {
+ const titleSet = allowedMenuTitles.value;
+ if (!titleSet || titleSet.size === 0) return true;
+ return titles.some((title) => titleSet.has(title));
+}
+
+const canShowSalesOverview = computed(() => hasAnyPermission(["閿�鍞彴璐�"]));
+const canShowPurchaseOverview = computed(() => hasAnyPermission(["閲囪喘鍙拌处"]));
+const canShowStockOverview = computed(() => hasAnyPermission(["搴撳瓨绠$悊"]));
+const hasOverviewSection = computed(
+ () =>
+ canShowSalesOverview.value ||
+ canShowPurchaseOverview.value ||
+ canShowStockOverview.value
+);
+const canShowContractAnalysis = computed(() =>
+ hasAnyPermission(["閿�鍞彴璐�", "瀹㈡埛妗f", "瀹㈡埛寰�鏉�"])
+);
+
+const userDisplayName = computed(
+ () => userStore.nickName
+);
+const heroInitial = computed(() => userDisplayName.value.slice(0, 1).toUpperCase());
+const heroTitle = computed(() => `浣犲ソ锛�${userDisplayName.value}`);
+const heroSubtitle = computed(
+ () => userStore.currentFactoryName || "褰撳墠璐﹀彿宸茶繘鍏ヤ笟鍔¢椤�"
+);
+const heroMetaItems = computed(() => {
+ const items = [];
+ if (userStore.roleName) items.push(userStore.roleName);
+ if (userStore.currentLoginTime) items.push(`鐧诲綍浜� ${userStore.currentLoginTime}`);
+ if (!items.length) items.push("褰撳墠涓氬姟姒傝");
+ return items;
+});
+const visibleSectionCount = computed(() => {
+ let count = 0;
+ if (quickTools.value.length > 0) count += 1;
+ if (hasOverviewSection.value) count += 1;
+ if (canShowContractAnalysis.value) count += 1;
+ return count;
+});
+const heroMetrics = computed(() => {
+ const items = [];
+
+ if (canShowSalesOverview.value) {
+ items.push({
+ label: "閿�鍞�",
+ value: overviewCards.value.sales.today,
+ hint: "鏈湀钀ヤ笟棰�",
+ });
+ }
+ if (canShowPurchaseOverview.value) {
+ items.push({
+ label: "閲囪喘",
+ value: overviewCards.value.purchase.today,
+ hint: "鏈湀閲囪喘棰�",
+ });
+ }
+ if (canShowStockOverview.value) {
+ items.push({
+ label: "搴撳瓨",
+ value: overviewCards.value.stock.today,
+ hint: "褰撳墠搴撳瓨閲�",
+ });
+ }
+ if (canShowContractAnalysis.value && items.length < 3) {
+ items.push({
+ label: "鍚堝悓",
+ value: contractSummaryView.value.sumText,
+ hint: "瀹㈡埛鍚堝悓棰�",
+ });
+ }
+ if (items.length < 3) {
+ items.push({
+ label: "蹇嵎",
+ value: String(quickTools.value.length),
+ hint: "鍙敤鍏ュ彛",
+ });
+ }
+ if (items.length < 3) {
+ items.push({
+ label: "妯″潡",
+ value: String(visibleSectionCount.value),
+ hint: "鍙鏉垮潡",
+ });
+ }
+
+ return items.slice(0, 3);
+});
function getByPath(obj, path) {
if (!obj || !path) return undefined;
@@ -423,7 +566,13 @@
async function loadHome() {
chartReady.value = false;
try {
- const [bRes, cRes] = await Promise.all([getBusiness(), analysisCustomerContractAmounts()]);
+ const businessPromise = hasOverviewSection.value
+ ? getBusiness()
+ : Promise.resolve({ data: {} });
+ const contractPromise = canShowContractAnalysis.value
+ ? analysisCustomerContractAmounts()
+ : Promise.resolve({ data: { item: [], sum: "0", chain: "0", yny: "0" } });
+ const [bRes, cRes] = await Promise.all([businessPromise, contractPromise]);
businessRaw.value = bRes?.data || {};
const cData = cRes?.data || {};
contractSummary.value = {
@@ -448,11 +597,21 @@
isCanvas2d.value = false;
}
triggerVersionCheck("onMounted");
- loadHome();
+ userStore
+ .getRouters()
+ .then(() => {
+ filterQuickToolsByRoutes();
+ loadHome();
+ })
+ .catch(() => {
+ filterQuickToolsByRoutes();
+ loadHome();
+ });
});
onShow(() => {
triggerVersionCheck("onShow");
+ filterQuickToolsByRoutes();
});
</script>
@@ -508,146 +667,169 @@
}
}
.hero-section {
- margin: 0 12px;
- margin-bottom: 12px;
- animation: fadeInUp 0.6s ease-out 0.1s both;
+ margin: 0 14px 12px;
+ animation: fadeInUp 0.6s ease-out 0.1s both;
}
-.bg-img {
- width: 100%;
- height: 10.25rem;
- background:
- linear-gradient(135deg, rgba(234, 245, 255, 0.98) 0%, rgba(220, 239, 255, 0.94) 42%, rgba(244, 250, 255, 0.96) 100%),
- url("/static/images/banner/backview.png") center/cover no-repeat;
- border-radius: 18px;
- position: relative;
- overflow: hidden;
- box-shadow: 0 12px 30px rgba(118, 154, 186, 0.16);
-
- &::before {
- content: "";
- position: absolute;
- inset: 0;
- background:
- radial-gradient(circle at 14% 22%, rgba(255, 255, 255, 0.95) 0, rgba(255, 255, 255, 0) 28%),
- radial-gradient(circle at 84% 18%, rgba(191, 226, 255, 0.7) 0, rgba(191, 226, 255, 0) 26%),
- linear-gradient(180deg, rgba(255, 255, 255, 0.46) 0%, rgba(255, 255, 255, 0.16) 42%, rgba(206, 229, 247, 0.22) 100%);
- pointer-events: none;
- }
-
- &::after {
- content: "";
- position: absolute;
- left: 18%;
- bottom: -44px;
- width: 64%;
- height: 88px;
- background: radial-gradient(ellipse at center, rgba(255, 255, 255, 0.72) 0%, rgba(255, 255, 255, 0) 72%);
- border-radius: 50%;
- filter: blur(10px);
- pointer-events: none;
- }
+.hero-banner {
+ position: relative;
+ overflow: hidden;
+ border-radius: 18px;
+ padding: 18px 16px 16px;
+ min-height: 182px;
+ background:
+ linear-gradient(135deg, rgba(22, 74, 170, 0.92) 0%, rgba(33, 115, 185, 0.88) 48%, rgba(18, 156, 144, 0.82) 100%),
+ url("/static/images/banner/backview.png") center right / cover no-repeat;
+ box-shadow: 0 14px 34px rgba(29, 78, 137, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+
+ &::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background:
+ linear-gradient(120deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.02) 32%, rgba(255, 255, 255, 0) 60%),
+ radial-gradient(circle at top right, rgba(255, 255, 255, 0.24) 0%, rgba(255, 255, 255, 0) 34%);
+ pointer-events: none;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ right: -28px;
+ bottom: -34px;
+ width: 156px;
+ height: 156px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.18) 0%, rgba(255, 255, 255, 0) 72%);
+ pointer-events: none;
+ }
}
-.hero-content {
- position: relative;
- z-index: 1;
- padding: 16px 16px 14px;
- height: 100%;
- backdrop-filter: blur(2px);
+.hero-top {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 14px;
}
-.hero-ornaments {
- position: relative;
- width: 100%;
- height: 100%;
+.hero-copy {
+ min-width: 0;
+ flex: 1;
}
-.hero-glow {
- position: absolute;
- border-radius: 50%;
- filter: blur(4px);
- background: radial-gradient(circle, rgba(255, 255, 255, 0.96) 0%, rgba(255, 255, 255, 0) 72%);
- opacity: 0.9;
+.hero-badge {
+ display: inline-flex;
+ align-items: center;
+ height: 26px;
+ padding: 0 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.16);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ color: rgba(255, 255, 255, 0.92);
+ font-size: 11px;
+ font-weight: 600;
}
-.hero-glow.glow-left {
- left: -10px;
- top: 8px;
- width: 120px;
- height: 120px;
+.hero-title {
+ display: block;
+ margin-top: 12px;
+ color: #ffffff;
+ font-size: 24px;
+ font-weight: 700;
+ line-height: 1.2;
}
-.hero-glow.glow-right {
- right: -20px;
- top: 4px;
- width: 144px;
- height: 144px;
- background: radial-gradient(circle, rgba(207, 234, 255, 0.92) 0%, rgba(207, 234, 255, 0) 74%);
+.hero-subtitle {
+ display: block;
+ margin-top: 8px;
+ color: rgba(255, 255, 255, 0.84);
+ font-size: 13px;
+ line-height: 1.45;
}
-.hero-mist {
- position: absolute;
- border-radius: 999px;
- background: linear-gradient(90deg, rgba(255, 255, 255, 0.52), rgba(255, 255, 255, 0.08));
- border: 1px solid rgba(255, 255, 255, 0.34);
- backdrop-filter: blur(10px);
- box-shadow: 0 10px 24px rgba(154, 190, 219, 0.14);
+.hero-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 12px;
}
-.hero-mist.mist-top {
- left: 18px;
- top: 20px;
- width: 112px;
- height: 18px;
+.hero-meta-item {
+ padding: 3px 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.12);
+ color: rgba(255, 255, 255, 0.86);
+ font-size: 11px;
+ line-height: 18px;
}
-.hero-mist.mist-bottom {
- left: 18px;
- top: 48px;
- width: 72px;
- height: 10px;
- opacity: 0.82;
+.hero-avatar {
+ position: relative;
+ z-index: 1;
+ width: 52px;
+ height: 52px;
+ flex: 0 0 52px;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.18);
+ border: 1px solid rgba(255, 255, 255, 0.22);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
-.hero-curve {
- position: absolute;
- border-radius: 999px;
- border: 2px solid rgba(255, 255, 255, 0.72);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.12));
- box-shadow:
- 0 10px 26px rgba(154, 190, 219, 0.16),
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
- backdrop-filter: blur(10px);
+.hero-avatar-text {
+ color: #ffffff;
+ font-size: 20px;
+ font-weight: 700;
}
-.hero-curve.curve-main {
- right: 18px;
- bottom: 22px;
- width: 176px;
- height: 84px;
- transform: rotate(-9deg);
- border-top-left-radius: 90px;
- border-bottom-right-radius: 90px;
- opacity: 1;
+.hero-panels {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+ margin-top: 18px;
}
-.hero-curve.curve-sub {
- right: 96px;
- bottom: 60px;
- width: 104px;
- height: 40px;
- transform: rotate(-9deg);
- border-top-left-radius: 60px;
- border-bottom-right-radius: 60px;
- opacity: 0.9;
+.hero-panel {
+ min-width: 0;
+ padding: 12px 10px;
+ border-radius: 14px;
+ background: rgba(11, 25, 48, 0.18);
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ backdrop-filter: blur(10px);
}
-.hero-wave {
- height: 1.1rem;
- background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(244, 249, 253, 0.96) 100%);
- margin-top: -1px;
- position: relative;
+.hero-panel-label {
+ display: block;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 11px;
+ line-height: 1.2;
+}
+
+.hero-panel-value {
+ display: block;
+ margin-top: 8px;
+ color: #ffffff;
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 1.2;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.hero-panel-hint {
+ display: block;
+ margin-top: 6px;
+ color: rgba(255, 255, 255, 0.72);
+ font-size: 11px;
+ line-height: 1.2;
}
.safe-top {
diff --git a/src/pages/indexItem.vue b/src/pages/indexItem.vue
index 61f6c15..35d13cf 100644
--- a/src/pages/indexItem.vue
+++ b/src/pages/indexItem.vue
@@ -27,6 +27,7 @@
<script setup>
import { onMounted, reactive, ref } from "vue";
+ import { OA_WORKBENCH_ITEMS } from "@/config/oaWorkbench.js";
import useUserStore from "@/store/modules/user";
import { onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
@@ -110,10 +111,15 @@
{ icon: "/static/images/icon/baojiaguanli.svg", label: "鎶ヤ环瀹℃壒" },
{ icon: "/static/images/icon/fahuoguanli.svg", label: "鍙戣揣瀹℃壒" },
],
+ "OA鍔炲叕": OA_WORKBENCH_ITEMS.map(item => ({ ...item })),
};
// 澶勭悊甯哥敤鍔熻兘鐐瑰嚮
const handleCommonItemClick = item => {
+ if (item.path) {
+ uni.navigateTo({ url: item.path });
+ return;
+ }
const url = routeMapping[item.label];
if (url) {
uni.navigateTo({ url });
diff --git a/src/pages/inspectionUpload/attachment.vue b/src/pages/inspectionUpload/attachment.vue
new file mode 100644
index 0000000..058e0d6
--- /dev/null
+++ b/src/pages/inspectionUpload/attachment.vue
@@ -0,0 +1,485 @@
+<template>
+ <view class="attachment-page">
+ <!-- 椤甸潰澶撮儴 -->
+ <PageHeader :title="`鏌ョ湅闄勪欢 - ${taskInfo?.taskName || ''}`"
+ @back="goBack" />
+ <!-- 椤甸潰鍐呭 -->
+ <view class="attachment-content">
+ <!-- 鍒嗙被鏍囩椤� -->
+ <view class="attachment-tabs">
+ <view class="tab-item"
+ :class="{ active: currentViewType === 'before' }"
+ @click="switchViewType('before')">
+ 鐢熶骇鍓� ({{ getAttachmentsByType(0).length }})
+ </view>
+ <view class="tab-item"
+ :class="{ active: currentViewType === 'after' }"
+ @click="switchViewType('after')">
+ 鐢熶骇涓� ({{ getAttachmentsByType(1).length }})
+ </view>
+ <view class="tab-item"
+ :class="{ active: currentViewType === 'issue' }"
+ @click="switchViewType('issue')">
+ 鐢熶骇鍚� ({{ getAttachmentsByType(2).length }})
+ </view>
+ </view>
+ <!-- 褰撳墠鍒嗙被鐨勯檮浠跺垪琛� -->
+ <view class="attachment-list-container">
+ <view v-if="getCurrentViewAttachments().length > 0"
+ class="attachment-list">
+ <view v-for="(file, index) in getCurrentViewAttachments()"
+ :key="index"
+ class="attachment-item"
+ @click="previewAttachment(file)">
+ <view class="attachment-preview-container">
+ <image v-if="isImageFile(file)"
+ :src="file.url || file.downloadUrl"
+ class="attachment-preview"
+ mode="aspectFill" />
+ <view v-else
+ class="attachment-video-preview">
+ <u-icon name="video"
+ size="40"
+ color="#409eff"></u-icon>
+ <text class="video-text">瑙嗛</text>
+ </view>
+ </view>
+ <view class="attachment-info">
+ <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '闄勪欢' }}</text>
+ <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="attachment-empty">
+ <text class="empty-text">璇ュ垎绫绘殏鏃犻檮浠�</text>
+ </view>
+ </view>
+ </view>
+ <!-- 瑙嗛棰勮寮圭獥 -->
+ <view v-if="showVideoDialog"
+ class="video-modal-overlay"
+ @click="closeVideoPreview">
+ <view class="video-modal-container"
+ @click.stop>
+ <view class="video-modal-header">
+ <text class="video-modal-title">{{ currentVideoFile?.originalFilename || '瑙嗛棰勮' }}</text>
+ <view class="close-btn-video"
+ @click="closeVideoPreview">
+ <u-icon name="close"
+ size="20"
+ color="#fff"></u-icon>
+ </view>
+ </view>
+ <view class="video-modal-body">
+ <video v-if="currentVideoFile"
+ :src="currentVideoFile.url || currentVideoFile.downloadUrl"
+ class="video-player"
+ controls
+ autoplay
+ @error="handleVideoError"></video>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import config from "@/config";
+
+ // 浠诲姟淇℃伅
+ const taskInfo = ref(null);
+
+ // 闄勪欢鍒楄〃
+ const attachmentList = ref([]);
+
+ // 褰撳墠鏌ョ湅绫诲瀷
+ const currentViewType = ref("before"); // 'before', 'after', 'issue'
+
+ // 瑙嗛棰勮鐩稿叧鐘舵��
+ const showVideoDialog = ref(false);
+ const currentVideoFile = ref(null);
+
+ // 鏂囦欢璁块棶鍩虹鍩�
+ const filePreviewBase = config.fileUrl;
+
+ // 椤甸潰鍔犺浇
+ onLoad(options => {
+ if (options.taskInfo) {
+ try {
+ taskInfo.value = JSON.parse(decodeURIComponent(options.taskInfo));
+ loadAttachments();
+ } catch (e) {
+ console.error("瑙f瀽浠诲姟淇℃伅澶辫触:", e);
+ uni.showToast({
+ title: "鍔犺浇澶辫触",
+ icon: "error",
+ });
+ }
+ }
+ });
+
+ // 鍔犺浇闄勪欢鏁版嵁
+ const loadAttachments = () => {
+ const task = taskInfo.value;
+ if (!task) return;
+
+ attachmentList.value = [];
+
+ // 鍚庣鍙嶆樉瀛楁 (VO浼樺厛)
+ const beforeList = Array.isArray(task?.commonFileListBeforeVO)
+ ? task.commonFileListBeforeVO
+ : Array.isArray(task?.commonFileListBefore)
+ ? task.commonFileListBefore
+ : [];
+
+ const duringList = Array.isArray(task?.commonFileListVO)
+ ? task.commonFileListVO
+ : Array.isArray(task?.commonFileListAfter)
+ ? task.commonFileListAfter
+ : []; // 鍏煎鏃ч�昏緫鎴栧懡鍚嶄笉涓�鑷�
+
+ const afterList = Array.isArray(task?.commonFileListAfterVO)
+ ? task.commonFileListAfterVO
+ : Array.isArray(task?.commonFileListIssue)
+ ? task.commonFileListIssue
+ : [];
+
+ // 濡傛灉 VO 閮芥病鏈夛紝灏濊瘯浠� commonFileList 杩囨护
+ const allList = Array.isArray(task?.commonFileList)
+ ? task.commonFileList
+ : [];
+
+ const finalBefore =
+ beforeList.length > 0 ? beforeList : allList.filter(f => f?.type === 10);
+ const finalDuring =
+ duringList.length > 0 ? duringList : allList.filter(f => f?.type === 11);
+ const finalAfter =
+ afterList.length > 0 ? afterList : allList.filter(f => f?.type === 12);
+
+ const mapToViewFile = (file, viewType) => {
+ // 鍏煎 previewURL, previewUrl, url, downloadURL, downloadUrl
+ const rawUrl =
+ file?.previewURL ||
+ file?.previewUrl ||
+ file?.url ||
+ file?.downloadURL ||
+ file?.downloadUrl ||
+ "";
+ const u = normalizeFileUrl(rawUrl);
+
+ return {
+ ...file,
+ type: viewType,
+ name:
+ file?.originalFilename || file?.bucketFilename || file?.name || "闄勪欢",
+ bucketFilename: file?.bucketFilename || file?.name,
+ originalFilename: file?.originalFilename || file?.name,
+ url: u,
+ downloadUrl: u,
+ size: file?.byteSize || file?.size || 0,
+ };
+ };
+
+ attachmentList.value.push(...finalBefore.map(f => mapToViewFile(f, 0)));
+ attachmentList.value.push(...finalDuring.map(f => mapToViewFile(f, 1)));
+ attachmentList.value.push(...finalAfter.map(f => mapToViewFile(f, 2)));
+ };
+
+ // 灏嗗悗绔繑鍥炵殑鏂囦欢鍦板潃瑙勮寖鎴愬彲璁块棶URL
+ const normalizeFileUrl = rawUrl => {
+ try {
+ if (!rawUrl || typeof rawUrl !== "string") return "";
+ const url = rawUrl.trim();
+ if (!url) return "";
+ if (/^https?:\/\//i.test(url)) return url;
+ if (url.startsWith("/")) return `${filePreviewBase}${url}`;
+
+ // Windows path -> web path
+ if (/^[a-zA-Z]:\\/.test(url)) {
+ const normalized = url.replace(/\\/g, "/");
+ const idx = normalized.indexOf("/prod/");
+ if (idx >= 0) {
+ const relative = normalized.slice(idx + "/prod/".length);
+ return `${filePreviewBase}/${relative}`;
+ }
+ return normalized;
+ }
+
+ return `${filePreviewBase}/${url.replace(/^\//, "")}`;
+ } catch (e) {
+ return rawUrl || "";
+ }
+ };
+
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 鍒囨崲鏌ョ湅绫诲瀷
+ const switchViewType = type => {
+ currentViewType.value = type;
+ };
+
+ // 鏍规嵁type鑾峰彇瀵瑰簲鍒嗙被鐨勯檮浠�
+ const getAttachmentsByType = typeValue => {
+ return attachmentList.value.filter(file => file.type === typeValue) || [];
+ };
+
+ // 鑾峰彇褰撳墠鏌ョ湅绫诲瀷鐨勯檮浠�
+ const getCurrentViewAttachments = () => {
+ switch (currentViewType.value) {
+ case "before":
+ return getAttachmentsByType(0);
+ case "after":
+ return getAttachmentsByType(1);
+ case "issue":
+ return getAttachmentsByType(2);
+ default:
+ return [];
+ }
+ };
+
+ // 鍒ゆ柇鏄惁涓哄浘鐗囨枃浠�
+ const isImageFile = file => {
+ if (file.contentType && file.contentType.startsWith("image/")) {
+ return true;
+ }
+ if (file.type === "image") return true;
+
+ const name = file.bucketFilename || file.originalFilename || file.name || "";
+ const ext = name.split(".").pop()?.toLowerCase();
+ return ["jpg", "jpeg", "png", "gif", "webp"].includes(ext);
+ };
+
+ // 棰勮闄勪欢
+ const previewAttachment = file => {
+ if (isImageFile(file)) {
+ const imageUrls = getCurrentViewAttachments()
+ .filter(f => isImageFile(f))
+ .map(f => f.url || f.downloadUrl);
+
+ uni.previewImage({
+ urls: imageUrls,
+ current: file.url || file.downloadUrl,
+ });
+ } else {
+ showVideoPreview(file);
+ }
+ };
+
+ // 鏄剧ず瑙嗛棰勮
+ const showVideoPreview = file => {
+ currentVideoFile.value = file;
+ showVideoDialog.value = true;
+ };
+
+ // 鍏抽棴瑙嗛棰勮
+ const closeVideoPreview = () => {
+ showVideoDialog.value = false;
+ currentVideoFile.value = null;
+ };
+
+ // 瑙嗛鎾斁閿欒澶勭悊
+ const handleVideoError = () => {
+ uni.showToast({
+ title: "瑙嗛鎾斁澶辫触",
+ icon: "error",
+ });
+ };
+
+ // 鏍煎紡鍖栨枃浠跺ぇ灏�
+ const formatFileSize = size => {
+ if (!size) return "";
+ if (size < 1024) return size + "B";
+ if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
+ return (size / (1024 * 1024)).toFixed(1) + "MB";
+ };
+</script>
+
+<style scoped>
+ .attachment-page {
+ min-height: 100vh;
+ background-color: #f5f5f5;
+ }
+
+ .attachment-content {
+ padding: 15px;
+ }
+
+ /* 鏍囩椤垫牱寮� */
+ .attachment-tabs {
+ display: flex;
+ background: #fff;
+ border-radius: 12px;
+ margin-bottom: 15px;
+ padding: 4px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .tab-item {
+ flex: 1;
+ text-align: center;
+ padding: 12px 8px;
+ font-size: 14px;
+ color: #666;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+ }
+
+ .tab-item.active {
+ background: #409eff;
+ color: #fff;
+ font-weight: 500;
+ }
+
+ /* 闄勪欢鍒楄〃鏍峰紡 */
+ .attachment-list-container {
+ background: #fff;
+ border-radius: 12px;
+ padding: 15px;
+ min-height: 400px;
+ }
+
+ .attachment-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+ }
+
+ .attachment-item {
+ width: calc(33.33% - 10px);
+ background: #f8f9fa;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ }
+
+ .attachment-item:active {
+ transform: scale(0.98);
+ }
+
+ .attachment-preview-container {
+ width: 100%;
+ height: 120px;
+ background: #e9ecef;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .attachment-preview {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ .attachment-video-preview {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ }
+
+ .video-text {
+ font-size: 12px;
+ color: #666;
+ }
+
+ .attachment-info {
+ padding: 10px;
+ }
+
+ .attachment-name {
+ font-size: 12px;
+ color: #333;
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 4px;
+ }
+
+ .attachment-size {
+ font-size: 10px;
+ color: #999;
+ }
+
+ /* 绌虹姸鎬佹牱寮� */
+ .attachment-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 80px 20px;
+ color: #999;
+ }
+
+ .empty-text {
+ margin-top: 15px;
+ font-size: 14px;
+ }
+
+ /* 瑙嗛寮圭獥鏍峰紡 */
+ .video-modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.9);
+ z-index: 10000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ }
+
+ .video-modal-container {
+ width: 100%;
+ max-width: 800px;
+ background: #000;
+ border-radius: 12px;
+ overflow: hidden;
+ }
+
+ .video-modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ background: #1a1a1a;
+ }
+
+ .video-modal-title {
+ font-size: 16px;
+ color: #fff;
+ font-weight: 500;
+ }
+
+ .close-btn-video {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 50%;
+ }
+
+ .video-modal-body {
+ padding: 20px;
+ }
+
+ .video-player {
+ width: 100%;
+ height: 400px;
+ border-radius: 8px;
+ }
+</style>
diff --git a/src/pages/inspectionUpload/components/formDia.vue b/src/pages/inspectionUpload/components/formDia.vue
index 6b900cd..62bf40f 100644
--- a/src/pages/inspectionUpload/components/formDia.vue
+++ b/src/pages/inspectionUpload/components/formDia.vue
@@ -8,53 +8,99 @@
>
<view class="popup-content">
<view class="popup-header">
- <text class="popup-title">涓婁紶</text>
+ <text class="popup-title">宸℃璁板綍涓婁紶</text>
</view>
<view class="upload-container">
+ <!-- 寮傚父鐘舵�侀�夋嫨 -->
<view class="form-container">
- <view class="title">鐢熶骇鍓�</view>
- <u-upload
- :fileList="beforeModelValue"
- @afterRead="afterRead"
- @delete="deleteFile"
- name="before"
- multiple
- :maxCount="10"
- :maxSize="5 * 1024 * 1024"
- accept="image/*"
- :previewFullImage="true"
- ></u-upload>
+ <view class="title">宸℃鐘舵��</view>
+ <view class="exception-section">
+ <view class="exception-options">
+ <view
+ class="exception-option"
+ :class="{ active: hasException === false }"
+ @click="setExceptionStatus(false)"
+ >
+ <u-icon name="checkmark-circle" size="20" color="#52c41a"></u-icon>
+ <text class="option-text">姝e父</text>
+ </view>
+ <view
+ class="exception-option"
+ :class="{ active: hasException === true }"
+ @click="setExceptionStatus(true)"
+ >
+ <u-icon name="close-circle" size="20" color="#ff4d4f"></u-icon>
+ <text class="option-text">瀛樺湪寮傚父</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 寮傚父鎻忚堪锛堜粎鍦ㄥ紓甯告椂鏄剧ず锛� -->
+ <view class="form-container" v-if="hasException === true">
+ <view class="title">寮傚父鎻忚堪</view>
+ <u-input
+ v-model="exceptionDescription"
+ type="textarea"
+ :maxlength="500"
+ placeholder="璇锋弿杩板紓甯告儏鍐�..."
+ :customStyle="{ padding: '10px', backgroundColor: '#f5f5f5' }"
+ />
</view>
- <view class="form-container">
- <view class="title">鐢熶骇鍚�</view>
- <u-upload
- :fileList="afterModelValue"
- @afterRead="afterRead"
- @delete="deleteFile"
- name="after"
- multiple
- :maxCount="10"
- :maxSize="5 * 1024 * 1024"
- accept="image/*"
- :previewFullImage="true"
- ></u-upload>
- </view>
-
- <view class="form-container">
- <view class="title">鐢熶骇闂</view>
- <u-upload
- :fileList="issueModelValue"
- @afterRead="afterRead"
- @delete="deleteFile"
- name="issue"
- multiple
- :maxCount="10"
- :maxSize="5 * 1024 * 1024"
- accept="image/*"
- :previewFullImage="true"
- ></u-upload>
+ <!-- 涓婁紶鍖哄煙锛堜粎鍦ㄥ紓甯告椂鏄剧ず锛� -->
+ <template v-if="hasException === true">
+ <view class="form-container">
+ <view class="title">鐢熶骇鍓�</view>
+ <u-upload
+ :fileList="beforeModelValue"
+ @afterRead="afterRead"
+ @delete="deleteFile"
+ name="before"
+ multiple
+ :maxCount="10"
+ :maxSize="5 * 1024 * 1024"
+ accept="image/*"
+ :previewFullImage="true"
+ ></u-upload>
+ </view>
+
+ <view class="form-container">
+ <view class="title">鐢熶骇鍚�</view>
+ <u-upload
+ :fileList="afterModelValue"
+ @afterRead="afterRead"
+ @delete="deleteFile"
+ name="after"
+ multiple
+ :maxCount="10"
+ :maxSize="5 * 1024 * 1024"
+ accept="image/*"
+ :previewFullImage="true"
+ ></u-upload>
+ </view>
+
+ <view class="form-container">
+ <view class="title">鐢熶骇闂</view>
+ <u-upload
+ :fileList="issueModelValue"
+ @afterRead="afterRead"
+ @delete="deleteFile"
+ name="issue"
+ multiple
+ :maxCount="10"
+ :maxSize="5 * 1024 * 1024"
+ accept="image/*"
+ :previewFullImage="true"
+ ></u-upload>
+ </view>
+ </template>
+
+ <!-- 姝e父鐘舵�佹彁绀� -->
+ <view class="form-container normal-tip" v-if="hasException === false">
+ <u-icon name="info-circle" size="40" color="#52c41a"></u-icon>
+ <text class="tip-text">璁惧杩愯姝e父锛屾棤闇�涓婁紶鐓х墖</text>
</view>
</view>
@@ -79,6 +125,11 @@
const afterModelValue = ref([])
const issueModelValue = ref([])
const infoData = ref(null)
+
+// 寮傚父鐘舵�侊細null=鏈�夋嫨, false=姝e父, true=寮傚父
+const hasException = ref(null)
+// 寮傚父鎻忚堪
+const exceptionDescription = ref('')
// 璁$畻涓婁紶URL
const uploadFileUrl = computed(() => {
@@ -196,9 +247,43 @@
}
}
+// 璁剧疆寮傚父鐘舵��
+const setExceptionStatus = (status) => {
+ hasException.value = status
+}
+
// 鎻愪氦琛ㄥ崟
const submitForm = async () => {
try {
+ // 妫�鏌ユ槸鍚﹂�夋嫨浜嗗贰妫�鐘舵��
+ if (hasException.value === null) {
+ uni.showToast({
+ title: '璇烽�夋嫨宸℃鐘舵��',
+ icon: 'none'
+ })
+ return
+ }
+
+ // 濡傛灉鏄紓甯哥姸鎬侊紝妫�鏌ユ槸鍚︽湁涓婁紶鏂囦欢
+ if (hasException.value === true) {
+ const totalFiles = beforeModelValue.value.length + afterModelValue.value.length + issueModelValue.value.length
+ if (totalFiles === 0) {
+ uni.showToast({
+ title: '璇蜂笂浼犲紓甯哥収鐗�',
+ icon: 'none'
+ })
+ return
+ }
+ // 妫�鏌ユ槸鍚﹀~鍐欎簡寮傚父鎻忚堪
+ if (!exceptionDescription.value.trim()) {
+ uni.showToast({
+ title: '璇峰~鍐欏紓甯告弿杩�',
+ icon: 'none'
+ })
+ return
+ }
+ }
+
let arr = []
if (beforeModelValue.value.length > 0) {
arr.push(...beforeModelValue.value.map(item => ({ ...item, statusType: 0 })))
@@ -212,6 +297,8 @@
// 鎻愪氦鏁版嵁
infoData.value.storageBlobDTO = arr
+ infoData.value.hasException = hasException.value
+ infoData.value.exceptionDescription = exceptionDescription.value
await submitInspectionRecord({ ...infoData.value })
uni.showToast({
@@ -238,6 +325,8 @@
beforeModelValue.value = []
afterModelValue.value = []
issueModelValue.value = []
+ hasException.value = null
+ exceptionDescription.value = ''
}
// 鍏抽棴寮规
@@ -311,4 +400,61 @@
border-top: 1px solid #f0f0f0;
background-color: #fafafa;
}
+
+// 寮傚父鐘舵�侀�夋嫨鏍峰紡
+.exception-section {
+ padding: 10px 0;
+}
+
+.exception-options {
+ display: flex;
+ gap: 15px;
+}
+
+.exception-option {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 15px 20px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s;
+ background-color: #fff;
+
+ &.active {
+ border-color: #1890ff;
+ background-color: #e6f7ff;
+ }
+
+ &:active {
+ opacity: 0.8;
+ }
+}
+
+.option-text {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+}
+
+// 姝e父鐘舵�佹彁绀烘牱寮�
+.normal-tip {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 40px 20px;
+ background-color: #f6ffed;
+ border: 1px dashed #b7eb8f;
+ border-radius: 8px;
+
+ .tip-text {
+ margin-top: 15px;
+ font-size: 14px;
+ color: #52c41a;
+ }
+}
</style>
diff --git a/src/pages/inspectionUpload/index.vue b/src/pages/inspectionUpload/index.vue
index 135244b..80c4c76 100644
--- a/src/pages/inspectionUpload/index.vue
+++ b/src/pages/inspectionUpload/index.vue
@@ -56,6 +56,10 @@
<text class="detail-value">{{ item.taskId || item.id }}</text>
</view>
<view class="detail-item">
+ <text class="detail-label">宸℃椤圭洰</text>
+ <text class="detail-value">{{ item.inspectionProject || '鏃�' }}</text>
+ </view>
+ <view class="detail-item">
<text class="detail-label">澶囨敞</text>
<text class="detail-value">{{ item.remarks || '鏃�' }}</text>
</view>
@@ -80,7 +84,7 @@
size="small"
type="primary"
inverted></uni-tag>
- <uni-tag v-else=""
+ <uni-tag v-else
text="鏈贰妫�"
size="small"
type="warning"
@@ -95,227 +99,6 @@
<view v-if="taskTableData?.length === 0"
class="no-data">
<text>鏆傛棤鏁版嵁</text>
- </view>
- </view>
- <!-- 鍥剧墖涓婁紶寮圭獥 - 鍘熺敓瀹炵幇 -->
- <view v-if="showUploadDialog"
- class="custom-modal-overlay"
- @click="closeUploadDialog">
- <view class="custom-modal-container"
- @click.stop>
- <view class="upload-popup-content">
- <view class="upload-popup-header">
- <text class="upload-popup-title">涓婁紶宸℃璁板綍</text>
- </view>
- <view class="upload-popup-body">
- <!-- 鍒嗙被鏍囩椤� -->
- <view class="upload-tabs">
- <view class="tab-item"
- :class="{ active: currentUploadType === 'before' }"
- @click="switchUploadType('before')">
- 鐢熶骇鍓�
- </view>
- <view class="tab-item"
- :class="{ active: currentUploadType === 'after' }"
- @click="switchUploadType('after')">
- 鐢熶骇涓�
- </view>
- <view class="tab-item"
- :class="{ active: currentUploadType === 'issue' }"
- @click="switchUploadType('issue')">
- 鐢熶骇鍚�
- </view>
- </view>
- <!-- 寮傚父鐘舵�侀�夋嫨 -->
- <view class="exception-section">
- <text class="section-title">鏄惁瀛樺湪寮傚父锛�</text>
- <view class="exception-options">
- <view class="exception-option"
- :class="{ active: hasException === false }"
- @click="setExceptionStatus(false)">
- <u-icon name="checkmark-circle"
- size="20"
- color="#52c41a"></u-icon>
- <text>姝e父</text>
- </view>
- <view class="exception-option"
- :class="{ active: hasException === true }"
- @click="setExceptionStatus(true)">
- <u-icon name="close-circle"
- size="20"
- color="#ff4d4f"></u-icon>
- <text>瀛樺湪寮傚父</text>
- </view>
- </view>
- </view>
- <!-- 褰撳墠鍒嗙被鐨勪笂浼犲尯鍩� -->
- <view class="simple-upload-area">
- <view class="upload-buttons">
- <u-button type="primary"
- @click="chooseMedia('image')"
- :loading="uploading"
- :disabled="getCurrentFiles().length >= uploadConfig.limit"
- :customStyle="{ marginRight: '10px', flex: 1 }">
- <u-icon name="camera"
- size="18"
- color="#fff"
- style="margin-right: 5px;"></u-icon>
- {{ uploading ? '涓婁紶涓�...' : '鎷嶇収' }}
- </u-button>
- <u-button type="success"
- @click="chooseMedia('video')"
- :loading="uploading"
- :disabled="getCurrentFiles().length >= uploadConfig.limit"
- :customStyle="{ flex: 1 }">
- <uni-icons type="videocam"
- name="videocam"
- size="18"
- color="#fff"
- style="margin-right: 5px;"></uni-icons>
- {{ uploading ? '涓婁紶涓�...' : '鎷嶈棰�' }}
- </u-button>
- </view>
- <!-- 涓婁紶杩涘害 -->
- <view v-if="uploading"
- class="upload-progress">
- <u-line-progress :percentage="uploadProgress"
- :showText="true"
- activeColor="#409eff"></u-line-progress>
- </view>
- <!-- 褰撳墠鍒嗙被鐨勬枃浠跺垪琛� -->
- <view v-if="getCurrentFiles().length > 0"
- class="file-list">
- <view v-for="(file, index) in getCurrentFiles()"
- :key="index"
- class="file-item">
- <view class="file-preview-container">
- <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
- :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
- class="file-preview"
- mode="aspectFill" />
- <view v-else-if="file.type === 'video'"
- class="video-preview">
- <uni-icons type="videocam"
- name="videocam"
- size="18"
- color="#fff"
- style="margin-right: 5px;"></uni-icons>
- <text class="video-text">瑙嗛</text>
- </view>
- <!-- 鍒犻櫎鎸夐挳 -->
- <view class="delete-btn"
- @click="removeFile(index)">
- <u-icon name="close"
- size="12"
- color="#fff"></u-icon>
- </view>
- </view>
- <view class="file-info">
- <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '鍥剧墖' : '瑙嗛')
- }}</text>
- <text class="file-size">{{ formatFileSize(file.size) }}</text>
- </view>
- </view>
- </view>
- <view v-if="getCurrentFiles().length === 0"
- class="empty-state">
- <text>璇烽�夋嫨瑕佷笂浼犵殑{{ getUploadTypeText() }}鍥剧墖鎴栬棰�</text>
- </view>
- <!-- 缁熻淇℃伅 -->
- <view class="upload-summary">
- <text class="summary-text">
- 鐢熶骇鍓�: {{ beforeModelValue.length }}涓枃浠� |
- 鐢熶骇涓�: {{ afterModelValue.length }}涓枃浠� |
- 鐢熶骇鍚�: {{ issueModelValue.length }}涓枃浠�
- </text>
- </view>
- </view>
- </view>
- <view class="upload-popup-footer">
- <u-button @click="closeUploadDialog"
- :customStyle="{ marginRight: '10px' }">鍙栨秷</u-button>
- <u-button v-if="hasException === true"
- type="warning"
- @click="goToRepair"
- :customStyle="{ marginRight: '10px' }">
- 鏂板鎶ヤ慨
- </u-button>
- <u-button type="primary"
- @click="submitUpload">鎻愪氦</u-button>
- </view>
- </view>
- </view>
- </view>
- <!-- 鏌ョ湅闄勪欢寮圭獥 -->
- <view v-if="showAttachmentDialog"
- class="custom-modal-overlay"
- @click="closeAttachmentDialog">
- <view class="custom-modal-container"
- @click.stop>
- <view class="attachment-popup-content">
- <view class="attachment-popup-header">
- <text class="attachment-popup-title">鏌ョ湅闄勪欢 - {{ currentViewTask?.taskName }}</text>
- <view class="close-btn-attachment"
- @click="closeAttachmentDialog">
- <u-icon name="close"
- size="16"
- color="#666"></u-icon>
- </view>
- </view>
- <view class="attachment-popup-body">
- <!-- 鍒嗙被鏍囩椤� -->
- <view class="attachment-tabs">
- <view class="tab-item"
- :class="{ active: currentViewType === 'before' }"
- @click="switchViewType('before')">
- 鐢熶骇鍓� ({{ getAttachmentsByType(0).length }})
- </view>
- <view class="tab-item"
- :class="{ active: currentViewType === 'after' }"
- @click="switchViewType('after')">
- 鐢熶骇涓� ({{ getAttachmentsByType(1).length }})
- </view>
- <view class="tab-item"
- :class="{ active: currentViewType === 'issue' }"
- @click="switchViewType('issue')">
- 鐢熶骇鍚� ({{ getAttachmentsByType(2).length }})
- </view>
- </view>
- <!-- 褰撳墠鍒嗙被鐨勯檮浠跺垪琛� -->
- <view class="attachment-content">
- <view v-if="getCurrentViewAttachments().length > 0"
- class="attachment-list">
- <view v-for="(file, index) in getCurrentViewAttachments()"
- :key="index"
- class="attachment-item"
- @click="previewAttachment(file)">
- <view class="attachment-preview-container">
- <image v-if="file.type === 'image' || isImageFile(file)"
- :src="file.url || file.downloadUrl"
- class="attachment-preview"
- mode="aspectFill" />
- <view v-else
- class="attachment-video-preview">
- <u-icon name="video"
- size="24"
- color="#409eff"></u-icon>
- <text class="video-text">瑙嗛</text>
- </view>
- </view>
- <view class="attachment-info">
- <text class="attachment-name">{{ file.originalFilename || file.bucketFilename || file.name || '闄勪欢'
- }}</text>
- <text class="attachment-size">{{ formatFileSize(file.byteSize || file.size) }}</text>
- </view>
- </view>
- </view>
- <view v-else
- class="attachment-empty">
- <text>璇ュ垎绫绘殏鏃犻檮浠�</text>
- </view>
- </view>
- </view>
- </view>
</view>
</view>
<!-- 瑙嗛棰勮寮圭獥 -->
@@ -378,57 +161,9 @@
const currentScanningTask = ref(null);
const infoData = ref(null);
- // 涓婁紶鐩稿叧鐘舵��
- const showUploadDialog = ref(false);
- const uploadFiles = ref([]); // 淇濈暀鐢ㄤ簬鍏煎鎬�
- const uploadStatusType = ref(0);
- const uploading = ref(false);
- const uploadProgress = ref(0);
- const number = ref(0);
- const uploadList = ref([]);
-
- // 涓変釜鍒嗙被鐨勪笂浼犵姸鎬�
- const beforeModelValue = ref([]); // 鐢熶骇鍓�
- const afterModelValue = ref([]); // 鐢熶骇涓�
- const issueModelValue = ref([]); // 鐢熶骇鍚�
-
- // 褰撳墠婵�娲荤殑涓婁紶绫诲瀷
- const currentUploadType = ref("before"); // 'before', 'after', 'issue'
-
- // 鏌ョ湅闄勪欢鐩稿叧鐘舵��
- const showAttachmentDialog = ref(false);
- const currentViewTask = ref(null);
- const currentViewType = ref("before"); // 'before', 'after', 'issue'
- const attachmentList = ref([]); // 褰撳墠鏌ョ湅浠诲姟鐨勯檮浠跺垪琛�
-
// 瑙嗛棰勮鐩稿叧鐘舵��
const showVideoDialog = ref(false);
const currentVideoFile = ref(null);
-
- // 寮傚父鐘舵��
- const hasException = ref(null); // null: 鏈�夋嫨, true: 瀛樺湪寮傚父, false: 姝e父
-
- // 涓婁紶閰嶇疆
- const uploadConfig = {
- action: "/file/upload",
- limit: 10,
- fileSize: 50, // MB
- fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
- maxVideoDuration: 60, // 绉�
- };
-
- // 璁$畻涓婁紶URL
- const uploadFileUrl = computed(() => {
- const baseUrl = config.baseUrl;
-
- return baseUrl + uploadConfig.action;
- });
-
- // 璁$畻璇锋眰澶�
- const headers = computed(() => {
- const token = getToken();
- return token ? { Authorization: "Bearer " + token } : {};
- });
// 璇锋眰鍙栨秷鏍囧織锛岀敤浜庡彇娑堟鍦ㄨ繘琛岀殑璇锋眰
let isRequestCancelled = false;
@@ -489,11 +224,6 @@
onUnmounted(() => {
// 璁剧疆鍙栨秷鏍囧織锛岄樆姝㈠悗缁殑寮傛鎿嶄綔
isRequestCancelled = true;
-
- // 鍏抽棴涓婁紶寮圭獥
- if (showUploadDialog.value) {
- showUploadDialog.value = false;
- }
});
// 杩斿洖涓婁竴椤�
@@ -567,11 +297,15 @@
const getFileStatus = record => {
let _beforeProduction =
- record.beforeProduction && record.beforeProduction.length;
+ (record.commonFileListBeforeVO && record.commonFileListBeforeVO.length) ||
+ (record.commonFileListBefore && record.commonFileListBefore.length);
let _afterProduction =
- record.afterProduction && record.afterProduction.length;
+ (record.commonFileListVO && record.commonFileListVO.length) ||
+ (record.commonFileListAfter && record.commonFileListAfter.length);
let _productionIssues =
- record.productionIssues && record.productionIssues.length;
+ (record.commonFileListAfterVO && record.commonFileListAfterVO.length) ||
+ (record.commonFileListIssue && record.commonFileListIssue.length);
+
if (_beforeProduction && _afterProduction && _productionIssues) {
return 2;
} else if (_beforeProduction || _afterProduction || _productionIssues) {
@@ -653,322 +387,27 @@
}
};
- // 鎵撳紑涓婁紶寮圭獥
+ // 鎵撳紑涓婁紶椤甸潰
const openUploadDialog = task => {
- // 璁剧疆浠诲姟淇℃伅鍒癷nfoData
- if (task) {
- infoData.value = {
- ...task,
- taskId: task.taskId || task.id,
- storageBlobDTO: [], // 鍒濆鍖栨枃浠跺垪琛�
- };
- }
-
- // 璁剧疆涓婁紶鐘舵�佺被鍨嬶紙鍙互鏍规嵁浠诲姟绫诲瀷璁剧疆涓嶅悓鐨勭姸鎬侊級
- uploadStatusType.value = 0; // 榛樿鐘舵��
-
- // 娓呯┖涔嬪墠鐨勬枃浠�
- uploadFiles.value = [];
-
- // 鏄剧ず涓婁紶寮圭獥
- showUploadDialog.value = true;
- };
-
- // 鍏抽棴涓婁紶寮圭獥
- const closeUploadDialog = () => {
- showUploadDialog.value = false;
- uploadFiles.value = [];
- // 娓呯悊涓変釜鍒嗙被鐨勬暟鎹�
- beforeModelValue.value = [];
- afterModelValue.value = [];
- issueModelValue.value = [];
- currentUploadType.value = "before";
- hasException.value = null; // 閲嶇疆寮傚父鐘舵��
- infoData.value = null; // 娓呯悊浠诲姟鏁版嵁
- };
-
- // 鍒囨崲涓婁紶绫诲瀷
- const switchUploadType = type => {
- currentUploadType.value = type;
- };
-
- // 鑾峰彇褰撳墠鍒嗙被鐨勬枃浠跺垪琛�
- const getCurrentFiles = () => {
- switch (currentUploadType.value) {
- case "before":
- return beforeModelValue.value || [];
- case "after":
- return afterModelValue.value || [];
- case "issue":
- return issueModelValue.value || [];
- default:
- return [];
- }
- };
-
- // 鑾峰彇涓婁紶绫诲瀷鏂囨湰
- const getUploadTypeText = () => {
- switch (currentUploadType.value) {
- case "before":
- return "鐢熶骇鍓�";
- case "after":
- return "鐢熶骇涓�";
- case "issue":
- return "鐢熶骇鍚�";
- default:
- return "";
- }
- };
-
- // 澶勭悊涓婁紶鏂囦欢鏇存柊
- const handleUploadUpdate = files => {
- uploadFiles.value = files;
- };
-
- // 璁剧疆寮傚父鐘舵��
- const setExceptionStatus = status => {
- hasException.value = status;
- };
-
- // 璺宠浆鍒版柊澧炴姤淇〉闈�
- const goToRepair = () => {
- try {
- // 瀛樺偍褰撳墠浠诲姟淇℃伅鍒版湰鍦板瓨鍌紝渚涙姤淇〉闈娇鐢�
- const taskInfo = {
- taskId: infoData.value?.taskId || infoData.value?.id,
- taskName: infoData.value?.taskName,
- inspectionLocation: infoData.value?.inspectionLocation,
- inspector: infoData.value?.inspector,
- // 浼犻�掑綋鍓嶄笂浼犵殑鏂囦欢淇℃伅
- uploadedFiles: {
- before: beforeModelValue.value,
- after: afterModelValue.value,
- issue: issueModelValue.value,
- },
- };
-
- uni.setStorageSync("repairTaskInfo", JSON.stringify(taskInfo));
-
- // 璺宠浆鍒版柊澧炴姤淇〉闈�
- uni.navigateTo({
- url: "/pages/equipmentManagement/repair/add",
- });
-
- // 鍏抽棴涓婁紶寮圭獥
- closeUploadDialog();
- } catch (error) {
- console.error("璺宠浆鎶ヤ慨椤甸潰澶辫触:", error);
- uni.showToast({
- title: "璺宠浆澶辫触锛岃閲嶈瘯",
- icon: "error",
- });
- }
- };
-
- // 鎻愪氦涓婁紶
- const submitUpload = async () => {
- try {
- // 妫�鏌ユ槸鍚﹂�夋嫨浜嗗紓甯哥姸鎬�
- if (hasException.value === null) {
- uni.showToast({
- title: "璇烽�夋嫨鏄惁瀛樺湪寮傚父",
- icon: "none",
- });
- return;
- }
-
- // 妫�鏌ユ槸鍚︽湁浠讳綍鏂囦欢涓婁紶
- const totalFiles =
- beforeModelValue.value.length +
- afterModelValue.value.length +
- issueModelValue.value.length;
- if (totalFiles === 0) {
- uni.showToast({
- title: "璇峰厛涓婁紶鏂囦欢",
- icon: "none",
- });
- return;
- }
-
- // 鏄剧ず鎻愪氦涓殑鍔犺浇鎻愮ず
- showLoadingToast("鎻愪氦涓�...");
-
- // 鎸夌収鎮ㄧ殑閫昏緫鍚堝苟鎵�鏈夊垎绫荤殑鏂囦欢
- let arr = [];
- if (beforeModelValue.value.length > 0) {
- arr.push(...beforeModelValue.value);
- }
- if (afterModelValue.value.length > 0) {
- arr.push(...afterModelValue.value);
- }
- if (issueModelValue.value.length > 0) {
- arr.push(...issueModelValue.value);
- }
-
- // 浼犵粰鍚庣鐨勪复鏃舵枃浠禝D鍒楄〃锛坱empFileIds锛�
- // 鍏煎锛氭湁浜涙帴鍙e彲鑳借繑鍥� tempId / tempFileId / id
- let tempFileIds = [];
- if (arr !== null && arr.length > 0) {
- tempFileIds = arr
- .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
- .filter(v => v !== undefined && v !== null && v !== "");
- }
-
- // 鎻愪氦鏁版嵁
- infoData.value.storageBlobDTO = arr;
- // 娣诲姞寮傚父鐘舵�佷俊鎭�
- infoData.value.hasException = hasException.value;
- infoData.value.tempFileIds = tempFileIds;
- const result = await uploadInspectionTask({ ...infoData.value });
-
- // 妫�鏌ユ彁浜ょ粨鏋�
- if (result && (result.code === 200 || result.success)) {
- // 鎻愪氦鎴愬姛
- closeToast(); // 鍏抽棴鍔犺浇鎻愮ず
-
- uni.showToast({
- title: "鎻愪氦鎴愬姛",
- icon: "success",
- });
-
- // 鍏抽棴寮圭獥
- closeUploadDialog();
-
- // 鍒锋柊鍒楄〃
- setTimeout(() => {
- reloadPage();
- }, 500);
- } else {
- // 鎻愪氦澶辫触
- closeToast();
- uni.showToast({
- title: result?.msg || result?.message || "鎻愪氦澶辫触",
- icon: "error",
- });
- }
- } catch (error) {
- console.error("鎻愪氦涓婁紶澶辫触:", error);
- closeToast(); // 鍏抽棴鍔犺浇鎻愮ず
-
- let errorMessage = "鎻愪氦澶辫触";
- if (error.message) {
- errorMessage = error.message;
- } else if (error.msg) {
- errorMessage = error.msg;
- } else if (typeof error === "string") {
- errorMessage = error;
- }
-
- uni.showToast({
- title: errorMessage,
- icon: "error",
- });
- }
+ // 灏嗕换鍔′俊鎭紶閫掑埌涓婁紶椤甸潰
+ const taskData = encodeURIComponent(JSON.stringify(task));
+ uni.navigateTo({
+ url: `/pages/inspectionUpload/upload?taskInfo=${taskData}`,
+ });
};
// 鍥剧墖涓婁紶(鍙�夋嫨鍥剧墖涓婁紶鎴栬�呮槸鐩告満鎷嶇収)
const startUploadForTask = async (task, type) => {
- // 鐩存帴鎵撳紑涓婁紶寮圭獥
+ // 鎵撳紑涓婁紶椤甸潰
openUploadDialog(task);
};
- // 鏌ョ湅闄勪欢
+ // 鏌ョ湅闄勪欢 - 璺宠浆鍒伴檮浠堕〉闈�
const viewAttachments = async task => {
- try {
- currentViewTask.value = task;
- currentViewType.value = "before";
-
- // 瑙f瀽鏂扮殑鏁版嵁缁撴瀯
- attachmentList.value = [];
-
- // 鍚庣鍙嶆樉瀛楁锛堜綘鎻愪緵鐨勬暟鎹粨鏋勶級锛�
- // - commonFileListBefore锛氱敓浜у墠锛堥�氬父 type=10锛�
- // - commonFileListAfter锛氱敓浜т腑锛堥�氬父 type=11锛�
- // - commonFileList锛氬彲鑳芥槸鍏ㄩ儴/鍏滃簳锛堣嫢鍖呭惈鐢熶骇鍚庯紝涓�鑸� type=12锛�
- const allList = Array.isArray(task?.commonFileList)
- ? task.commonFileList
- : [];
- const beforeList = Array.isArray(task?.commonFileListBefore)
- ? task.commonFileListBefore
- : allList.filter(f => f?.type === 10);
- const afterList = Array.isArray(task?.commonFileListAfter)
- ? task.commonFileListAfter
- : allList.filter(f => f?.type === 11);
- // 濡傛灉鍚庣鍚庣画琛ヤ簡 commonFileListIssue锛屽垯浼樺厛鐢紱鍚﹀垯浠� commonFileList 閲屾寜 type=12 鍏滃簳
- const issueList = Array.isArray(task?.commonFileListIssue)
- ? task.commonFileListIssue
- : allList.filter(f => f?.type === 12);
-
- const mapToViewFile = (file, viewType) => {
- const u = normalizeFileUrl(file?.url || file?.downloadUrl || "");
- return {
- ...file,
- // 鐢ㄤ簬涓夋爣绛鹃〉鍒嗙粍锛�0=鐢熶骇鍓� 1=鐢熶骇涓� 2=鐢熶骇鍚�
- type: viewType,
- name: file?.name || file?.originalFilename || file?.bucketFilename,
- bucketFilename: file?.bucketFilename || file?.name,
- originalFilename: file?.originalFilename || file?.name,
- url: u,
- downloadUrl: u,
- size: file?.size || file?.byteSize,
- };
- };
-
- attachmentList.value.push(...beforeList.map(f => mapToViewFile(f, 0)));
- attachmentList.value.push(...afterList.map(f => mapToViewFile(f, 1)));
- attachmentList.value.push(...issueList.map(f => mapToViewFile(f, 2)));
-
- showAttachmentDialog.value = true;
- } catch (error) {
- uni.showToast({
- title: "鑾峰彇闄勪欢澶辫触",
- icon: "error",
- });
- }
- };
-
- // 鍏抽棴闄勪欢鏌ョ湅寮圭獥
- const closeAttachmentDialog = () => {
- showAttachmentDialog.value = false;
- currentViewTask.value = null;
- attachmentList.value = [];
- currentViewType.value = "before";
- };
-
- // 鍒囨崲鏌ョ湅绫诲瀷
- const switchViewType = type => {
- currentViewType.value = type;
- };
-
- // 鏍规嵁type鑾峰彇瀵瑰簲鍒嗙被鐨勯檮浠�
- const getAttachmentsByType = typeValue => {
- return attachmentList.value.filter(file => file.type === typeValue) || [];
- };
- // 鑾峰彇type鍊�
- const getTabType = () => {
- switch (currentUploadType.value) {
- case "before":
- return 10;
- case "after":
- return 11;
- case "issue":
- return 12;
- default:
- return 10;
- }
- };
- // 鑾峰彇褰撳墠鏌ョ湅绫诲瀷鐨勯檮浠�
- const getCurrentViewAttachments = () => {
- switch (currentViewType.value) {
- case "before":
- return getAttachmentsByType(0);
- case "after":
- return getAttachmentsByType(1);
- case "issue":
- return getAttachmentsByType(2);
- default:
- return [];
- }
+ const taskData = encodeURIComponent(JSON.stringify(task));
+ uni.navigateTo({
+ url: `/pages/inspectionUpload/attachment?taskInfo=${taskData}`,
+ });
};
// 鍒ゆ柇鏄惁涓哄浘鐗囨枃浠�
@@ -1058,475 +497,6 @@
title: "瑙嗛鎾斁澶辫触",
icon: "error",
});
- };
-
- // 鎷嶇収/鎷嶈棰戯紙鐪熸満浼樺厛鐢� chooseMedia锛涗笉鏀寔鍒欓檷绾э級
- const chooseMedia = type => {
- if (getCurrentFiles().length >= uploadConfig.limit) {
- uni.showToast({
- title: `鏈�澶氬彧鑳介�夋嫨${uploadConfig.limit}涓枃浠禶,
- icon: "none",
- });
- return;
- }
-
- const remaining = uploadConfig.limit - getCurrentFiles().length;
-
- // 浼樺厛锛歝hooseMedia锛堟敮鎸� image/video锛�
- if (typeof uni.chooseMedia === "function") {
- uni.chooseMedia({
- count: Math.min(remaining, 1),
- mediaType: [type || "image"],
- sizeType: ["compressed", "original"],
- sourceType: ["camera"],
- success: res => {
- try {
- const files = res?.tempFiles || [];
- if (!files.length) throw new Error("鏈幏鍙栧埌鏂囦欢");
-
- files.forEach((tf, idx) => {
- const filePath = tf.tempFilePath || tf.path || "";
- const fileType = tf.fileType || type || "image";
- const ext = fileType === "video" ? "mp4" : "jpg";
- const file = {
- tempFilePath: filePath,
- path: filePath,
- type: fileType,
- name: `${fileType}_${Date.now()}_${idx}.${ext}`,
- size: tf.size || 0,
- duration: tf.duration || 0,
- createTime: Date.now(),
- uid: Date.now() + Math.random() + idx,
- };
- handleBeforeUpload(file);
- });
- } catch (e) {
- console.error("澶勭悊鎷嶆憚缁撴灉澶辫触:", e);
- uni.showToast({ title: "澶勭悊鏂囦欢澶辫触", icon: "error" });
- }
- },
- fail: err => {
- console.error("鎷嶆憚澶辫触:", err);
- uni.showToast({ title: "鎷嶆憚澶辫触", icon: "error" });
- },
- });
- return;
- }
-
- // 闄嶇骇锛歝hooseImage / chooseVideo
- if (type === "video") {
- chooseVideo();
- } else {
- uni.chooseImage({
- count: 1,
- sizeType: ["compressed", "original"],
- sourceType: ["camera"],
- success: res => {
- const tempFilePath = res?.tempFilePaths?.[0];
- const tempFile = res?.tempFiles?.[0] || {};
- if (!tempFilePath) return;
- handleBeforeUpload({
- tempFilePath,
- path: tempFilePath,
- type: "image",
- name: `photo_${Date.now()}.jpg`,
- size: tempFile.size || 0,
- createTime: Date.now(),
- uid: Date.now() + Math.random(),
- });
- },
- });
- }
- };
-
- // 鎷嶇収
- const chooseImage = () => {
- if (uploadFiles.value.length >= uploadConfig.limit) {
- uni.showToast({
- title: `鏈�澶氬彧鑳芥媿鎽�${uploadConfig.limit}涓枃浠禶,
- icon: "none",
- });
- return;
- }
-
- uni.chooseMedia({
- count: 1,
- mediaType: ["image", "video"],
- sizeType: ["compressed", "original"],
- sourceType: ["camera"],
- success: res => {
- try {
- if (!res.tempFiles || res.tempFiles.length === 0) {
- throw new Error("鏈幏鍙栧埌鍥剧墖鏂囦欢");
- }
-
- const tempFilePath = res.tempFiles[0];
- const tempFile =
- res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {};
-
- const file = {
- tempFilePath: tempFilePath,
- path: tempFilePath, // 淇濇寔鍏煎鎬�
- type: "image",
- name: `photo_${Date.now()}.jpg`,
- size: tempFile.size || 0,
- createTime: new Date().getTime(),
- uid: Date.now() + Math.random(),
- };
-
- handleBeforeUpload(file);
- } catch (error) {
- console.error("澶勭悊鎷嶇収缁撴灉澶辫触:", error);
- uni.showToast({
- title: "澶勭悊鍥剧墖澶辫触",
- icon: "error",
- });
- }
- },
- fail: err => {
- console.error("鎷嶇収澶辫触:", err);
- uni.showToast({
- title: "鎷嶇収澶辫触: " + (err.errMsg || "鏈煡閿欒"),
- icon: "error",
- });
- },
- });
- };
-
- // 鎷嶈棰�
- const chooseVideo = () => {
- if (uploadFiles.value.length >= uploadConfig.limit) {
- uni.showToast({
- title: `鏈�澶氬彧鑳芥媿鎽�${uploadConfig.limit}涓枃浠禶,
- icon: "none",
- });
- return;
- }
-
- uni.chooseVideo({
- sourceType: ["camera"],
- maxDuration: uploadConfig.maxVideoDuration,
- camera: "back",
- success: res => {
- try {
- if (!res.tempFilePath) {
- throw new Error("鏈幏鍙栧埌瑙嗛鏂囦欢");
- }
-
- const file = {
- tempFilePath: res.tempFilePath,
- path: res.tempFilePath, // 淇濇寔鍏煎鎬�
- type: "video",
- name: `video_${Date.now()}.mp4`,
- size: res.size || 0,
- duration: res.duration || 0,
- createTime: new Date().getTime(),
- uid: Date.now() + Math.random(),
- };
-
- handleBeforeUpload(file);
- } catch (error) {
- console.error("澶勭悊鎷嶈棰戠粨鏋滃け璐�:", error);
- uni.showToast({
- title: "澶勭悊瑙嗛澶辫触",
- icon: "error",
- });
- }
- },
- fail: err => {
- console.error("鎷嶈棰戝け璐�:", err);
- uni.showToast({
- title: "鎷嶈棰戝け璐�: " + (err.errMsg || "鏈煡閿欒"),
- icon: "error",
- });
- },
- });
- };
-
- // 鍒犻櫎鏂囦欢
- const removeFile = index => {
- uni.showModal({
- title: "纭鍒犻櫎",
- content: "纭畾瑕佸垹闄よ繖涓枃浠跺悧锛�",
- success: res => {
- if (res.confirm) {
- // 鏍规嵁褰撳墠涓婁紶绫诲瀷鍒犻櫎瀵瑰簲鍒嗙被鐨勬枃浠�
- switch (currentUploadType.value) {
- case "before":
- beforeModelValue.value.splice(index, 1);
- break;
- case "after":
- afterModelValue.value.splice(index, 1);
- break;
- case "issue":
- issueModelValue.value.splice(index, 1);
- break;
- }
- uni.showToast({
- title: "鍒犻櫎鎴愬姛",
- icon: "success",
- });
- }
- },
- });
- };
-
- // 妫�鏌ョ綉缁滆繛鎺�
- const checkNetworkConnection = () => {
- return new Promise(resolve => {
- uni.getNetworkType({
- success: res => {
- if (res.networkType === "none") {
- resolve(false);
- } else {
- resolve(true);
- }
- },
- fail: () => {
- resolve(false);
- },
- });
- });
- };
-
- // 涓婁紶鍓嶆牎楠�
- const handleBeforeUpload = async file => {
- // 鏍¢獙鏂囦欢绫诲瀷
- if (
- uploadConfig.fileType &&
- Array.isArray(uploadConfig.fileType) &&
- uploadConfig.fileType.length > 0
- ) {
- const fileName = file.name || "";
- const fileExtension = fileName
- ? fileName.split(".").pop().toLowerCase()
- : "";
-
- // 鏍规嵁鏂囦欢绫诲瀷纭畾鏈熸湜鐨勬墿灞曞悕
- let expectedTypes = [];
- if (file.type === "image") {
- expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"];
- } else if (file.type === "video") {
- expectedTypes = ["mp4", "mov", "avi", "wmv"];
- }
-
- // 妫�鏌ユ枃浠舵墿灞曞悕鏄惁鍦ㄥ厑璁哥殑绫诲瀷涓�
- if (fileExtension && expectedTypes.length > 0) {
- const isAllowed = expectedTypes.some(
- type => uploadConfig.fileType.includes(type) && type === fileExtension
- );
-
- if (!isAllowed) {
- uni.showToast({
- title: `鏂囦欢鏍煎紡涓嶆敮鎸侊紝璇锋媿鎽� ${expectedTypes.join("/")} 鏍煎紡鐨勬枃浠禶,
- icon: "none",
- });
- return false;
- }
- }
- }
-
- // 鏍¢獙閫氳繃锛屽紑濮嬩笂浼�
- uploadFile(file);
- return true;
- };
-
- // 鏂囦欢涓婁紶澶勭悊锛堢湡鏈鸿蛋 uni.uploadFile锛�
- const uploadFile = async file => {
- uploading.value = true;
- uploadProgress.value = 0;
- number.value++; // 澧炲姞涓婁紶璁℃暟
-
- // 纭繚token瀛樺湪
- const token = getToken();
- if (!token) {
- handleUploadError("鐢ㄦ埛鏈櫥褰�");
- return;
- }
-
- const typeValue = getTabType(); // 鐢熶骇鍓�:10, 鐢熶骇涓�:11, 鐢熶骇鍚�:12
-
- uploadWithUniUploadFile(
- file,
- file.tempFilePath || file.path || "",
- typeValue,
- token
- );
- };
-
- // 浣跨敤uni.uploadFile涓婁紶锛堥潪H5鐜鎴朒5鍥為��鏂规锛�
- const uploadWithUniUploadFile = (file, filePath, typeValue, token) => {
- if (!filePath) {
- handleUploadError("鏂囦欢璺緞涓嶅瓨鍦�");
- return;
- }
-
- const uploadTask = uni.uploadFile({
- url: uploadFileUrl.value,
- filePath: filePath,
- name: "file",
- formData: {
- type: typeValue,
- },
- header: {
- Authorization: `Bearer ${token}`,
- },
- success: res => {
- try {
- if (res.statusCode === 200) {
- const response = JSON.parse(res.data);
- if (response.code === 200) {
- handleUploadSuccess(response, file);
- uni.showToast({
- title: "涓婁紶鎴愬姛",
- icon: "success",
- });
- } else {
- handleUploadError(response.msg || "鏈嶅姟鍣ㄨ繑鍥為敊璇�");
- }
- } else {
- handleUploadError(`鏈嶅姟鍣ㄩ敊璇紝鐘舵�佺爜: ${res.statusCode}`);
- }
- } catch (e) {
- console.error("瑙f瀽鍝嶅簲澶辫触:", e);
- console.error("鍘熷鍝嶅簲鏁版嵁:", res.data);
- handleUploadError("鍝嶅簲鏁版嵁瑙f瀽澶辫触: " + e.message);
- }
- },
- fail: err => {
- console.error("涓婁紶澶辫触:", err.errMsg || err);
- number.value--; // 涓婁紶澶辫触鏃跺噺灏戣鏁�
-
- let errorMessage = "涓婁紶澶辫触";
- if (err.errMsg) {
- if (err.errMsg.includes("statusCode: null")) {
- errorMessage = "缃戠粶杩炴帴澶辫触锛岃妫�鏌ョ綉缁滆缃�";
- } else if (err.errMsg.includes("timeout")) {
- errorMessage = "涓婁紶瓒呮椂锛岃閲嶈瘯";
- } else if (err.errMsg.includes("fail")) {
- errorMessage = "涓婁紶澶辫触锛岃妫�鏌ョ綉缁滆繛鎺�";
- } else {
- errorMessage = err.errMsg;
- }
- }
-
- handleUploadError(errorMessage);
- },
- complete: () => {
- uploading.value = false;
- uploadProgress.value = 0;
- },
- });
-
- // 鐩戝惉涓婁紶杩涘害
- if (uploadTask && uploadTask.onProgressUpdate) {
- uploadTask.onProgressUpdate(res => {
- uploadProgress.value = res.progress;
- });
- }
- };
-
- // 涓婁紶澶辫触澶勭悊
- const handleUploadError = (message = "涓婁紶鏂囦欢澶辫触", showRetry = false) => {
- uploading.value = false;
- uploadProgress.value = 0;
-
- if (showRetry) {
- uni.showModal({
- title: "涓婁紶澶辫触",
- content: message + "锛屾槸鍚﹂噸璇曪紵",
- success: res => {
- if (res.confirm) {
- // 鐢ㄦ埛閫夋嫨閲嶈瘯锛岃繖閲屽彲浠ラ噸鏂拌Е鍙戜笂浼�
- }
- },
- });
- } else {
- uni.showToast({
- title: message,
- icon: "error",
- });
- }
- };
-
- // 涓婁紶鎴愬姛鍥炶皟
- const handleUploadSuccess = (res, file) => {
- console.log("涓婁紶鎴愬姛鍝嶅簲:", res);
-
- // 澶勭悊涓嶅悓鐨勬暟鎹粨鏋勶細鍙兘鏄暟缁勶紝涔熷彲鑳芥槸鍗曚釜瀵硅薄
- let uploadedFile = null;
- uploadedFile = res.data;
-
- if (!uploadedFile) {
- console.error("鏃犳硶瑙f瀽涓婁紶鍝嶅簲鏁版嵁:", res);
- number.value--; // 涓婁紶澶辫触鏃跺噺灏戣鏁�
- handleUploadError("涓婁紶鍝嶅簲鏁版嵁鏍煎紡閿欒", false);
- return;
- }
-
- // 鏍规嵁褰撳墠涓婁紶绫诲瀷璁剧疆type瀛楁
- let typeValue = 0; // 榛樿涓虹敓浜у墠
- switch (currentUploadType.value) {
- case "before":
- typeValue = 0;
- break;
- case "after":
- typeValue = 1;
- break;
- case "issue":
- typeValue = 2;
- break;
- }
-
- // 纭繚涓婁紶鐨勬枃浠舵暟鎹畬鏁达紝鍖呭惈id鍜宼ype
- const fileData = {
- ...file,
- id: uploadedFile.id, // 娣诲姞鏈嶅姟鍣ㄨ繑鍥炵殑id
- tempId: uploadedFile.tempId ?? uploadedFile.tempFileId ?? uploadedFile.id,
- url:
- uploadedFile.url ||
- uploadedFile.downloadUrl ||
- file.tempFilePath ||
- file.path,
- bucketFilename:
- uploadedFile.bucketFilename || uploadedFile.originalFilename || file.name,
- downloadUrl: uploadedFile.downloadUrl || uploadedFile.url,
- size: uploadedFile.size || uploadedFile.byteSize || file.size,
- createTime: uploadedFile.createTime || new Date().getTime(),
- type: typeValue, // 娣诲姞绫诲瀷瀛楁锛�0=鐢熶骇鍓�, 1=鐢熶骇涓�, 2=鐢熶骇鍚�
- };
-
- uploadList.value.push(fileData);
-
- // 绔嬪嵆娣诲姞鍒板搴旂殑鍒嗙被锛屼笉绛夊緟鎵�鏈夋枃浠朵笂浼犲畬鎴�
- switch (currentUploadType.value) {
- case "before":
- beforeModelValue.value.push(fileData);
- break;
- case "after":
- afterModelValue.value.push(fileData);
- break;
- case "issue":
- issueModelValue.value.push(fileData);
- break;
- }
-
- // 閲嶇疆涓婁紶鍒楄〃锛堝洜涓哄凡缁忔坊鍔犲埌瀵瑰簲鍒嗙被浜嗭級
- uploadList.value = [];
- number.value = 0;
- };
-
- // 涓婁紶缁撴潫澶勭悊锛堝凡搴熷純锛岀幇鍦ㄥ湪handleUploadSuccess涓洿鎺ュ鐞嗭級
- const uploadedSuccessfully = () => {
- // 姝ゅ嚱鏁板凡涓嶅啀浣跨敤锛屾枃浠朵笂浼犳垚鍔熷悗绔嬪嵆娣诲姞鍒板搴斿垎绫�
- };
-
- // 鏍煎紡鍖栨枃浠跺ぇ灏�
- const formatFileSize = size => {
- if (!size) return "";
- if (size < 1024) return size + "B";
- if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB";
- return (size / (1024 * 1024)).toFixed(1) + "MB";
};
</script>
@@ -1725,416 +695,6 @@
display: flex;
align-items: center;
justify-content: center;
- }
-
- /* 涓婁紶寮圭獥鏍峰紡 */
- .upload-popup-content {
- background: #fff;
- border-radius: 12px;
- width: 100%;
- min-height: 300px;
- max-height: 70vh;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
- }
-
- .upload-popup-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 15px 20px;
- border-bottom: 1px solid #eee;
- }
-
- .upload-popup-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
- }
-
- .upload-popup-body {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- }
-
- .upload-popup-footer {
- display: flex;
- justify-content: flex-end;
- padding: 15px 20px;
- border-top: 1px solid #eee;
- gap: 10px;
- }
-
- /* 绠�鍖栦笂浼犵粍浠舵牱寮� */
- .simple-upload-area {
- padding: 15px;
- }
-
- .upload-buttons {
- display: flex;
- gap: 10px;
- margin-bottom: 15px;
- }
-
- .file-list {
- margin-top: 15px;
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- }
-
- .file-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- background: #fff;
- border-radius: 12px;
- padding: 8px;
- border: 1px solid #e9ecef;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
- transition: all 0.3s ease;
- width: calc(50% - 6px);
- min-width: 120px;
- }
-
- .file-item:hover {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- transform: translateY(-2px);
- }
-
- .file-preview-container {
- position: relative;
- margin-bottom: 8px;
- }
-
- .file-preview {
- width: 80px;
- height: 80px;
- border-radius: 8px;
- object-fit: cover;
- border: 2px solid #f0f0f0;
- }
-
- .video-preview {
- width: 80px;
- height: 80px;
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border: 2px solid #f0f0f0;
- }
-
- .video-text {
- font-size: 12px;
- color: #666;
- margin-top: 4px;
- }
-
- .delete-btn {
- position: absolute;
- top: -6px;
- right: -6px;
- width: 20px;
- height: 20px;
- background: #ff4757;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- box-shadow: 0 2px 4px rgba(255, 71, 87, 0.3);
- transition: all 0.3s ease;
- }
-
- .delete-btn:hover {
- background: #ff3742;
- transform: scale(1.1);
- }
-
- .file-info {
- text-align: center;
- width: 100%;
- }
-
- .file-name {
- font-size: 12px;
- color: #333;
- font-weight: 500;
- display: block;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100px;
- }
-
- .file-size {
- font-size: 10px;
- color: #999;
- margin-top: 2px;
- display: block;
- }
-
- .empty-state {
- text-align: center;
- padding: 40px 20px;
- color: #999;
- font-size: 14px;
- background: #f8f9fa;
- border-radius: 8px;
- border: 2px dashed #ddd;
- }
-
- .upload-progress {
- margin: 15px 0;
- padding: 0 10px;
- }
-
- /* 涓婁紶鏍囩椤垫牱寮� */
- .upload-tabs {
- display: flex;
- background: #f8f9fa;
- border-radius: 8px;
- margin-bottom: 15px;
- padding: 4px;
- }
-
- .tab-item {
- flex: 1;
- text-align: center;
- padding: 8px 12px;
- font-size: 14px;
- color: #666;
- border-radius: 6px;
- transition: all 0.3s ease;
- cursor: pointer;
- }
-
- .tab-item.active {
- background: #409eff;
- color: #fff;
- font-weight: 500;
- }
-
- .tab-item:hover:not(.active) {
- background: #e9ecef;
- color: #333;
- }
-
- /* 寮傚父鐘舵�侀�夋嫨鏍峰紡 */
- .exception-section {
- margin-bottom: 20px;
- padding: 15px;
- background: #f8f9fa;
- border-radius: 8px;
- border: 1px solid #e9ecef;
- }
-
- .section-title {
- display: block;
- font-size: 14px;
- font-weight: 600;
- color: #333;
- margin-bottom: 12px;
- }
-
- .exception-options {
- display: flex;
- gap: 12px;
- }
-
- .exception-option {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 12px 16px;
- background: #fff;
- border: 2px solid #e9ecef;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.3s ease;
- font-size: 14px;
- color: #666;
- }
-
- .exception-option.active {
- border-color: #409eff;
- background: #f0f8ff;
- color: #409eff;
- font-weight: 500;
- }
-
- .exception-option:hover:not(.active) {
- border-color: #d9d9d9;
- background: #fafafa;
- }
-
- /* 缁熻淇℃伅鏍峰紡 */
- .upload-summary {
- margin-top: 15px;
- padding: 10px;
- background: #f8f9fa;
- border-radius: 6px;
- border-left: 3px solid #409eff;
- }
-
- .summary-text {
- font-size: 12px;
- color: #666;
- line-height: 1.4;
- }
-
- /* 鏌ョ湅闄勪欢寮圭獥鏍峰紡 */
- .attachment-popup-content {
- background: #fff;
- border-radius: 12px;
- width: 100%;
- min-height: 400px;
- max-height: 70vh;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
- }
-
- .attachment-popup-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 15px 20px;
- border-bottom: 1px solid #eee;
- background: #f8f9fa;
- }
-
- .attachment-popup-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
- }
-
- .close-btn-attachment {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- background: #f5f5f5;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: all 0.3s ease;
- }
-
- .close-btn-attachment:hover {
- background: #e9ecef;
- transform: scale(1.1);
- }
-
- .attachment-popup-body {
- flex: 1;
- padding: 15px 20px;
- overflow-y: auto;
- }
-
- .attachment-tabs {
- display: flex;
- background: #f8f9fa;
- border-radius: 8px;
- margin-bottom: 15px;
- padding: 4px;
- }
-
- .attachment-content {
- min-height: 200px;
- }
-
- .attachment-list {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- }
-
- .attachment-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- background: #fff;
- border-radius: 12px;
- padding: 8px;
- border: 1px solid #e9ecef;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
- transition: all 0.3s ease;
- width: calc(33.33% - 8px);
- min-width: 100px;
- cursor: pointer;
- }
-
- .attachment-item:hover {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- transform: translateY(-2px);
- }
-
- .attachment-preview-container {
- margin-bottom: 8px;
- }
-
- .attachment-preview {
- width: 80px;
- height: 80px;
- border-radius: 8px;
- object-fit: cover;
- border: 2px solid #f0f0f0;
- }
-
- .attachment-video-preview {
- width: 80px;
- height: 80px;
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
- border-radius: 8px;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- border: 2px solid #f0f0f0;
- }
-
- .attachment-info {
- text-align: center;
- width: 100%;
- }
-
- .attachment-name {
- font-size: 12px;
- color: #333;
- font-weight: 500;
- display: block;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 80px;
- }
-
- .attachment-size {
- font-size: 10px;
- color: #999;
- margin-top: 2px;
- display: block;
- }
-
- .attachment-empty {
- text-align: center;
- padding: 60px 20px;
- color: #999;
- font-size: 14px;
- background: #f8f9fa;
- border-radius: 8px;
- border: 2px dashed #ddd;
}
/* 瑙嗛棰勮寮圭獥鏍峰紡 */
diff --git a/src/pages/inspectionUpload/upload.vue b/src/pages/inspectionUpload/upload.vue
new file mode 100644
index 0000000..27ba41f
--- /dev/null
+++ b/src/pages/inspectionUpload/upload.vue
@@ -0,0 +1,982 @@
+<template>
+ <view class="inspection-upload-page">
+ <!-- 椤甸潰澶撮儴 -->
+ <PageHeader title="涓婁紶宸℃璁板綍"
+ @back="goBack" />
+ <!-- 椤甸潰鍐呭 -->
+ <view class="upload-content">
+ <!-- 浠诲姟淇℃伅鍗$墖 -->
+ <view class="task-info-card"
+ v-if="taskInfo">
+ <view class="task-info-header">
+ <text class="task-name">{{ taskInfo.taskName }}</text>
+ </view>
+ <view class="task-info-body">
+ <view class="info-item">
+ <text class="info-label">浠诲姟ID</text>
+ <text class="info-value">{{ taskInfo.taskId || taskInfo.id }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">宸℃浣嶇疆</text>
+ <text class="info-value">{{ taskInfo.inspectionLocation || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鎵ц浜�</text>
+ <text class="info-value">{{ taskInfo.inspector || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ <!-- 寮傚父鐘舵�侀�夋嫨 -->
+ <view class="section-card">
+ <view class="section-title">宸℃鐘舵��</view>
+ <view class="exception-options">
+ <view class="exception-option"
+ :class="{ active: hasException === false }"
+ @click="setExceptionStatus(false)">
+ <u-icon name="checkmark-circle"
+ size="20"
+ color="#52c41a"></u-icon>
+ <text class="option-text">姝e父</text>
+ </view>
+ <view class="exception-option"
+ :class="{ active: hasException === true }"
+ @click="setExceptionStatus(true)">
+ <u-icon name="close-circle"
+ size="20"
+ color="#ff4d4f"></u-icon>
+ <text class="option-text">瀛樺湪寮傚父</text>
+ </view>
+ </view>
+ </view>
+ <!-- 寮傚父鎻忚堪锛堜粎鍦ㄥ紓甯告椂鏄剧ず锛� -->
+ <view class="section-card"
+ v-if="hasException === true">
+ <view class="section-title">寮傚父鎻忚堪</view>
+ <textarea v-model="abnormalDescription"
+ class="exception-textarea"
+ maxlength="500"
+ placeholder="璇锋弿杩板紓甯告儏鍐�..." />
+ </view>
+ <!-- 鍒嗙被鏍囩椤碉紙浠呭湪寮傚父鏃舵樉绀猴級 -->
+ <view class="section-card"
+ v-if="hasException === true">
+ <view class="upload-tabs">
+ <view class="tab-item"
+ :class="{ active: currentUploadType === 'before' }"
+ @click="switchUploadType('before')">
+ 鐢熶骇鍓�
+ </view>
+ <view class="tab-item"
+ :class="{ active: currentUploadType === 'after' }"
+ @click="switchUploadType('after')">
+ 鐢熶骇涓�
+ </view>
+ <view class="tab-item"
+ :class="{ active: currentUploadType === 'issue' }"
+ @click="switchUploadType('issue')">
+ 鐢熶骇鍚�
+ </view>
+ </view>
+ <!-- 褰撳墠鍒嗙被鐨勪笂浼犲尯鍩� -->
+ <view class="upload-area">
+ <view class="upload-buttons">
+ <u-button type="primary"
+ @click="chooseMedia('image')"
+ :loading="uploading"
+ :disabled="getCurrentFiles().length >= uploadConfig.limit"
+ :customStyle="{ marginRight: '10px', flex: 1 }">
+ <u-icon name="camera"
+ size="18"
+ color="#fff"
+ style="margin-right: 5px"></u-icon>
+ {{ uploading ? '涓婁紶涓�...' : '鎷嶇収' }}
+ </u-button>
+ <u-button type="success"
+ @click="chooseMedia('video')"
+ :loading="uploading"
+ :disabled="getCurrentFiles().length >= uploadConfig.limit"
+ :customStyle="{ flex: 1 }">
+ <uni-icons type="videocam"
+ size="18"
+ color="#fff"
+ style="margin-right: 5px"></uni-icons>
+ {{ uploading ? '涓婁紶涓�...' : '鎷嶈棰�' }}
+ </u-button>
+ </view>
+ <!-- 涓婁紶杩涘害 -->
+ <view v-if="uploading"
+ class="upload-progress">
+ <u-line-progress :percentage="uploadProgress"
+ :showText="true"
+ activeColor="#409eff"></u-line-progress>
+ </view>
+ <!-- 褰撳墠鍒嗙被鐨勬枃浠跺垪琛� -->
+ <view v-if="getCurrentFiles().length > 0"
+ class="file-list">
+ <view v-for="(file, index) in getCurrentFiles()"
+ :key="index"
+ class="file-item">
+ <view class="file-preview-container">
+ <image v-if="file.type === 'image' || (file.type !== 'video' && !file.type)"
+ :src="file.url || file.tempFilePath || file.path || file.downloadUrl"
+ class="file-preview"
+ mode="aspectFill" />
+ <view v-else-if="file.type === 'video'"
+ class="video-preview">
+ <uni-icons type="videocam"
+ size="18"
+ color="#fff"
+ style="margin-right: 5px"></uni-icons>
+ <text class="video-text">瑙嗛</text>
+ </view>
+ <!-- 鍒犻櫎鎸夐挳 -->
+ <view class="delete-btn"
+ @click="removeFile(index)">
+ <u-icon name="close"
+ size="12"
+ color="#fff"></u-icon>
+ </view>
+ </view>
+ <view class="file-info">
+ <text class="file-name">{{ file.bucketFilename || file.name || (file.type === 'image' ? '鍥剧墖' : '瑙嗛') }}</text>
+ <text class="file-size">{{ formatFileSize(file.size) }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-if="getCurrentFiles().length === 0"
+ class="empty-state">
+ <text>璇烽�夋嫨瑕佷笂浼犵殑{{ getUploadTypeText() }}鍥剧墖鎴栬棰�</text>
+ </view>
+ </view>
+ <!-- 缁熻淇℃伅 -->
+ <view class="upload-summary">
+ <text class="summary-text">
+ 鐢熶骇鍓�: {{ beforeModelValue.length }}涓枃浠� |
+ 鐢熶骇涓�: {{ afterModelValue.length }}涓枃浠� |
+ 鐢熶骇鍚�: {{ issueModelValue.length }}涓枃浠�
+ </text>
+ </view>
+ </view>
+ <!-- 姝e父鐘舵�佹彁绀� -->
+ <view class="normal-tip-card"
+ v-if="hasException === false">
+ <u-icon name="info-circle"
+ size="60"
+ color="#52c41a"></u-icon>
+ <text class="tip-text">璁惧杩愯姝e父锛屾棤闇�涓婁紶鐓х墖</text>
+ </view>
+ </view>
+ <!-- 搴曢儴鎸夐挳 -->
+ <view class="footer-buttons">
+ <u-button @click="goBack"
+ :customStyle="{ marginRight: '10px' }">鍙栨秷</u-button>
+ <u-button v-if="hasException === true"
+ type="warning"
+ @click="goToRepair"
+ :customStyle="{ marginRight: '10px' }">
+ 鏂板鎶ヤ慨
+ </u-button>
+ <u-button type="primary"
+ @click="submitUpload">鎻愪氦</u-button>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, computed, onMounted } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { uploadInspectionTask } from "@/api/inspectionManagement";
+ import { getToken } from "@/utils/auth";
+ import config from "@/config";
+
+ // 浠诲姟淇℃伅
+ const taskInfo = ref(null);
+
+ // 涓婁紶鐩稿叧鐘舵��
+ const uploading = ref(false);
+ const uploadProgress = ref(0);
+
+ // 涓変釜鍒嗙被鐨勪笂浼犵姸鎬�
+ const beforeModelValue = ref([]); // 鐢熶骇鍓�
+ const afterModelValue = ref([]); // 鐢熶骇涓�
+ const issueModelValue = ref([]); // 鐢熶骇鍚�
+
+ // 褰撳墠婵�娲荤殑涓婁紶绫诲瀷
+ const currentUploadType = ref("before"); // 'before', 'after', 'issue'
+
+ // 寮傚父鐘舵��
+ const hasException = ref(null); // null: 鏈�夋嫨, true: 瀛樺湪寮傚父, false: 姝e父
+ // 寮傚父鎻忚堪
+ const abnormalDescription = ref("");
+
+ // 涓婁紶閰嶇疆
+ const uploadConfig = {
+ action: "/common/upload",
+ limit: 10,
+ fileSize: 50, // MB
+ fileType: ["jpg", "jpeg", "png", "mp4", "mov"],
+ maxVideoDuration: 60, // 绉�
+ };
+
+ // 璁$畻涓婁紶URL
+ const uploadFileUrl = computed(() => {
+ const baseUrl = config.baseUrl;
+ return baseUrl + uploadConfig.action;
+ });
+
+ // 椤甸潰鍔犺浇
+ onLoad(options => {
+ if (options.taskInfo) {
+ try {
+ const info = JSON.parse(decodeURIComponent(options.taskInfo));
+ taskInfo.value = info;
+
+ // 鍥炴樉閫昏緫锛氫粠 taskInfo 涓仮澶嶅凡涓婁紶鐨勬枃浠�
+ const mapFiles = list => {
+ if (!list || !Array.isArray(list)) return [];
+ return list.map(item => {
+ // 澶勭悊 URL锛屽幓闄ゅ彲鑳界殑绌烘牸
+ const finalUrl = (item.url || item.previewURL || "").trim();
+ // 鑷姩鎺ㄦ柇鏂囦欢绫诲瀷
+ let fileType = item.type;
+ if (!fileType && item.contentType) {
+ fileType = item.contentType.startsWith("video") ? "video" : "image";
+ } else if (!fileType) {
+ fileType = "image"; // 榛樿鍥剧墖
+ }
+
+ return {
+ ...item,
+ url: finalUrl,
+ name: item.name || item.originalFilename,
+ tempId: item.tempId || item.id || item.tempFileId,
+ size: item.size || item.byteSize || 0, // 鏄犲皠澶у皬瀛楁
+ type: fileType,
+ status: "success",
+ };
+ });
+ };
+
+ // 淇瀛楁鏄犲皠锛欱eforeVO(鐢熶骇鍓�), VO(鐢熶骇涓�), AfterVO(鐢熶骇鍚�)
+ if (
+ info.commonFileListBeforeVO &&
+ Array.isArray(info.commonFileListBeforeVO)
+ ) {
+ beforeModelValue.value = mapFiles(info.commonFileListBeforeVO);
+ }
+ console.log(beforeModelValue.value, "beforeModelValue");
+
+ if (info.commonFileListVO && Array.isArray(info.commonFileListVO)) {
+ afterModelValue.value = mapFiles(info.commonFileListVO);
+ }
+ if (
+ info.commonFileListAfterVO &&
+ Array.isArray(info.commonFileListAfterVO)
+ ) {
+ issueModelValue.value = mapFiles(info.commonFileListAfterVO);
+ }
+
+ // 濡傛灉鏈夊紓甯告弿杩帮紝涔熸仮澶�
+ if (info.abnormalDescription) {
+ abnormalDescription.value = info.abnormalDescription;
+ }
+ // 濡傛灉鏈夊紓甯哥姸鎬侊紝涔熸仮澶�
+ if (info.hasException !== undefined && info.hasException !== null) {
+ hasException.value = info.hasException;
+ } else if (
+ info.inspectionResult !== undefined &&
+ info.inspectionResult !== null
+ ) {
+ // 0-寮傚父锛�1-姝e父
+ hasException.value = String(info.inspectionResult) === "0";
+ }
+
+ // 鑷姩鍏滃簳锛氬鏋滃瓨鍦ㄥ凡涓婁紶鏂囦欢锛屽垯蹇呯劧鏄紓甯哥姸鎬侊紝纭繚 UI 姝e父鏄剧ず
+ if (
+ !hasException.value &&
+ (beforeModelValue.value.length > 0 ||
+ afterModelValue.value.length > 0 ||
+ issueModelValue.value.length > 0)
+ ) {
+ hasException.value = true;
+ }
+ } catch (e) {
+ console.error("瑙f瀽浠诲姟淇℃伅澶辫触:", e);
+ }
+ }
+ });
+
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 鍒囨崲涓婁紶绫诲瀷
+ const switchUploadType = type => {
+ currentUploadType.value = type;
+ };
+
+ // 鑾峰彇褰撳墠鍒嗙被鐨勬枃浠跺垪琛�
+ const getCurrentFiles = () => {
+ switch (currentUploadType.value) {
+ case "before":
+ return beforeModelValue.value || [];
+ case "after":
+ return afterModelValue.value || [];
+ case "issue":
+ return issueModelValue.value || [];
+ default:
+ return [];
+ }
+ };
+
+ // 鑾峰彇涓婁紶绫诲瀷鏂囨湰
+ const getUploadTypeText = () => {
+ switch (currentUploadType.value) {
+ case "before":
+ return "鐢熶骇鍓�";
+ case "after":
+ return "鐢熶骇涓�";
+ case "issue":
+ return "鐢熶骇鍚�";
+ default:
+ return "";
+ }
+ };
+
+ // 璁剧疆寮傚父鐘舵��
+ const setExceptionStatus = status => {
+ hasException.value = status;
+ };
+
+ // 璺宠浆鍒版柊澧炴姤淇〉闈�
+ const goToRepair = () => {
+ try {
+ const taskData = {
+ taskId: taskInfo.value?.taskId || taskInfo.value?.id,
+ taskName: taskInfo.value?.taskName,
+ inspectionLocation: taskInfo.value?.inspectionLocation,
+ inspector: taskInfo.value?.inspector,
+ hasException: hasException.value,
+ inspectionResult: hasException.value ? 0 : 1, // 0-寮傚父锛�1-姝e父
+ commonFileListBeforeDTO: beforeModelValue.value,
+ commonFileListDTO: afterModelValue.value,
+ commonFileListAfterDTO: issueModelValue.value,
+ uploadedFiles: {
+ before: beforeModelValue.value,
+ after: afterModelValue.value,
+ issue: issueModelValue.value,
+ },
+ };
+
+ uni.setStorageSync("repairTaskInfo", JSON.stringify(taskData));
+
+ uni.navigateTo({
+ url: "/pages/equipmentManagement/repair/add",
+ });
+ } catch (error) {
+ console.error("璺宠浆鎶ヤ慨椤甸潰澶辫触:", error);
+ uni.showToast({
+ title: "璺宠浆澶辫触锛岃閲嶈瘯",
+ icon: "error",
+ });
+ }
+ };
+
+ // 鎻愪氦涓婁紶
+ const submitUpload = async () => {
+ try {
+ // 妫�鏌ユ槸鍚﹂�夋嫨浜嗗紓甯哥姸鎬�
+ if (hasException.value === null) {
+ uni.showToast({
+ title: "璇烽�夋嫨宸℃鐘舵��",
+ icon: "none",
+ });
+ return;
+ }
+
+ // 濡傛灉鏄紓甯哥姸鎬侊紝妫�鏌ユ槸鍚︽湁涓婁紶鏂囦欢鍜屾弿杩�
+ if (hasException.value === true) {
+ const totalFiles =
+ beforeModelValue.value.length +
+ afterModelValue.value.length +
+ issueModelValue.value.length;
+ if (totalFiles === 0) {
+ uni.showToast({
+ title: "璇蜂笂浼犲紓甯哥収鐗�",
+ icon: "none",
+ });
+ return;
+ }
+ // 妫�鏌ユ槸鍚﹀~鍐欎簡寮傚父鎻忚堪
+ if (!abnormalDescription.value.trim()) {
+ uni.showToast({
+ title: "璇峰~鍐欏紓甯告弿杩�",
+ icon: "none",
+ });
+ return;
+ }
+ }
+
+ // 鏄剧ず鎻愪氦涓殑鍔犺浇鎻愮ず
+ uni.showLoading({
+ title: "鎻愪氦涓�...",
+ mask: true,
+ });
+
+ // 鎸夌収閫昏緫鍚堝苟鎵�鏈夊垎绫荤殑鏂囦欢鐢ㄤ簬鎻愬彇ID
+ const allFiles = [
+ ...beforeModelValue.value,
+ ...afterModelValue.value,
+ ...issueModelValue.value,
+ ];
+
+ // 浼犵粰鍚庣鐨勪复鏃舵枃浠禝D鍒楄〃
+ let tempFileIds = [];
+ if (allFiles.length > 0) {
+ tempFileIds = allFiles
+ .map(item => item?.tempId ?? item?.tempFileId ?? item?.id)
+ .filter(v => v !== undefined && v !== null && v !== "");
+ }
+
+ // 鎻愪氦鏁版嵁
+ const submitData = {
+ ...taskInfo.value,
+ commonFileListBeforeDTO: beforeModelValue.value, // 鐢熶骇鍓�
+ commonFileListDTO: afterModelValue.value, // 鐢熶骇涓�
+ commonFileListAfterDTO: issueModelValue.value, // 鐢熶骇鍚�
+ hasException: hasException.value,
+ inspectionResult: hasException.value ? 0 : 1, // 0-寮傚父锛�1-姝e父
+ abnormalDescription: abnormalDescription.value,
+ tempFileIds: tempFileIds,
+ };
+
+ const result = await uploadInspectionTask(submitData);
+
+ // 妫�鏌ユ彁浜ょ粨鏋�
+ if (result && (result.code === 200 || result.success)) {
+ uni.hideLoading();
+ uni.showToast({
+ title: "鎻愪氦鎴愬姛",
+ icon: "success",
+ });
+
+ // 杩斿洖鍒楄〃椤靛苟鍒锋柊
+ setTimeout(() => {
+ uni.navigateBack();
+ }, 500);
+ } else {
+ uni.hideLoading();
+ uni.showToast({
+ title: result?.msg || result?.message || "鎻愪氦澶辫触",
+ icon: "error",
+ });
+ }
+ } catch (error) {
+ console.error("鎻愪氦涓婁紶澶辫触:", error);
+ uni.hideLoading();
+ uni.showToast({
+ title: error?.message || "鎻愪氦澶辫触",
+ icon: "error",
+ });
+ }
+ };
+
+ // 鏍煎紡鍖栨枃浠跺ぇ灏�
+ const formatFileSize = size => {
+ if (!size) return "0 B";
+ const units = ["B", "KB", "MB", "GB"];
+ let index = 0;
+ let fileSize = size;
+ while (fileSize >= 1024 && index < units.length - 1) {
+ fileSize /= 1024;
+ index++;
+ }
+ return `${fileSize.toFixed(2)} ${units[index]}`;
+ };
+
+ // 鎷嶇収/鎷嶈棰�
+ const chooseMedia = type => {
+ if (getCurrentFiles().length >= uploadConfig.limit) {
+ uni.showToast({
+ title: `鏈�澶氬彧鑳介�夋嫨${uploadConfig.limit}涓枃浠禶,
+ icon: "none",
+ });
+ return;
+ }
+
+ const remaining = uploadConfig.limit - getCurrentFiles().length;
+
+ // 浼樺厛浣跨敤 chooseMedia
+ if (typeof uni.chooseMedia === "function") {
+ uni.chooseMedia({
+ count: Math.min(remaining, 1),
+ mediaType: [type || "image"],
+ sizeType: ["compressed", "original"],
+ sourceType: ["camera"],
+ success: res => {
+ try {
+ const files = res?.tempFiles || [];
+ if (!files.length) throw new Error("鏈幏鍙栧埌鏂囦欢");
+
+ files.forEach((tf, idx) => {
+ const filePath = tf.tempFilePath || tf.path || "";
+ const fileType = tf.fileType || type || "image";
+ const ext = fileType === "video" ? "mp4" : "jpg";
+ const file = {
+ tempFilePath: filePath,
+ path: filePath,
+ type: fileType,
+ name: `${fileType}_${Date.now()}_${idx}.${ext}`,
+ size: tf.size || 0,
+ duration: tf.duration || 0,
+ createTime: Date.now(),
+ };
+ uploadFile(file);
+ });
+ } catch (err) {
+ uni.showToast({ title: err.message || "澶勭悊鏂囦欢澶辫触", icon: "none" });
+ }
+ },
+ fail: err => {
+ console.error("閫夋嫨濯掍綋澶辫触:", err);
+ uni.showToast({ title: "閫夋嫨澶辫触", icon: "none" });
+ },
+ });
+ } else {
+ // 闄嶇骇鏂规
+ if (type === "video") {
+ uni.chooseVideo({
+ sourceType: ["camera"],
+ success: res => {
+ const file = {
+ tempFilePath: res.tempFilePath,
+ path: res.tempFilePath,
+ type: "video",
+ name: `video_${Date.now()}.mp4`,
+ size: res.size || 0,
+ duration: res.duration || 0,
+ createTime: Date.now(),
+ };
+ uploadFile(file);
+ },
+ fail: () => {
+ uni.showToast({ title: "閫夋嫨瑙嗛澶辫触", icon: "none" });
+ },
+ });
+ } else {
+ uni.chooseImage({
+ count: Math.min(remaining, 9),
+ sizeType: ["compressed"],
+ sourceType: ["camera"],
+ success: res => {
+ const list = res.tempFilePaths || res.tempFiles || [];
+ list.forEach((src, idx) => {
+ const path = typeof src === "string" ? src : src.path;
+ const file = {
+ tempFilePath: path,
+ path: path,
+ type: "image",
+ name: `image_${Date.now()}_${idx}.jpg`,
+ size: 0,
+ createTime: Date.now(),
+ };
+ uploadFile(file);
+ });
+ },
+ fail: () => {
+ uni.showToast({ title: "閫夋嫨鍥剧墖澶辫触", icon: "none" });
+ },
+ });
+ }
+ }
+ };
+
+ // 涓婁紶鍗曚釜鏂囦欢
+ const uploadFile = file => {
+ const token = getToken();
+ if (!token) {
+ uni.showToast({ title: "鐢ㄦ埛鏈櫥褰�", icon: "none" });
+ return;
+ }
+
+ uploading.value = true;
+ uploadProgress.value = 0;
+
+ const uploadTask = uni.uploadFile({
+ url: uploadFileUrl.value,
+ filePath: file.tempFilePath,
+ name: "files",
+ header: {
+ Authorization: `Bearer ${token}`,
+ },
+ formData: {
+ type: getTabType(),
+ },
+ success: res => {
+ try {
+ const data = JSON.parse(res.data);
+ if (data.code === 200) {
+ // 鍏煎 CommonUpload.vue 鐨勫鐞嗛�昏緫
+ const resultData = Array.isArray(data.data)
+ ? data.data[0]
+ : data.data;
+
+ // 澶勭悊 url 鍜� name 璧嬪��
+ const finalUrl = resultData.url || resultData.previewURL;
+ const finalName = resultData.name || resultData.originalFilename;
+ const finalId =
+ resultData.tempId || resultData.id || resultData.tempFileId;
+
+ const uploadedFile = {
+ ...file,
+ ...resultData, // 鍖呭惈鍚庣杩斿洖鐨勬墍鏈夊瓧娈�
+ url: finalUrl,
+ name: finalName,
+ tempId: finalId,
+ status: "success",
+ };
+
+ // 鏍规嵁褰撳墠绫诲瀷娣诲姞鍒板搴旀暟缁�
+ if (currentUploadType.value === "before") {
+ beforeModelValue.value.push(uploadedFile);
+ } else if (currentUploadType.value === "after") {
+ afterModelValue.value.push(uploadedFile);
+ } else if (currentUploadType.value === "issue") {
+ issueModelValue.value.push(uploadedFile);
+ }
+
+ uni.showToast({ title: "涓婁紶鎴愬姛", icon: "success" });
+ } else {
+ uni.showToast({ title: data.msg || "涓婁紶澶辫触", icon: "none" });
+ }
+ } catch (e) {
+ uni.showToast({ title: "瑙f瀽鍝嶅簲澶辫触", icon: "none" });
+ }
+ },
+ fail: err => {
+ console.error("涓婁紶澶辫触:", err);
+ uni.showToast({ title: "涓婁紶澶辫触", icon: "none" });
+ },
+ complete: () => {
+ uploading.value = false;
+ },
+ });
+
+ // 鐩戝惉涓婁紶杩涘害
+ uploadTask.onProgressUpdate(res => {
+ uploadProgress.value = res.progress;
+ });
+ };
+
+ // 鑾峰彇type鍊�
+ const getTabType = () => {
+ switch (currentUploadType.value) {
+ case "before":
+ return 10;
+ case "after":
+ return 11;
+ case "issue":
+ return 12;
+ default:
+ return 10;
+ }
+ };
+
+ // 鍒犻櫎鏂囦欢
+ const removeFile = index => {
+ const files = getCurrentFiles();
+ files.splice(index, 1);
+ };
+</script>
+
+<style scoped>
+ .inspection-upload-page {
+ min-height: 100vh;
+ background-color: #f5f5f5;
+ padding-bottom: 80px;
+ }
+
+ .upload-content {
+ padding: 15px;
+ }
+
+ /* 浠诲姟淇℃伅鍗$墖 */
+ .task-info-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 15px;
+ margin-bottom: 15px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .task-info-header {
+ margin-bottom: 12px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .task-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .task-info-body {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .info-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .info-label {
+ font-size: 13px;
+ color: #999;
+ }
+
+ .info-value {
+ font-size: 13px;
+ color: #666;
+ }
+
+ /* 閫氱敤鍗$墖鏍峰紡 */
+ .section-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 15px;
+ margin-bottom: 15px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .section-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 12px;
+ }
+
+ /* 寮傚父鐘舵�侀�夋嫨 */
+ .exception-options {
+ display: flex;
+ gap: 12px;
+ }
+
+ .exception-option {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 14px 16px;
+ background: #f8f9fa;
+ border: 2px solid #e9ecef;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ }
+
+ .exception-option.active {
+ border-color: #409eff;
+ background: #f0f8ff;
+ }
+
+ .option-text {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ }
+
+ /* 寮傚父鎻忚堪 */
+ .exception-textarea {
+ width: 100%;
+ min-height: 100px;
+ padding: 12px;
+ background: #f8f9fa;
+ border: 1px solid #e9ecef;
+ border-radius: 8px;
+ font-size: 14px;
+ color: #333;
+ resize: none;
+ box-sizing: border-box;
+ }
+
+ .exception-textarea:focus {
+ outline: none;
+ border-color: #409eff;
+ background: #fff;
+ }
+
+ /* 鍒嗙被鏍囩椤� */
+ .upload-tabs {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+ }
+
+ .tab-item {
+ flex: 1;
+ padding: 10px;
+ text-align: center;
+ background: #f5f5f5;
+ border-radius: 6px;
+ font-size: 13px;
+ color: #666;
+ cursor: pointer;
+ transition: all 0.3s;
+ }
+
+ .tab-item.active {
+ background: #409eff;
+ color: #fff;
+ }
+
+ /* 涓婁紶鍖哄煙 */
+ .upload-area {
+ padding: 10px 0;
+ }
+
+ .upload-buttons {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+ }
+
+ .upload-progress {
+ margin-bottom: 15px;
+ }
+
+ /* 鏂囦欢鍒楄〃 */
+ .file-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ }
+
+ .file-item {
+ width: calc(33.33% - 7px);
+ }
+
+ .file-preview-container {
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1;
+ border-radius: 8px;
+ overflow: hidden;
+ background: #f5f5f5;
+ }
+
+ .file-preview {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ .video-preview {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: #333;
+ }
+
+ .video-text {
+ font-size: 12px;
+ color: #fff;
+ margin-top: 5px;
+ }
+
+ .delete-btn {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ width: 22px;
+ height: 22px;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .file-info {
+ margin-top: 5px;
+ }
+
+ .file-name {
+ display: block;
+ font-size: 11px;
+ color: #666;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .file-size {
+ display: block;
+ font-size: 10px;
+ color: #999;
+ margin-top: 2px;
+ }
+
+ .empty-state {
+ text-align: center;
+ padding: 30px;
+ color: #999;
+ font-size: 13px;
+ }
+
+ /* 缁熻淇℃伅 */
+ .upload-summary {
+ margin-top: 15px;
+ padding: 10px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ border-left: 3px solid #409eff;
+ }
+
+ .summary-text {
+ font-size: 12px;
+ color: #666;
+ }
+
+ /* 姝e父鐘舵�佹彁绀� */
+ .normal-tip-card {
+ background: #f6ffed;
+ border: 2px dashed #b7eb8f;
+ border-radius: 12px;
+ padding: 50px 20px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 15px;
+ }
+
+ .normal-tip-card .tip-text {
+ margin-top: 15px;
+ font-size: 16px;
+ color: #52c41a;
+ font-weight: 500;
+ }
+
+ /* 搴曢儴鎸夐挳 */
+ .footer-buttons {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ padding: 15px;
+ background: #fff;
+ border-top: 1px solid #f0f0f0;
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
+ }
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/Qualified.vue b/src/pages/inventoryManagement/stockManagement/Qualified.vue
deleted file mode 100644
index 12b9060..0000000
--- a/src/pages/inventoryManagement/stockManagement/Qualified.vue
+++ /dev/null
@@ -1,151 +0,0 @@
-<template>
- <view class="qualified-record-container">
- <view class="search-section">
- <view class="search-bar">
- <view class="search-input">
- <up-input
- class="search-text"
- placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
- v-model="searchForm.productName"
- @confirm="handleQuery"
- clearable
- />
- </view>
- <view class="filter-button" @click="handleQuery">
- <up-icon name="search" size="24" color="#999"></up-icon>
- </view>
- </view>
- </view>
-
- <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
- <view v-for="item in tableData" :key="item.id" class="ledger-item" :class="{ 'low-stock': isLowStock(item) }">
- <view class="item-header">
- <view class="item-left">
- <view class="document-icon">
- <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
- </view>
- <text class="item-id">{{ item.productName }}</text>
- </view>
- <view class="item-right">
- <text class="item-tag tag-type">鍚堟牸搴撳瓨</text>
- </view>
- </view>
-
- <up-divider></up-divider>
-
- <view class="item-details">
- <view class="detail-row">
- <text class="detail-label">瑙勬牸鍨嬪彿</text>
- <text class="detail-value">{{ item.model }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">搴撳瓨鏁伴噺</text>
- <text class="detail-value">{{ item.qualitity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">閿佸畾/鍐荤粨</text>
- <text class="detail-value">{{ item.lockedQuantity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鍙敤搴撳瓨</text>
- <text class="detail-value" style="color: #2979ff; font-weight: bold;">{{ item.unLockedQuantity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">搴撳瓨棰勮</text>
- <text class="detail-value">{{ item.warnNum }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鏇存柊鏃堕棿</text>
- <text class="detail-value">{{ item.updateTime }}</text>
- </view>
- </view>
- </view>
- <up-loadmore :status="loadStatus" />
- </scroll-view>
- <view v-else-if="!loading" class="no-data">
- <up-empty mode="data" text="鏆傛棤搴撳瓨鏁版嵁"></up-empty>
- </view>
- </view>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted } from 'vue';
-import { getStockInventoryListPage } from "@/api/inventoryManagement/stockInventory.js";
-
-const tableData = ref([]);
-const loading = ref(false);
-const loadStatus = ref('loadmore');
-const page = reactive({ current: 1, size: 10 });
-const total = ref(0);
-const searchForm = reactive({ productName: '' });
-
-const handleQuery = () => {
- page.current = 1;
- tableData.value = [];
- getList();
-};
-
-const getList = () => {
- if (loading.value) return;
- loading.value = true;
- loadStatus.value = 'loading';
- getStockInventoryListPage({ ...searchForm, ...page, type: 'qualified' }).then(res => {
- loading.value = false;
- const records = res.data.records || [];
- tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
- total.value = res.data.total;
- loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
- }).catch(() => {
- loading.value = false;
- loadStatus.value = 'loadmore';
- });
-};
-
-const loadMore = () => {
- if (loadStatus.value === 'nomore' || loading.value) return;
- page.current++;
- getList();
-};
-
-const isLowStock = (row) => {
- const stock = Number(row?.unLockedQuantity ?? 0);
- const warn = Number(row?.warnNum ?? 0);
- return Number.isFinite(stock) && Number.isFinite(warn) && stock < warn;
-};
-
-onMounted(() => {
- getList();
-});
-</script>
-
-<style scoped lang="scss">
-@import '@/styles/sales-common.scss';
-
-.qualified-record-container {
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.tag-type {
- background-color: #e3f2fd;
- color: #2196f3;
- padding: 2px 8px;
- border-radius: 4px;
- font-size: 12px;
-}
-
-.ledger-list {
- flex: 1;
- overflow-y: auto;
-}
-
-.low-stock {
- background-color: #fde2e2;
- color: #c45656;
-}
-
-.no-data {
- padding-top: 100px;
-}
-</style>
diff --git a/src/pages/inventoryManagement/stockManagement/Record.vue b/src/pages/inventoryManagement/stockManagement/Record.vue
new file mode 100644
index 0000000..e5d5ca2
--- /dev/null
+++ b/src/pages/inventoryManagement/stockManagement/Record.vue
@@ -0,0 +1,443 @@
+<template>
+ <view class="record-container">
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
+ v-model="searchForm.productName"
+ @confirm="handleQuery"
+ clearable />
+ </view>
+ <view class="filter-button"
+ @click="handleQuery">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <scroll-view scroll-y
+ class="ledger-list"
+ v-if="tableData.length > 0"
+ @scrolltolower="loadMore">
+ <view v-for="item in tableData"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.productName }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.model }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ item.unit }}</text>
+ </view>
+ <view class="detail-row"
+ @click="handleShowBatch(item.batchNo)">
+ <text class="detail-label">鎵瑰彿</text>
+ <view class="detail-value batch-no-wrapper">
+ <text class="batch-no-text"
+ :class="{ 'clickable': isBatchClickable(item.batchNo) }">
+ {{ formatBatchNo(item.batchNo) }}
+ </text>
+ <up-icon v-if="isBatchClickable(item.batchNo)"
+ name="arrow-right"
+ size="14"
+ color="#2979ff"></up-icon>
+ </view>
+ </view>
+ <view class="quantity-section">
+ <view class="quantity-box qualified">
+ <text class="q-label">鍚堟牸搴撳瓨</text>
+ <text class="q-value">{{ item.qualifiedQuantity }}</text>
+ </view>
+ <view class="quantity-box unqualified">
+ <text class="q-label">涓嶅悎鏍煎簱瀛�</text>
+ <text class="q-value">{{ item.unQualifiedQuantity }}</text>
+ </view>
+ </view>
+ <view class="quantity-section">
+ <view class="quantity-box locked">
+ <text class="q-label">鍚堟牸鍐荤粨</text>
+ <text class="q-value">{{ item.qualifiedLockedQuantity }}</text>
+ </view>
+ <view class="quantity-box locked">
+ <text class="q-label">涓嶅悎鏍煎喕缁�</text>
+ <text class="q-value">{{ item.unQualifiedLockedQuantity }}</text>
+ </view>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴撳瓨棰勮</text>
+ <text class="detail-value">{{ item.warnNum }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏇存柊鏃堕棿</text>
+ <text class="detail-value">{{ item.updateTime }}</text>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus" />
+ </scroll-view>
+ <view v-else-if="!loading"
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤搴撳瓨鏁版嵁"></up-empty>
+ </view>
+ <!-- 鎵瑰彿鍒楄〃寮圭獥 -->
+ <up-popup v-model:show="showBatchPopup"
+ @close="showBatchPopup = false"
+ mode="bottom"
+ round="20"
+ closeable>
+ <view class="batch-popup-content">
+ <view class="popup-header">
+ <text class="popup-title">鎵瑰彿璇︽儏</text>
+ </view>
+ <scroll-view scroll-y
+ class="batch-list-scroll">
+ <view class="batch-list">
+ <view v-for="(batch, index) in currentBatchList"
+ :key="index"
+ class="batch-item">
+ <view class="batch-index-box">
+ <text class="batch-index">{{ index + 1 }}</text>
+ </view>
+ <text class="batch-text">{{ batch }}</text>
+ </view>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted } from "vue";
+ import { getStockInventoryListPageCombined } from "@/api/inventoryManagement/stockInventory.js";
+
+ const props = defineProps({
+ productId: {
+ type: Number,
+ required: true,
+ },
+ });
+
+ const tableData = ref([]);
+ const loading = ref(false);
+ const loadStatus = ref("loadmore");
+ const page = reactive({ current: 1, size: 10 });
+ const total = ref(0);
+ const searchForm = reactive({
+ productName: "",
+ topParentProductId: props.productId,
+ });
+
+ const showBatchPopup = ref(false);
+ const currentBatchList = ref([]);
+
+ const handleQuery = () => {
+ page.current = 1;
+ tableData.value = [];
+ getList();
+ };
+
+ const getList = () => {
+ if (loading.value) return;
+ loading.value = true;
+ loadStatus.value = "loading";
+
+ getStockInventoryListPageCombined({
+ ...searchForm,
+ current: page.current,
+ size: page.size,
+ })
+ .then(res => {
+ loading.value = false;
+ const records = res.data.records || [];
+ tableData.value =
+ page.current === 1 ? records : [...tableData.value, ...records];
+ total.value = res.data.total;
+ loadStatus.value =
+ tableData.value.length >= total.value ? "nomore" : "loadmore";
+ })
+ .catch(() => {
+ loading.value = false;
+ loadStatus.value = "loadmore";
+ });
+ };
+
+ const loadMore = () => {
+ if (loadStatus.value === "loadmore") {
+ page.current++;
+ getList();
+ }
+ };
+
+ const handleShowBatch = batchNo => {
+ if (!batchNo) return;
+ // 鏀寔閫楀彿銆佺┖鏍兼垨鎹㈣鍒嗛殧
+ currentBatchList.value = batchNo
+ .split(/[,锛孿s\n]+/)
+ .filter(item => item.trim() !== "");
+ if (currentBatchList.value.length > 0) {
+ showBatchPopup.value = true;
+ }
+ };
+
+ const formatBatchNo = batchNo => {
+ if (!batchNo) return "-";
+ if (batchNo.length > 25) {
+ return batchNo.substring(0, 25) + "...";
+ }
+ return batchNo;
+ };
+
+ const isBatchClickable = batchNo => {
+ if (!batchNo) return false;
+ return batchNo.length > 25 || batchNo.includes(",") || batchNo.includes("锛�");
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .record-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f7fa;
+ }
+
+ .search-section {
+ padding: 20rpx;
+ background-color: #ffffff;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ }
+
+ .search-bar {
+ display: flex;
+ align-items: center;
+ background-color: #f2f2f2;
+ border-radius: 40rpx;
+ padding: 0 30rpx;
+ height: 80rpx;
+ }
+
+ .search-input {
+ flex: 1;
+ }
+
+ .search-text {
+ font-size: 28rpx;
+ }
+
+ .filter-button {
+ padding-left: 20rpx;
+ }
+
+ .ledger-list {
+ padding: 20rpx;
+ box-sizing: border-box;
+ overflow: auto;
+ }
+
+ .ledger-item {
+ background-color: #ffffff;
+ border-radius: 16rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+ }
+
+ .item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+ }
+
+ .item-left {
+ display: flex;
+ align-items: center;
+ }
+
+ .document-icon {
+ width: 40rpx;
+ height: 40rpx;
+ background: linear-gradient(135deg, #2979ff, #1565c0);
+ border-radius: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 16rpx;
+ }
+
+ .item-id {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #303133;
+ }
+
+ .item-details {
+ .detail-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16rpx;
+ font-size: 26rpx;
+
+ .detail-label {
+ color: #909399;
+ }
+
+ .detail-value {
+ color: #303133;
+ font-weight: 500;
+ }
+
+ .batch-no-wrapper {
+ display: flex;
+ align-items: center;
+ max-width: 70%;
+
+ .batch-no-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &.clickable {
+ color: #2979ff;
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+ }
+
+ .batch-popup-content {
+ background-color: #fff;
+ padding: 30rpx;
+ max-height: 70vh;
+ display: flex;
+ flex-direction: column;
+
+ .popup-header {
+ padding-bottom: 30rpx;
+ border-bottom: 1rpx solid #ebeef5;
+ margin-bottom: 20rpx;
+ text-align: center;
+
+ .popup-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #303133;
+ }
+ }
+
+ .batch-list-scroll {
+ flex: 1;
+ overflow: hidden;
+ }
+
+ .batch-list {
+ padding: 10rpx 0;
+
+ .batch-item {
+ display: flex;
+ align-items: center;
+ padding: 24rpx 0;
+ border-bottom: 1rpx solid #f2f6fc;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .batch-index-box {
+ width: 40rpx;
+ height: 40rpx;
+ background-color: #f0f2f5;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 20rpx;
+
+ .batch-index {
+ font-size: 20rpx;
+ color: #909399;
+ }
+ }
+
+ .batch-text {
+ font-size: 28rpx;
+ color: #303133;
+ flex: 1;
+ word-break: break-all;
+ }
+ }
+ }
+ }
+
+ .quantity-section {
+ display: flex;
+ gap: 20rpx;
+ margin: 20rpx 0;
+
+ .quantity-box {
+ flex: 1;
+ padding: 16rpx;
+ border-radius: 8rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .q-label {
+ font-size: 22rpx;
+ margin-bottom: 8rpx;
+ }
+
+ .q-value {
+ font-size: 32rpx;
+ font-weight: bold;
+ }
+
+ &.qualified {
+ background-color: #ecf5ff;
+ color: #409eff;
+ }
+
+ &.unqualified {
+ background-color: #fef0f0;
+ color: #f56c6c;
+ }
+
+ &.locked {
+ background-color: #f4f4f5;
+ color: #909399;
+ }
+ }
+ }
+
+ .no-data {
+ padding-top: 200rpx;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/stockManagement/Unqualified.vue b/src/pages/inventoryManagement/stockManagement/Unqualified.vue
deleted file mode 100644
index 48dafc4..0000000
--- a/src/pages/inventoryManagement/stockManagement/Unqualified.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<template>
- <view class="unqualified-record-container">
- <view class="search-section">
- <view class="search-bar">
- <view class="search-input">
- <up-input
- class="search-text"
- placeholder="璇疯緭鍏ヤ骇鍝佸ぇ绫�"
- v-model="searchForm.productName"
- @confirm="handleQuery"
- clearable
- />
- </view>
- <view class="filter-button" @click="handleQuery">
- <up-icon name="search" size="24" color="#999"></up-icon>
- </view>
- </view>
- </view>
-
- <scroll-view scroll-y class="ledger-list" v-if="tableData.length > 0" @scrolltolower="loadMore">
- <view v-for="item in tableData" :key="item.id" class="ledger-item">
- <view class="item-header">
- <view class="item-left">
- <view class="document-icon">
- <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
- </view>
- <text class="item-id">{{ item.productName }}</text>
- </view>
- <view class="item-right">
- <text class="item-tag tag-type" style="background-color: #fde2e2; color: #f56c6c;">涓嶅悎鏍煎簱瀛�</text>
- </view>
- </view>
-
- <up-divider></up-divider>
-
- <view class="item-details">
- <view class="detail-row">
- <text class="detail-label">瑙勬牸鍨嬪彿</text>
- <text class="detail-value">{{ item.model }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">搴撳瓨鏁伴噺</text>
- <text class="detail-value">{{ item.qualitity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">閿佸畾/鍐荤粨</text>
- <text class="detail-value">{{ item.lockedQuantity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鍙敤搴撳瓨</text>
- <text class="detail-value" style="color: #f56c6c; font-weight: bold;">{{ item.unLockedQuantity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鏇存柊鏃堕棿</text>
- <text class="detail-value">{{ item.updateTime }}</text>
- </view>
- </view>
- </view>
- <up-loadmore :status="loadStatus" />
- </scroll-view>
- <view v-else-if="!loading" class="no-data">
- <up-empty mode="data" text="鏆傛棤涓嶅悎鏍煎簱瀛樻暟鎹�"></up-empty>
- </view>
- </view>
-</template>
-
-<script setup>
-import { ref, reactive, onMounted } from 'vue';
-import { getStockUninventoryListPage } from "@/api/inventoryManagement/stockUninventory.js";
-
-const tableData = ref([]);
-const loading = ref(false);
-const loadStatus = ref('loadmore');
-const page = reactive({ current: 1, size: 10 });
-const total = ref(0);
-const searchForm = reactive({ productName: '' });
-
-const handleQuery = () => {
- page.current = 1;
- tableData.value = [];
- getList();
-};
-
-const getList = () => {
- if (loading.value) return;
- loading.value = true;
- loadStatus.value = 'loading';
- getStockUninventoryListPage({ ...searchForm, ...page, type: 'unqualified' }).then(res => {
- loading.value = false;
- const records = res.data.records || [];
- tableData.value = page.current === 1 ? records : [...tableData.value, ...records];
- total.value = res.data.total;
- loadStatus.value = tableData.value.length >= total.value ? 'nomore' : 'loadmore';
- }).catch(() => {
- loading.value = false;
- loadStatus.value = 'loadmore';
- });
-};
-
-const loadMore = () => {
- if (loadStatus.value === 'nomore' || loading.value) return;
- page.current++;
- getList();
-};
-
-onMounted(() => {
- getList();
-});
-</script>
-
-<style scoped lang="scss">
-@import '@/styles/sales-common.scss';
-
-.unqualified-record-container {
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.tag-type {
- padding: 2px 8px;
- border-radius: 4px;
- font-size: 12px;
-}
-
-.ledger-list {
- flex: 1;
- overflow-y: auto;
-}
-
-.no-data {
- padding-top: 100px;
-}
-</style>
diff --git a/src/pages/inventoryManagement/stockManagement/index.vue b/src/pages/inventoryManagement/stockManagement/index.vue
index 98ebf44..45d27ad 100644
--- a/src/pages/inventoryManagement/stockManagement/index.vue
+++ b/src/pages/inventoryManagement/stockManagement/index.vue
@@ -1,57 +1,104 @@
<template>
<view class="app-container">
- <PageHeader title="搴撳瓨绠$悊" @back="goBack" />
- <up-tabs :list="tabs" @click="handleTabClick" :current="activeTab"/>
- <swiper class="swiper-box" :current="activeTab" @change="handleSwiperChange">
- <swiper-item class="swiper-item">
- <qualified-record />
- </swiper-item>
- <swiper-item class="swiper-item">
- <unqualified-record />
- </swiper-item>
- </swiper>
+ <PageHeader title="搴撳瓨绠$悊"
+ @back="goBack" />
+ <view v-if="loading"
+ class="loading-state">
+ <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+ </view>
+ <template v-else>
+ <up-tabs :list="tabs"
+ @click="handleTabClick"
+ :current="activeTab" />
+ <swiper class="swiper-box"
+ :current="activeTab"
+ @change="handleSwiperChange">
+ <swiper-item class="swiper-item"
+ v-for="tab in products"
+ :key="tab.id">
+ <record :product-id="tab.id"
+ v-if="activeTab === products.indexOf(tab) || initializedTabs.includes(tab.id)" />
+ </swiper-item>
+ </swiper>
+ </template>
</view>
</template>
<script setup>
-import { ref } from 'vue';
-import PageHeader from "@/components/PageHeader.vue";
-import QualifiedRecord from "./Qualified.vue";
-import UnqualifiedRecord from "./Unqualified.vue";
+ import { ref, onMounted } from "vue";
+ import PageHeader from "@/components/PageHeader.vue";
+ import Record from "./Record.vue";
+ import { productTreeList } from "@/api/basicData/product.js";
-const activeTab = ref(0);
-const tabs = ref([
- { name: '鍚堟牸搴撳瓨' },
- { name: '涓嶅悎鏍煎簱瀛�' }
-]);
+ const activeTab = ref(0);
+ const tabs = ref([]);
+ const products = ref([]);
+ const loading = ref(false);
+ const initializedTabs = ref([]);
-const handleTabClick = (item) => {
- activeTab.value = item.index;
-};
+ const handleTabClick = item => {
+ activeTab.value = item.index;
+ if (!initializedTabs.value.includes(products.value[item.index].id)) {
+ initializedTabs.value.push(products.value[item.index].id);
+ }
+ };
-const handleSwiperChange = (e) => {
- activeTab.value = e.detail.current;
-};
+ const handleSwiperChange = e => {
+ const index = e.detail.current;
+ activeTab.value = index;
+ if (!initializedTabs.value.includes(products.value[index].id)) {
+ initializedTabs.value.push(products.value[index].id);
+ }
+ };
-const goBack = () => {
- uni.navigateBack();
-};
+ const fetchProducts = async () => {
+ loading.value = true;
+ try {
+ const res = await productTreeList();
+ // 杩囨护鏍硅妭鐐逛骇鍝�
+ products.value = res
+ .filter(item => item.parentId === null)
+ .map(({ id, productName }) => ({ id, productName }));
+ tabs.value = products.value.map(p => ({ name: p.productName }));
+
+ if (products.value.length > 0) {
+ activeTab.value = 0;
+ initializedTabs.value = [products.value[0].id];
+ }
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ onMounted(() => {
+ fetchProducts();
+ });
</script>
<style scoped lang="scss">
-.app-container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- background-color: #f8f9fa;
-}
-.swiper-box {
- flex: 1;
-}
-.swiper-item {
- height: 100%;
-}
-:deep(.up-tabs) {
- background-color: #fff;
-}
+ .app-container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ background-color: #f8f9fa;
+ }
+ .loading-state {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .swiper-box {
+ flex: 1;
+ }
+ .swiper-item {
+ height: 100%;
+ }
+ :deep(.up-tabs) {
+ background-color: #fff;
+ }
</style>
diff --git a/src/pages/login.vue b/src/pages/login.vue
index ee49113..2d96046 100644
--- a/src/pages/login.vue
+++ b/src/pages/login.vue
@@ -22,6 +22,21 @@
clearable
type="password"></up-input>
</view>
+ <!-- <view class="input-item flex align-center"
+ v-if="factoryList.length > 0">
+ <up-input prefixIcon="home"
+ placeholder="璇烽�夋嫨宸ュ巶"
+ border="bottom"
+ readonly
+ @click="showFactorySelect = true"
+ v-model="selectedFactoryName"
+ clearable></up-input>
+ <up-action-sheet :show="showFactorySelect"
+ :actions="factoryList"
+ title="璇烽�夋嫨宸ュ巶"
+ @close="showFactorySelect = false"
+ @select="handleFactorySelect"></up-action-sheet>
+ </view> -->
<view>
<button @click="handleLogin"
class="login-btn cu-btn block bg-blue lg round">鐧诲綍</button>
@@ -70,9 +85,17 @@
const loginForm = ref({
userName: "",
password: "",
- currentFatoryName: "",
+ factoryId: "",
});
const factoryList = ref([]); // 鍏徃鍒楄〃
+ const showFactorySelect = ref(false);
+ const selectedFactoryName = ref("");
+
+ const handleFactorySelect = e => {
+ loginForm.value.factoryId = e.id;
+ selectedFactoryName.value = e.name;
+ showFactorySelect.value = false;
+ };
// 淇濆瓨瀵嗙爜鍒版湰鍦板瓨鍌�
function savePassword() {
@@ -127,17 +150,28 @@
id: item.deptId,
name: item.deptName,
}));
+ // 濡傛灉鍙湁涓�涓伐鍘傦紝榛樿閫変腑
+ if (factoryList.value.length === 1) {
+ loginForm.value.factoryId = factoryList.value[0].id;
+ selectedFactoryName.value = factoryList.value[0].name;
+ }
} else {
// 濡傛灉res.data涓嶆槸鏁扮粍锛岃缃负绌烘暟缁�
factoryList.value = [];
+ loginForm.value.factoryId = "";
+ selectedFactoryName.value = "";
}
})
.catch(error => {
showToast("鑾峰彇鍏徃鍒楄〃澶辫触:", error);
factoryList.value = [];
+ loginForm.value.factoryId = "";
+ selectedFactoryName.value = "";
});
} else {
factoryList.value = [];
+ loginForm.value.factoryId = "";
+ selectedFactoryName.value = "";
}
}
@@ -146,6 +180,8 @@
showToast("璇疯緭鍏ユ偍鐨勮处鍙�");
} else if (loginForm.value.password === "") {
showToast("璇疯緭鍏ユ偍鐨勫瘑鐮�");
+ } else if (factoryList.value.length > 0 && loginForm.value.factoryId === "") {
+ showToast("璇烽�夋嫨宸ュ巶");
} else {
showToast("鐧诲綍涓紝璇疯�愬績绛夊緟...");
pwdLogin();
@@ -254,7 +290,10 @@
const accountInfo = uni.getAccountInfoSync();
if (accountInfo?.miniProgram?.version) {
versionName.value = accountInfo.miniProgram.version;
- console.log("[login-version] 褰撳墠鐜=MP-WEIXIN锛岀増鏈�=", versionName.value);
+ console.log(
+ "[login-version] 褰撳墠鐜=MP-WEIXIN锛岀増鏈�=",
+ versionName.value
+ );
}
} catch (e) {
// 鑾峰彇澶辫触鏃朵娇鐢ㄩ粯璁ゅ��
@@ -270,18 +309,27 @@
// @ts-ignore
const appid = plus.runtime.appid;
// @ts-ignore
- plus.runtime.getProperty(appid, (info) => {
+ plus.runtime.getProperty(appid, info => {
const v = info?.version || info?.versionName || "";
if (v) {
versionName.value = String(v);
- console.log("[login-version] 褰撳墠鐜=APP-PLUS锛岀増鏈�=", versionName.value);
+ console.log(
+ "[login-version] 褰撳墠鐜=APP-PLUS锛岀増鏈�=",
+ versionName.value
+ );
} else {
- console.log("[login-version] APP-PLUS 鑾峰彇鍒扮殑鐗堟湰瀛楁涓虹┖锛屼娇鐢ㄩ粯璁ゅ��:", versionName.value);
+ console.log(
+ "[login-version] APP-PLUS 鑾峰彇鍒扮殑鐗堟湰瀛楁涓虹┖锛屼娇鐢ㄩ粯璁ゅ��:",
+ versionName.value
+ );
}
console.log("[login-version] 鏈�缁堢増鏈彿:", versionName.value);
});
} else {
- console.log("[login-version] APP-PLUS 鐜涓嬬己灏� getProperty锛屼娇鐢ㄩ粯璁ゅ��:", versionName.value);
+ console.log(
+ "[login-version] APP-PLUS 鐜涓嬬己灏� getProperty锛屼娇鐢ㄩ粯璁ゅ��:",
+ versionName.value
+ );
console.log("[login-version] 鏈�缁堢増鏈彿:", versionName.value);
}
// #endif
diff --git a/src/pages/message.vue b/src/pages/message.vue
index d035392..e1ce2a4 100644
--- a/src/pages/message.vue
+++ b/src/pages/message.vue
@@ -310,7 +310,7 @@
const handleTabChange = val => {
console.log(val);
activeTab.value = val.id;
- page.current = 2;
+ page.current = 1;
loadMessages(false);
};
diff --git a/src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue b/src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue
new file mode 100644
index 0000000..72d30b6
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/_components/ApproveInstanceDetailBody.vue
@@ -0,0 +1,413 @@
+<!--
+ 瀹℃壒瀹炰緥璇︽儏灞曠ず锛氬熀鏈俊鎭� + 濉姤 + 娴佺▼ + 瀹℃壒璁板綍
+-->
+<template>
+ <view class="detail-body">
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">鍩烘湰淇℃伅</text>
+ </view>
+ <view class="info-rows">
+ <view class="info-row">
+ <text class="info-label">涓氬姟鍗曞彿</text>
+ <text class="info-value">{{ row.instanceNo || row.id || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">瀹℃壒鐘舵��</text>
+ <u-tag :type="statusTagType(row.status)"
+ :text="statusLabel(row.status)"
+ size="mini" />
+ </view>
+ <view class="info-row">
+ <text class="info-label">妯℃澘鍚嶇О</text>
+ <text class="info-value">{{ row.templateName || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">涓氬姟鍚嶇О</text>
+ <text class="info-value">{{ row.businessName || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鐢宠浜�</text>
+ <text class="info-value">{{ row.applicantName || "鈥�" }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鐢宠鏍囬</text>
+ <text class="info-value">{{ row.title || "鈥�" }}</text>
+ </view>
+ <view v-if="rejectReason"
+ class="info-row">
+ <text class="info-label">椹冲洖鍘熷洜</text>
+ <text class="info-value reject-text">{{ rejectReason }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">鐢宠鏃堕棿</text>
+ <text class="info-value">{{ formatDateTime(row.applyTime || row.createTime) }}</text>
+ </view>
+ <view v-if="row.finishTime"
+ class="info-row">
+ <text class="info-label">瀹屾垚鏃堕棿</text>
+ <text class="info-value">{{ formatDateTime(row.finishTime) }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">濉姤鍐呭</text>
+ </view>
+ <view v-if="displayFields.length"
+ class="info-rows">
+ <view v-for="field in displayFields"
+ :key="field.key"
+ class="info-row">
+ <text class="info-label">{{ field.label }}</text>
+ <text class="info-value">{{ displayFieldValue(field) }}</text>
+ </view>
+ <view v-for="(extra, idx) in moduleExtraRows"
+ :key="`extra-${idx}`"
+ class="info-row">
+ <text class="info-label">{{ extra.label }}</text>
+ <text class="info-value">{{ extra.value }}</text>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤濉姤鍐呭</view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒娴佺▼锛坽{ flowNodes.length }} 椤癸級</text>
+ </view>
+ <view v-if="flowNodes.length"
+ class="flow-wrap">
+ <view v-for="(node, nodeIndex) in flowNodes"
+ :key="nodeIndex"
+ class="flow-node-block">
+ <view class="flow-node-card">
+ <view class="node-header">
+ <view class="node-level-badge">{{ node.levelNo }}</view>
+ <text class="node-level-text">绗瑊{ levelLabel(node.levelNo) }}绾�</text>
+ <u-tag size="mini"
+ :type="node.approveType === 'OR' ? 'warning' : 'primary'"
+ :text="node.approveType === 'OR' ? '鎴栫' : '浼氱'"
+ plain />
+ </view>
+ <view class="approver-list">
+ <view v-for="(a, aIdx) in node.approvers"
+ :key="aIdx"
+ class="approver-row">
+ <text class="approver-name">{{ a.approverName }}</text>
+ <u-tag v-if="a.taskStatus"
+ size="mini"
+ :type="taskStatusTagType(a.taskStatus)"
+ :text="taskStatusText(a.taskStatus)"
+ plain />
+ </view>
+ </view>
+ </view>
+ <view v-if="nodeIndex < flowNodes.length - 1"
+ class="flow-connector-line" />
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤娴佺▼鑺傜偣</view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒璁板綍</text>
+ </view>
+ <view v-if="approvalRecords.length"
+ class="record-list">
+ <view v-for="(rec, index) in approvalRecords"
+ :key="rec.id ?? index"
+ class="record-item">
+ <view class="record-head">
+ <text class="record-operator">{{ rec.operatorName }}</text>
+ <u-tag size="mini"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'error' : 'info'"
+ :text="recordActionLabel(rec.result)"
+ plain />
+ </view>
+ <text class="record-time">{{ rec.time }}</text>
+ <text class="record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</text>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤瀹℃壒璁板綍</view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed } from "vue";
+ import { APPROVAL_MODULE_KEYS } from "../../../_utils/approvalModuleRegistry.js";
+ import {
+ computeLeaveDurationDisplay,
+ computeOvertimeHoursDisplay,
+ } from "../../../_utils/approvalModuleApplyExtras.js";
+ import { resolveInstanceFormPayload } from "../../../_utils/approvalModuleListSearch.js";
+ import {
+ businessStatusTagType,
+ businessStatusText,
+ displayFieldValue,
+ formatDateTime,
+ getRejectReasonFromRecords,
+ instanceStatusTagType,
+ instanceStatusText,
+ mapApprovalRecords,
+ mapTasksToFlowNodes,
+ recordActionLabel,
+ resolveInstanceDisplayFields,
+ taskStatusTagType,
+ taskStatusText,
+ } from "../../../_utils/approveListUtils.js";
+
+ const props = defineProps({
+ row: { type: Object, default: () => ({}) },
+ moduleKey: { type: String, default: "" },
+ });
+
+ const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+ const isBusinessModule = computed(() =>
+ [
+ APPROVAL_MODULE_KEYS.LEAVE,
+ APPROVAL_MODULE_KEYS.OVERTIME,
+ APPROVAL_MODULE_KEYS.TRANSFER,
+ APPROVAL_MODULE_KEYS.REGULAR,
+ APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+ ].includes(props.moduleKey)
+ );
+
+ const statusLabel = status =>
+ isBusinessModule.value ? businessStatusText(status) : instanceStatusText(status);
+
+ const statusTagType = status =>
+ isBusinessModule.value ? businessStatusTagType(status) : instanceStatusTagType(status);
+
+ const displayFields = computed(() => resolveInstanceDisplayFields(props.row));
+
+ const moduleExtraRows = computed(() => {
+ const rows = [];
+ const { fields, formPayload } = resolveInstanceFormPayload(props.row);
+ const payload = { ...formPayload };
+ (fields || []).forEach(f => {
+ if (f?.key && payload[f.key] == null) payload[f.key] = f.value ?? "";
+ });
+ if (props.moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ const balance = payload.leaveBalanceDays;
+ if (balance != null && balance !== "") {
+ rows.push({ label: "鍋囨湡浣欓", value: `${balance} 澶ー });
+ }
+ const days = computeLeaveDurationDisplay(fields, payload);
+ if (days) rows.push({ label: "璇峰亣鏃堕暱", value: `${days} 澶ー });
+ }
+ if (props.moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ const hours = computeOvertimeHoursDisplay(fields, payload);
+ if (hours) rows.push({ label: "鍔犵彮鏃堕暱", value: `${hours} 灏忔椂` });
+ }
+ if (props.moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) {
+ const post = payload.originalPostName || payload.originalPost;
+ if (post) rows.push({ label: "鍘熷矖浣�", value: post });
+ }
+ return rows;
+ });
+
+ const flowNodes = computed(() => mapTasksToFlowNodes(props.row?.tasks));
+
+ const approvalRecords = computed(() =>
+ mapApprovalRecords(props.row?.records)
+ );
+
+ const rejectReason = computed(() =>
+ getRejectReasonFromRecords(props.row?.records)
+ );
+
+ const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
+</script>
+
+<style scoped lang="scss">
+ $primary: #2979ff;
+ $text: #1f2d3d;
+ $text-muted: #909399;
+
+ .detail-body {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .section-card {
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
+ }
+
+ .section-head {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f7;
+ }
+
+ .section-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: $text;
+ padding-left: 10px;
+ border-left: 3px solid $primary;
+ line-height: 1.2;
+ }
+
+ .info-rows {
+ padding: 4px 16px 12px;
+ }
+
+ .info-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 10px 0;
+ border-bottom: 1px solid #f5f6f8;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .info-label {
+ flex-shrink: 0;
+ font-size: 14px;
+ color: $text-muted;
+ min-width: 72px;
+ }
+
+ .info-value {
+ flex: 1;
+ font-size: 14px;
+ color: $text;
+ text-align: right;
+ word-break: break-all;
+ }
+
+ .reject-text {
+ color: #f56c6c;
+ }
+
+ .flow-wrap {
+ padding: 10px 16px 14px;
+ }
+
+ .flow-node-block {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .flow-node-card {
+ background: #fafbfd;
+ border: 1px solid #e8eef5;
+ border-radius: 10px;
+ padding: 12px;
+ }
+
+ .node-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 10px;
+ }
+
+ .node-level-badge {
+ width: 26px;
+ height: 26px;
+ border-radius: 8px;
+ background: $primary;
+ color: #fff;
+ font-size: 13px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .node-level-text {
+ flex: 1;
+ font-size: 14px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .approver-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .approver-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ }
+
+ .approver-name {
+ font-size: 13px;
+ color: #606266;
+ }
+
+ .flow-connector-line {
+ width: 2px;
+ height: 12px;
+ background: #d0dff0;
+ margin: 4px auto;
+ }
+
+ .record-list {
+ padding: 8px 16px 14px;
+ }
+
+ .record-item {
+ padding: 10px 0;
+ border-bottom: 1px solid #f0f2f5;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .record-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ }
+
+ .record-operator {
+ font-size: 14px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .record-time {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+ color: $text-muted;
+ }
+
+ .record-opinion {
+ display: block;
+ margin-top: 6px;
+ font-size: 13px;
+ color: #606266;
+ line-height: 1.5;
+ }
+
+ .empty-hint {
+ padding: 12px 16px 16px;
+ font-size: 13px;
+ color: $text-muted;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/apply.vue b/src/pages/oa/ApproveManage/approve-list/apply.vue
new file mode 100644
index 0000000..b529db3
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/apply.vue
@@ -0,0 +1,1274 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 鍙戣捣瀹℃壒
+ 璺敱锛�/pages/oa/ApproveManage/approve-list/apply
+-->
+<template>
+ <view class="approve-apply-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <scroll-view class="form-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view v-if="loading"
+ class="loading-wrap">
+ <up-loading-icon mode="circle" />
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+ <template v-else-if="detail">
+ <up-form :model="form"
+ label-width="100"
+ input-align="right">
+ <u-cell-group title="鍩烘湰淇℃伅"
+ class="form-section">
+ <up-form-item label="瀹℃壒鏍囬"
+ required
+ class="form-item-name">
+ <up-input v-model="form.title"
+ class="name-input-inline"
+ placeholder="璇疯緭鍏ュ鎵规爣棰�"
+ maxlength="100"
+ clearable />
+ </up-form-item>
+ <up-form-item label="瀹℃壒妯℃澘"
+ class="form-item-readonly">
+ <up-input :model-value="templateName"
+ readonly />
+ </up-form-item>
+ <up-form-item label="鐢宠浜�"
+ class="form-item-readonly">
+ <up-input :model-value="displayApplicantName"
+ readonly />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">濉姤鍐呭</text>
+ </view>
+ <view v-if="formConfigData.prompt"
+ class="form-prompt">
+ {{ formConfigData.prompt }}
+ </view>
+ <up-form v-if="formConfigData.fields.length"
+ :model="formValues"
+ label-width="100"
+ input-align="right"
+ class="dynamic-form">
+ <up-form-item v-for="field in displayTemplateFields"
+ :key="field.key"
+ :label="field.label"
+ :required="!!field.required"
+ :label-position="formItemLabelPosition(field)"
+ :class="formItemClass(field)">
+ <up-textarea v-if="isTextareaField(field)"
+ v-model="formValues[field.key]"
+ :placeholder="`璇疯緭鍏�${field.label}`"
+ maxlength="500"
+ border="surround"
+ height="80" />
+ <view v-else-if="isDatetimerangeField(field)"
+ class="daterange-fill">
+ <view class="range-fill-row"
+ @click="openRangePicker(field, 'start')">
+ <text class="range-fill-label">寮�濮�</text>
+ <up-input :model-value="getRangePartDisplay(field, 'start')"
+ placeholder="寮�濮嬫椂闂�"
+ readonly />
+ <up-icon name="calendar"
+ size="16"
+ color="#909399" />
+ </view>
+ <text class="range-fill-sep">鑷�</text>
+ <view class="range-fill-row"
+ @click="openRangePicker(field, 'end')">
+ <text class="range-fill-label">缁撴潫</text>
+ <up-input :model-value="getRangePartDisplay(field, 'end')"
+ placeholder="缁撴潫鏃堕棿"
+ readonly />
+ <up-icon name="calendar"
+ size="16"
+ color="#909399" />
+ </view>
+ </view>
+ <view v-else-if="isDateLikeField(field)"
+ class="field-trigger"
+ @click="openDatePicker(field)">
+ <up-input :model-value="formatFieldDisplayValue(field, formValues[field.key])"
+ :placeholder="`璇烽�夋嫨${field.label}`"
+ readonly />
+ <up-icon :name="getDatePickerMode(field) === 'time' ? 'clock' : 'calendar'"
+ size="18"
+ color="#909399" />
+ </view>
+ <view v-else-if="isSelectField(field)"
+ class="field-trigger"
+ @click="openSelectPicker(field)">
+ <up-input :model-value="getSelectDisplayText(field)"
+ :placeholder="`璇烽�夋嫨${field.label}`"
+ readonly />
+ <up-icon name="arrow-right"
+ size="16"
+ color="#c0c4cc" />
+ </view>
+ <up-input v-else
+ v-model="formValues[field.key]"
+ :type="isNumberField(field) ? 'digit' : 'text'"
+ :placeholder="`璇疯緭鍏�${field.label}`"
+ clearable />
+ </up-form-item>
+ </up-form>
+ <view v-else
+ class="empty-hint">璇ユā鏉挎殏鏃犲~鎶ラ」</view>
+
+ <!-- 璇峰亣锛氬亣鏈熶綑棰� + 鏃堕暱鑷姩璁$畻 -->
+ <view v-if="isLeaveModule"
+ class="module-extra-block">
+ <up-form :model="extraForm"
+ label-width="100"
+ input-align="right"
+ class="dynamic-form">
+ <up-form-item label="鍋囨湡浣欓"
+ required
+ class="form-item-inline">
+ <up-input v-model="extraForm.leaveBalanceDays"
+ type="digit"
+ placeholder="璇疯緭鍏ュぉ鏁�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璇峰亣鏃堕暱"
+ class="form-item-inline">
+ <view class="readonly-with-unit">
+ <up-input :model-value="leaveDurationText"
+ readonly
+ placeholder="鏍规嵁璇峰亣鏃堕棿鑷姩璁$畻" />
+ <text class="unit-text">澶�</text>
+ </view>
+ </up-form-item>
+ </up-form>
+ </view>
+
+ <!-- 鍔犵彮锛氭椂闀胯嚜鍔ㄨ绠� -->
+ <view v-if="isOvertimeModule"
+ class="module-extra-block">
+ <up-form label-width="100"
+ input-align="right"
+ class="dynamic-form">
+ <up-form-item label="鍔犵彮鏃堕暱"
+ class="form-item-inline">
+ <view class="readonly-with-unit">
+ <up-input :model-value="overtimeHoursText"
+ readonly
+ placeholder="鏍规嵁鍔犵彮鏃堕棿鑷姩璁$畻" />
+ <text class="unit-text">灏忔椂</text>
+ </view>
+ </up-form-item>
+ </up-form>
+ </view>
+
+ <!-- 璋冨矖锛氬師宀椾綅鑷姩甯﹀嚭 -->
+ <view v-if="isTransferModule"
+ class="module-extra-block">
+ <up-form label-width="100"
+ input-align="right"
+ class="dynamic-form">
+ <up-form-item label="鍘熷矖浣�"
+ class="form-item-readonly">
+ <up-input :model-value="extraForm.originalPostName"
+ readonly
+ placeholder="閫夋嫨鐢宠浜哄悗鑷姩甯﹀嚭" />
+ </up-form-item>
+ </up-form>
+ </view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒娴佺▼</text>
+ </view>
+ <view v-if="detail.nodes?.length"
+ class="flow-wrap">
+ <view v-for="(node, nodeIndex) in detail.nodes"
+ :key="node.id || nodeIndex"
+ class="flow-node-block">
+ <view class="flow-node-card">
+ <view class="node-header">
+ <view class="node-level-badge">{{ node.levelNo || nodeIndex + 1 }}</view>
+ <text class="node-level-text">绗瑊{ levelLabel(node.levelNo || nodeIndex + 1) }}绾�</text>
+ </view>
+ <view class="approve-type-row approve-type-row--readonly">
+ <view class="type-btn"
+ :class="{ active: node.approveType !== 'OR' }">
+ 浼氱
+ </view>
+ <view class="type-btn"
+ :class="{ active: node.approveType === 'OR' }">
+ 鎴栫
+ </view>
+ </view>
+ <view class="approver-list">
+ <view v-for="(approver, aIdx) in node.approvers || []"
+ :key="approver.id || aIdx"
+ class="approver-chip">
+ <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view>
+ <text class="approver-name">{{ approver.approverName || "-" }}</text>
+ </view>
+ <text v-if="!(node.approvers || []).length"
+ class="empty-hint inline">鏆傛棤瀹℃壒浜�</text>
+ </view>
+ </view>
+ <view v-if="nodeIndex < detail.nodes.length - 1"
+ class="flow-connector">
+ <view class="flow-connector-line" />
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤瀹℃壒鑺傜偣</view>
+ </view>
+ </template>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="data"
+ text="鏈幏鍙栧埌妯℃澘璇︽儏" />
+ </view>
+ </scroll-view>
+
+ <FooterButtons v-if="!loading && detail"
+ cancel-text="鍙栨秷"
+ :confirm-text="confirmText"
+ :loading="submitting"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+
+ <up-popup :show="showDatePicker"
+ mode="bottom"
+ @close="showDatePicker = false">
+ <up-datetime-picker :show="true"
+ v-model="datePickerTs"
+ :mode="datePickerMode"
+ @confirm="onDateConfirm"
+ @cancel="onDatePickerCancel" />
+ </up-popup>
+
+ <up-action-sheet :show="showSelectSheet"
+ :title="selectSheetTitle"
+ :actions="selectSheetActions"
+ @select="onSelectOption"
+ @close="showSelectSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref, watch } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js";
+ import {
+ saveApprovalInstance,
+ updateApprovalInstance,
+ } from "@/api/oa/approvalInstance.js";
+ import useUserStore from "@/store/modules/user";
+ import { parseTime } from "@/utils/ruoyi";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess.js";
+ import { findPostOptions } from "@/api/system/post.js";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+ import {
+ computeLeaveDurationDisplay,
+ computeOvertimeHoursDisplay,
+ displayTemplateFieldsByModule,
+ findApplicantTemplateField,
+ findLeaveTimeTemplateField,
+ findOvertimeTimeTemplateField,
+ inferModuleKeyFromRow,
+ loadModuleExtrasFromRow,
+ resolveOriginalPostName,
+ syncModuleExtrasToFormValues,
+ unwrapUserArray,
+ userById,
+ validateModuleExtras,
+ buildPostIdToNameMap,
+ } from "../../_utils/approvalModuleApplyExtras.js";
+ import {
+ formatDatetimerangeDisplay,
+ formatFieldDateValue,
+ formatFieldDisplayValue,
+ getDatePickerMode,
+ getFieldInitialValue,
+ getFieldOptionLabel,
+ isDatetimerangeField,
+ isDateLikeField,
+ isNumberField,
+ isSelectField,
+ isTextareaField,
+ joinDatetimerangeValue,
+ mergeFormConfigForEdit,
+ parseDatetimerangeValue,
+ resolveFieldOptions,
+ parseApprovalFormConfig,
+ parseFieldDateToTs,
+ } from "../../_utils/approvalFormField.js";
+
+ import { EDIT_STORAGE_KEY } from "../../_utils/approveListUtils.js";
+
+ const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+ const userStore = useUserStore();
+ const moduleKey = ref("");
+ const templateId = ref("");
+ const instanceId = ref("");
+ const instanceRow = ref(null);
+ const detail = ref(null);
+ const loading = ref(false);
+ const submitting = ref(false);
+ const formValues = reactive({});
+ const form = reactive({ title: "" });
+ const extraForm = reactive({
+ leaveBalanceDays: undefined,
+ originalPostName: "",
+ });
+ const postIdToName = ref({});
+ const transferUserPool = ref([]);
+
+ const isLeaveModule = computed(
+ () => moduleKey.value === APPROVAL_MODULE_KEYS.LEAVE
+ );
+ const isOvertimeModule = computed(
+ () => moduleKey.value === APPROVAL_MODULE_KEYS.OVERTIME
+ );
+ const isTransferModule = computed(
+ () => moduleKey.value === APPROVAL_MODULE_KEYS.TRANSFER
+ );
+
+ const showDatePicker = ref(false);
+ const datePickerTs = ref(Date.now());
+ const activeDateField = ref(null);
+ const activeRangePart = ref("start");
+
+ const datePickerMode = computed(() => {
+ const field = activeDateField.value;
+ if (!field) return "date";
+ if (isDatetimerangeField(field)) return "datetime";
+ return getDatePickerMode(field);
+ });
+
+ const showSelectSheet = ref(false);
+ const activeSelectField = ref(null);
+ const pickerUserList = ref([]);
+ const pickerDeptList = ref([]);
+
+ const isEditMode = computed(() => !!instanceId.value);
+
+ const pageTitle = computed(() => (isEditMode.value ? "淇敼瀹℃壒" : "鍙戣捣瀹℃壒"));
+ const confirmText = computed(() => (isEditMode.value ? "淇濆瓨淇敼" : "鎻愪氦瀹℃壒"));
+
+ const applicantName = computed(
+ () => userStore.nickName || userStore.name || "-"
+ );
+
+ const displayApplicantName = computed(
+ () => instanceRow.value?.applicantName || applicantName.value
+ );
+
+ const templateName = computed(
+ () => detail.value?.templateName || instanceRow.value?.templateName || "-"
+ );
+
+ const formConfigData = computed(() => {
+ if (isEditMode.value) {
+ return mergeFormConfigForEdit(
+ detail.value?.formConfig,
+ instanceRow.value?.formConfig
+ );
+ }
+ return parseApprovalFormConfig(detail.value?.formConfig);
+ });
+
+ const displayTemplateFields = computed(() =>
+ displayTemplateFieldsByModule(moduleKey.value, formConfigData.value.fields)
+ );
+
+ const leaveDurationText = computed(() => {
+ if (!isLeaveModule.value) return "";
+ const fields = formConfigData.value.fields;
+ const timeField = findLeaveTimeTemplateField(fields);
+ if (timeField?.key) void formValues[timeField.key];
+ return computeLeaveDurationDisplay(fields, formValues);
+ });
+
+ const overtimeHoursText = computed(() => {
+ if (!isOvertimeModule.value) return "";
+ const fields = formConfigData.value.fields;
+ const timeField = findOvertimeTimeTemplateField(fields);
+ if (timeField?.key) void formValues[timeField.key];
+ return computeOvertimeHoursDisplay(fields, formValues);
+ });
+
+ const applicantPickerValue = computed(() => {
+ const f = findApplicantTemplateField(formConfigData.value.fields);
+ return f?.key ? formValues[f.key] : undefined;
+ });
+
+ const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
+
+ const selectSheetTitle = computed(
+ () => (activeSelectField.value?.label ? `閫夋嫨${activeSelectField.value.label}` : "璇烽�夋嫨")
+ );
+
+ const selectSheetActions = computed(() => {
+ const field = activeSelectField.value;
+ if (!field) return [];
+ return resolveFieldOptions(field, {
+ users: pickerUserList.value,
+ depts: pickerDeptList.value,
+ }).map(opt => ({
+ name: opt.label,
+ value: opt.value,
+ }));
+ });
+
+ const formItemClass = field => {
+ if (isTextareaField(field)) return "form-item-textarea";
+ if (isDatetimerangeField(field)) return "form-item-daterange";
+ if (isSelectField(field) || isDateLikeField(field)) return "form-item-select";
+ return "form-item-inline";
+ };
+
+ /** 澶氳鏂囨湰銆佹棩鏈熻寖鍥达細鏍囩缃《锛岄伩鍏嶉暱鏂囨鍦ㄧ獎鍒楀唴鏂 */
+ const formItemLabelPosition = field => {
+ if (isTextareaField(field) || isDatetimerangeField(field)) return "top";
+ return "left";
+ };
+
+ const getRangePartDisplay = (field, part) => {
+ const parts = parseDatetimerangeValue(formValues[field.key]);
+ const val = part === "start" ? parts.start : parts.end;
+ return val ? formatFieldDisplayValue({ type: "datetime" }, val) : "";
+ };
+
+ const openRangePicker = (field, part) => {
+ activeDateField.value = field;
+ activeRangePart.value = part;
+ const parts = parseDatetimerangeValue(formValues[field.key]);
+ const val = part === "start" ? parts.start : parts.end;
+ datePickerTs.value = parseFieldDateToTs(val) ?? Date.now();
+ showDatePicker.value = true;
+ };
+
+ const getSelectDisplayText = field => {
+ const stored = formValues[field.key];
+ const options = resolveFieldOptions(field, {
+ users: pickerUserList.value,
+ depts: pickerDeptList.value,
+ });
+ const matched = options.find(
+ opt =>
+ String(opt.value) === String(stored) || String(opt.label) === String(stored)
+ );
+ return (
+ matched?.label ||
+ getFieldOptionLabel(field, stored) ||
+ (stored !== undefined && stored !== null ? String(stored) : "")
+ );
+ };
+
+ const initFormValues = fields => {
+ Object.keys(formValues).forEach(key => {
+ delete formValues[key];
+ });
+ fields.forEach(field => {
+ if (!field?.key) return;
+ formValues[field.key] = getFieldInitialValue(field);
+ });
+ };
+
+ const openSelectPicker = field => {
+ const options = resolveFieldOptions(field, {
+ users: pickerUserList.value,
+ depts: pickerDeptList.value,
+ });
+ if (!options.length) {
+ uni.showToast({ title: "璇ュ瓧娈垫湭閰嶇疆涓嬫媺閫夐」", icon: "none" });
+ return;
+ }
+ activeSelectField.value = field;
+ showSelectSheet.value = true;
+ };
+
+ const onSelectOption = action => {
+ const key = activeSelectField.value?.key;
+ if (key) {
+ formValues[key] = action.value;
+ }
+ showSelectSheet.value = false;
+ activeSelectField.value = null;
+ };
+
+ const openDatePicker = field => {
+ activeDateField.value = field;
+ const current = formValues[field.key];
+ datePickerTs.value = parseFieldDateToTs(current) ?? Date.now();
+ showDatePicker.value = true;
+ };
+
+ const onDatePickerCancel = () => {
+ showDatePicker.value = false;
+ activeDateField.value = null;
+ };
+
+ const onDateConfirm = e => {
+ const ts = e?.value ?? datePickerTs.value;
+ const field = activeDateField.value;
+ if (field?.key) {
+ if (isDatetimerangeField(field)) {
+ const parts = parseDatetimerangeValue(formValues[field.key]);
+ const formatted = formatFieldDateValue({ type: "datetime" }, ts);
+ formValues[field.key] = joinDatetimerangeValue(
+ activeRangePart.value === "start" ? formatted : parts.start,
+ activeRangePart.value === "end" ? formatted : parts.end
+ );
+ } else {
+ formValues[field.key] = formatFieldDateValue(field, ts);
+ }
+ }
+ onDatePickerCancel();
+ };
+
+ const validateForm = () => {
+ if (!form.title?.trim()) {
+ uni.showToast({ title: "璇疯緭鍏ュ鎵规爣棰�", icon: "none" });
+ return false;
+ }
+ for (const field of displayTemplateFields.value) {
+ if (!field.required) continue;
+ const val = formValues[field.key];
+ if (val === undefined || val === null || String(val).trim() === "") {
+ const action =
+ isSelectField(field) || isDateLikeField(field) || isDatetimerangeField(field)
+ ? "璇烽�夋嫨"
+ : "璇峰~鍐�";
+ uni.showToast({ title: `${action}${field.label}`, icon: "none" });
+ return false;
+ }
+ if (isDatetimerangeField(field)) {
+ const { start, end } = parseDatetimerangeValue(val);
+ if (!start || !end) {
+ uni.showToast({ title: `璇峰畬鏁撮�夋嫨${field.label}`, icon: "none" });
+ return false;
+ }
+ }
+ if (isSelectField(field)) {
+ const options = resolveFieldOptions(field, {
+ users: pickerUserList.value,
+ depts: pickerDeptList.value,
+ });
+ if (
+ options.length &&
+ !options.some(
+ opt =>
+ String(opt.value) === String(val) || String(opt.label) === String(val)
+ )
+ ) {
+ uni.showToast({ title: `${field.label}閫夐」鏃犳晥`, icon: "none" });
+ return false;
+ }
+ }
+ }
+ if (!detail.value?.nodes?.length) {
+ uni.showToast({ title: "妯℃澘鏈厤缃鎵规祦绋�", icon: "none" });
+ return false;
+ }
+ const moduleMsg = validateModuleExtras(
+ moduleKey.value,
+ formConfigData.value.fields,
+ formValues,
+ extraForm
+ );
+ if (moduleMsg) {
+ uni.showToast({ title: moduleMsg, icon: "none" });
+ return false;
+ }
+ return true;
+ };
+
+ const buildFormConfigPayload = () => {
+ syncModuleExtrasToFormValues(
+ moduleKey.value,
+ formValues,
+ extraForm,
+ formConfigData.value.fields
+ );
+ const allFields = formConfigData.value.fields || [];
+ return JSON.stringify({
+ prompt: formConfigData.value.prompt,
+ fields: allFields.map(field => ({
+ ...field,
+ value: formValues[field.key] ?? "",
+ })),
+ });
+ };
+
+ const buildSavePayload = () => ({
+ templateId: detail.value.id,
+ templateName: detail.value.templateName,
+ businessType: detail.value.businessType,
+ title: form.title.trim(),
+ status: "PENDING",
+ currentLevel: 1,
+ applicantId: userStore.id,
+ applicantName: applicantName.value,
+ applyTime: parseTime(new Date()),
+ deptId: userStore.currentDeptId || undefined,
+ formConfig: buildFormConfigPayload(),
+ });
+
+ const buildUpdatePayload = () => {
+ const row = instanceRow.value || {};
+ return {
+ id: instanceId.value,
+ instanceNo: row.instanceNo,
+ templateId: row.templateId ?? detail.value?.id,
+ templateName: row.templateName ?? detail.value?.templateName,
+ businessId: row.businessId,
+ businessType: row.businessType ?? detail.value?.businessType,
+ title: form.title.trim(),
+ status: row.status || "PENDING",
+ currentLevel: row.currentLevel,
+ applicantId: row.applicantId,
+ applicantName: row.applicantName,
+ applyTime: row.applyTime,
+ finishTime: row.finishTime,
+ createUser: row.createUser,
+ createTime: row.createTime,
+ updateUser: row.updateUser,
+ updateTime: row.updateTime,
+ deptId: row.deptId,
+ deleted: row.deleted,
+ formConfig: buildFormConfigPayload(),
+ approveAction: row.approveAction,
+ approveComment: row.approveComment,
+ };
+ };
+
+ const handleSubmit = () => {
+ if (!validateForm() || submitting.value) return;
+
+ submitting.value = true;
+ const submitApi = isEditMode.value
+ ? updateApprovalInstance
+ : saveApprovalInstance;
+ const payload = isEditMode.value ? buildUpdatePayload() : buildSavePayload();
+
+ submitApi(payload)
+ .then(() => {
+ uni.showToast({
+ title: isEditMode.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛",
+ icon: "success",
+ });
+ if (isEditMode.value) {
+ uni.removeStorageSync(EDIT_STORAGE_KEY);
+ }
+ setTimeout(() => {
+ uni.navigateBack({ delta: isEditMode.value ? 1 : 2 });
+ }, 300);
+ })
+ .catch(() => {
+ uni.showToast({
+ title: isEditMode.value ? "淇敼澶辫触" : "鎻愪氦澶辫触",
+ icon: "none",
+ });
+ })
+ .finally(() => {
+ submitting.value = false;
+ });
+ };
+
+ const loadTemplateDetail = () => {
+ if (!templateId.value) return Promise.resolve();
+ return getApprovalTemplateDetail(templateId.value)
+ .then(res => {
+ detail.value = res?.data || null;
+ if (!detail.value) {
+ uni.showToast({ title: "鏈幏鍙栧埌妯℃澘璇︽儏", icon: "none" });
+ }
+ return detail.value;
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇妯℃澘璇︽儏澶辫触", icon: "none" });
+ return null;
+ });
+ };
+
+ const loadForCreate = async () => {
+ loading.value = true;
+ detail.value = null;
+ try {
+ await loadTemplateDetail();
+ if (!detail.value) return;
+ initFormValues(displayTemplateFields.value);
+ resetModuleExtras();
+ if (!form.title && detail.value.templateName) {
+ form.title = `${detail.value.templateName}鐢宠`;
+ }
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const loadForEdit = async () => {
+ const row = uni.getStorageSync(EDIT_STORAGE_KEY);
+ if (!row || String(row.id) !== String(instanceId.value)) {
+ uni.showToast({ title: "鏈幏鍙栧埌瀹℃壒鏁版嵁", icon: "none" });
+ setTimeout(() => uni.navigateBack(), 500);
+ return;
+ }
+ instanceRow.value = row;
+ if (!moduleKey.value) {
+ moduleKey.value = inferModuleKeyFromRow(row);
+ }
+ templateId.value = row.templateId;
+ form.title = row.title || "";
+
+ loading.value = true;
+ detail.value = null;
+ try {
+ await loadTemplateDetail();
+ if (!detail.value) return;
+ initFormValues(displayTemplateFields.value);
+ applyModuleExtrasFromRow();
+ if (isTransferModule.value) {
+ await ensureTransferLookupData();
+ syncOriginalPostFromApplicant(applicantPickerValue.value);
+ }
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ function resetModuleExtras() {
+ extraForm.leaveBalanceDays = undefined;
+ extraForm.originalPostName = "";
+ }
+
+ function applyModuleExtrasFromRow() {
+ const loaded = loadModuleExtrasFromRow(
+ moduleKey.value,
+ instanceRow.value,
+ formValues
+ );
+ if (loaded.leaveBalanceDays != null) {
+ extraForm.leaveBalanceDays = loaded.leaveBalanceDays;
+ }
+ if (loaded.originalPostName) {
+ extraForm.originalPostName = loaded.originalPostName;
+ }
+ }
+
+ async function ensureTransferLookupData() {
+ if (!transferUserPool.value.length) {
+ try {
+ const res = await userListNoPageByTenantId();
+ transferUserPool.value = unwrapUserArray(res);
+ } catch {
+ transferUserPool.value = [];
+ }
+ }
+ if (!Object.keys(postIdToName.value).length) {
+ try {
+ const res = await findPostOptions();
+ const rows = res?.data ?? res?.rows ?? [];
+ postIdToName.value = buildPostIdToNameMap(Array.isArray(rows) ? rows : []);
+ } catch {
+ postIdToName.value = {};
+ }
+ }
+ }
+
+ function syncOriginalPostFromApplicant(uid) {
+ if (!isTransferModule.value) return;
+ if (!uid) {
+ extraForm.originalPostName = "";
+ return;
+ }
+ const user = userById(transferUserPool.value, uid);
+ extraForm.originalPostName = resolveOriginalPostName(user, postIdToName.value);
+ }
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const loadPickerSourceData = () => {
+ userListNoPageByTenantId()
+ .then(res => {
+ pickerUserList.value = res?.data || [];
+ })
+ .catch(() => {
+ pickerUserList.value = [];
+ });
+ getDept()
+ .then(res => {
+ pickerDeptList.value = res?.data || [];
+ })
+ .catch(() => {
+ pickerDeptList.value = [];
+ });
+ };
+
+ watch(applicantPickerValue, async uid => {
+ if (!isTransferModule.value) return;
+ await ensureTransferLookupData();
+ syncOriginalPostFromApplicant(uid);
+ });
+
+ onLoad(options => {
+ moduleKey.value = options?.moduleKey || "";
+ loadPickerSourceData();
+ if (isTransferModule.value) {
+ ensureTransferLookupData();
+ }
+ if (options?.id) {
+ instanceId.value = options.id;
+ loadForEdit();
+ return;
+ }
+ if (options?.templateId) {
+ templateId.value = options.templateId;
+ loadForCreate();
+ return;
+ }
+ uni.showToast({ title: "缂哄皯椤甸潰鍙傛暟", icon: "none" });
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ $primary: #2979ff;
+ $text: #1f2d3d;
+ $text-secondary: #606266;
+ $text-muted: #909399;
+ $bg-page: #f0f3f8;
+ $radius-lg: 12px;
+ $radius-md: 10px;
+ $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05);
+
+ .approve-apply-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background: $bg-page;
+ }
+
+ .form-scroll {
+ flex: 1;
+ height: 0;
+ padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
+ }
+
+ .loading-wrap {
+ padding: 48px 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .loading-text {
+ font-size: 14px;
+ color: $text-muted;
+ }
+
+ .form-section {
+ margin-bottom: 10px;
+ border-radius: $radius-lg;
+ overflow: hidden;
+ box-shadow: $shadow-card;
+ }
+
+ :deep(.form-section .u-cell-group__title) {
+ padding: 12px 16px 8px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: $text !important;
+ background: #fff !important;
+ }
+
+ :deep(.form-section .u-form-item) {
+ padding: 0 16px !important;
+ }
+
+ :deep(.form-section .u-form-item__body) {
+ padding: 10px 0 !important;
+ min-height: auto !important;
+ }
+
+ :deep(.form-item-name .u-form-item__body) {
+ flex-direction: row !important;
+ align-items: center !important;
+ }
+
+ :deep(.form-item-name .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ justify-content: flex-end !important;
+ }
+
+ :deep(.name-input-inline),
+ :deep(.name-input-inline .u-input__content) {
+ width: 100% !important;
+ flex: 1 !important;
+ }
+
+ :deep(.name-input-inline input),
+ :deep(.name-input-inline .u-input__content__field-wrapper__field) {
+ width: 100% !important;
+ text-align: right !important;
+ font-size: 15px !important;
+ }
+
+ :deep(.form-item-readonly .u-form-item__body) {
+ align-items: center !important;
+ }
+
+ :deep(.form-item-readonly .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ justify-content: flex-end !important;
+ }
+
+ :deep(.form-item-readonly .u-input__content__field-wrapper__field) {
+ text-align: right !important;
+ color: #303133 !important;
+ }
+
+ .dynamic-form {
+ padding: 0 0 4px;
+ }
+
+ :deep(.dynamic-form .u-form-item) {
+ padding: 0 16px !important;
+ }
+
+ :deep(.dynamic-form .u-form-item__body) {
+ padding: 10px 0 !important;
+ min-height: auto !important;
+ }
+
+ :deep(.form-item-inline .u-form-item__body) {
+ flex-direction: row !important;
+ align-items: center !important;
+ }
+
+ :deep(.form-item-inline .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ justify-content: flex-end !important;
+ }
+
+ :deep(.form-item-inline input),
+ :deep(.form-item-inline .u-input__content__field-wrapper__field) {
+ text-align: right !important;
+ font-size: 15px !important;
+ }
+
+ :deep(.form-item-select .u-form-item__body) {
+ align-items: center !important;
+ }
+
+ :deep(.form-item-select .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ justify-content: flex-end !important;
+ }
+
+ :deep(.form-item-textarea .u-form-item__body),
+ :deep(.form-item-daterange .u-form-item__body) {
+ flex-direction: column !important;
+ align-items: stretch !important;
+ padding: 10px 0 12px !important;
+ }
+
+ :deep(.form-item-textarea .u-form-item__body__left),
+ :deep(.form-item-daterange .u-form-item__body__left) {
+ width: 100% !important;
+ max-width: 100% !important;
+ margin-bottom: 8px !important;
+ padding-right: 0 !important;
+ }
+
+ :deep(.form-item-textarea .u-form-item__body__left__content__label),
+ :deep(.form-item-daterange .u-form-item__body__left__content__label) {
+ white-space: normal !important;
+ line-height: 1.45 !important;
+ font-size: 14px !important;
+ }
+
+ :deep(.form-item-textarea .u-form-item__body__right),
+ :deep(.form-item-daterange .u-form-item__body__right) {
+ width: 100% !important;
+ flex: none !important;
+ }
+
+ :deep(.form-item-textarea .u-form-item__content),
+ :deep(.form-item-daterange .u-form-item__content) {
+ width: 100% !important;
+ justify-content: stretch !important;
+ }
+
+ :deep(.dynamic-form .u-form-item__body__left__content__label) {
+ white-space: nowrap;
+ }
+
+ .field-trigger {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 6px;
+ width: 100%;
+ min-width: 0;
+ }
+
+ :deep(.field-trigger .u-input) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ }
+
+ :deep(.field-trigger .u-input__content__field-wrapper__field) {
+ text-align: right !important;
+ font-size: 15px !important;
+ }
+
+ .daterange-fill {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .range-fill-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ background: #f7f9fc;
+ border: 1px solid #eef1f6;
+ border-radius: 8px;
+ }
+
+ .range-fill-label {
+ flex-shrink: 0;
+ width: 36px;
+ font-size: 13px;
+ color: #909399;
+ }
+
+ .range-fill-sep {
+ font-size: 12px;
+ color: #c0c4cc;
+ text-align: center;
+ }
+
+ :deep(.range-fill-row .u-input) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ }
+
+ .section-card {
+ margin-bottom: 10px;
+ background: #fff;
+ border-radius: $radius-lg;
+ overflow: hidden;
+ box-shadow: $shadow-card;
+ }
+
+ .section-head {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f7;
+ }
+
+ .section-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: $text;
+ padding-left: 10px;
+ border-left: 3px solid $primary;
+ line-height: 1.2;
+ }
+
+ .form-prompt {
+ margin: 12px 16px 0;
+ padding: 10px 12px;
+ font-size: 13px;
+ color: $text-secondary;
+ background: #f8fafc;
+ border-radius: 8px;
+ line-height: 1.5;
+ }
+
+ .flow-wrap {
+ padding: 10px 16px 14px;
+ }
+
+ .flow-node-block {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .flow-node-card {
+ background: #fafbfd;
+ border: 1px solid #e8eef5;
+ border-radius: $radius-md;
+ padding: 12px;
+ }
+
+ .node-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 10px;
+ }
+
+ .node-level-badge {
+ width: 26px;
+ height: 26px;
+ border-radius: 8px;
+ background: $primary;
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ .node-level-text {
+ flex: 1;
+ font-size: 15px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .approve-type-row {
+ display: flex;
+ background: #f0f3f8;
+ border-radius: 8px;
+ padding: 3px;
+ margin-bottom: 10px;
+
+ &--readonly {
+ pointer-events: none;
+ }
+ }
+
+ .type-btn {
+ flex: 1;
+ text-align: center;
+ padding: 8px 0;
+ font-size: 14px;
+ color: $text-secondary;
+ border-radius: 6px;
+
+ &.active {
+ background: #fff;
+ color: $primary;
+ font-weight: 500;
+ }
+ }
+
+ .approver-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+ }
+
+ .approver-chip {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 12px 6px 6px;
+ background: #fff;
+ border: 1px solid #dce8f8;
+ border-radius: 24px;
+ box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06);
+ }
+
+ .approver-avatar {
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .approver-name {
+ font-size: 13px;
+ color: $text;
+ max-width: 120px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .flow-connector {
+ display: flex;
+ justify-content: center;
+ padding: 4px 0;
+ }
+
+ .flow-connector-line {
+ width: 2px;
+ height: 14px;
+ background: #d0dff0;
+ }
+
+ .empty-hint {
+ padding: 12px 16px 16px;
+ font-size: 13px;
+ color: $text-muted;
+
+ &.inline {
+ padding: 0;
+ }
+ }
+
+ .empty-wrap {
+ padding: 48px 20px;
+ }
+
+ .module-extra-block {
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px dashed #e8ecf0;
+ }
+
+ .readonly-with-unit {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ justify-content: flex-end;
+ }
+
+ .readonly-with-unit :deep(.u-input) {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .unit-text {
+ flex-shrink: 0;
+ font-size: 14px;
+ color: $text-muted;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/approve.vue b/src/pages/oa/ApproveManage/approve-list/approve.vue
new file mode 100644
index 0000000..9201818
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/approve.vue
@@ -0,0 +1,180 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 瀹℃壒澶勭悊
+ 宸梾/璐圭敤鎶ラ攢浣跨敤鎶ラ攢璇︽儏 + 瀹℃壒鍒楄〃 approve 鎺ュ彛
+-->
+<template>
+ <view class="oa-detail-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <scroll-view v-if="displayReady"
+ class="oa-detail-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <ReimburseInstanceDetailBody v-if="isReimburse"
+ :reimburse-row="reimburseRow"
+ :module-key="detailModuleKey" />
+
+ <ApproveInstanceDetailBody v-else
+ :row="row"
+ :module-key="detailModuleKey" />
+
+ <view class="section-card opinion-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒鎰忚</text>
+ </view>
+ <view class="opinion-wrap">
+ <up-textarea v-model="approveOpinion"
+ placeholder="閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏叿浣撳師鍥�"
+ maxlength="500"
+ count
+ height="100"
+ border="surround" />
+ </view>
+ </view>
+ </scroll-view>
+
+ <view v-else
+ class="oa-empty">
+ <up-empty mode="data"
+ :text="loading ? '鍔犺浇涓�' : '鏈幏鍙栧埌瀹℃壒鏁版嵁'" />
+ </view>
+
+ <view v-if="displayReady"
+ class="oa-page-footer">
+ <text class="oa-footer-btn btn-default"
+ :class="{ 'is-disabled': submitting }"
+ @click="goBack">鍙栨秷</text>
+ <text class="oa-footer-btn btn-danger"
+ :class="{ 'is-disabled': submitting }"
+ @click="submitApprove('rejected')">椹冲洖</text>
+ <text class="oa-footer-btn btn-success"
+ :class="{ 'is-disabled': submitting }"
+ @click="submitApprove('approved')">閫氳繃</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ApproveInstanceDetailBody from "./_components/ApproveInstanceDetailBody.vue";
+ import ReimburseInstanceDetailBody from "../../ReimburseManage/_components/ReimburseInstanceDetailBody.vue";
+ import { approveApprovalInstance } from "@/api/oa/approvalInstance.js";
+ import {
+ buildApproveInstanceDto,
+ canApproveInstance,
+ loadInstanceRow,
+ } from "../../_utils/approveListUtils.js";
+ import { inferModuleKeyFromRow } from "../../_utils/approvalModuleApplyExtras.js";
+ import {
+ inferReimburseModuleKeyFromInstance,
+ isReimburseApprovalInstance,
+ loadReimburseDetailForInstance,
+ } from "../../_utils/reimburseApproveBridge.js";
+ import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js";
+
+ const row = ref(null);
+ const reimburseRow = ref(null);
+ const loading = ref(false);
+ const approveOpinion = ref("");
+ const submitting = ref(false);
+
+ const isReimburse = computed(() => isReimburseApprovalInstance(row.value));
+
+ const detailModuleKey = computed(() => {
+ if (isReimburse.value) {
+ return (
+ reimburseRow.value?.moduleKey ||
+ inferReimburseModuleKeyFromInstance(row.value)
+ );
+ }
+ return inferModuleKeyFromRow(row.value);
+ });
+
+ const pageTitle = computed(() => {
+ if (isReimburse.value) {
+ const label = getApprovalModuleConfig(detailModuleKey.value)?.label || "鎶ラ攢";
+ return `${label}瀹℃壒`;
+ }
+ return "瀹℃壒澶勭悊";
+ });
+
+ const displayReady = computed(() =>
+ isReimburse.value ? Boolean(reimburseRow.value) : Boolean(row.value)
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const submitApprove = uiResult => {
+ if (!row.value?.id || submitting.value) return;
+
+ if (uiResult === "rejected" && !(approveOpinion.value || "").trim()) {
+ uni.showToast({ title: "椹冲洖鏃惰濉啓瀹℃壒鎰忚", icon: "none" });
+ return;
+ }
+
+ submitting.value = true;
+ const dto = buildApproveInstanceDto(
+ row.value.id,
+ uiResult,
+ approveOpinion.value
+ );
+
+ approveApprovalInstance(dto)
+ .then(() => {
+ uni.showToast({
+ title: uiResult === "approved" ? "宸查�氳繃" : "宸查┏鍥�",
+ icon: "success",
+ });
+ setTimeout(() => {
+ const pages = getCurrentPages();
+ const prevRoute = pages[pages.length - 2]?.route || "";
+ const delta = prevRoute.includes("approve-list/detail") ? 2 : 1;
+ uni.navigateBack({ delta });
+ }, 400);
+ })
+ .catch(() => {
+ uni.showToast({ title: "瀹℃壒鎿嶄綔澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ submitting.value = false;
+ });
+ };
+
+ onLoad(async options => {
+ if (!options?.id) {
+ uni.showToast({ title: "缂哄皯瀹℃壒 ID", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ const cached = loadInstanceRow(options.id);
+ if (!cached) {
+ uni.showToast({ title: "璇蜂粠鍒楄〃杩涘叆", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ if (!canApproveInstance(cached)) {
+ uni.showToast({ title: "褰撳墠瀹℃壒鏃犻渶鎮ㄥ鐞�", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ row.value = cached;
+ if (isReimburseApprovalInstance(cached)) {
+ loading.value = true;
+ try {
+ const { reimburseRow: mapped } = await loadReimburseDetailForInstance(cached);
+ reimburseRow.value = mapped;
+ } catch {
+ uni.showToast({ title: "鍔犺浇鎶ラ攢璇︽儏澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "../../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/detail.vue b/src/pages/oa/ApproveManage/approve-list/detail.vue
new file mode 100644
index 0000000..9c38634
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/detail.vue
@@ -0,0 +1,174 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 瀹℃壒璇︽儏
+ 璺敱锛�/pages/oa/ApproveManage/approve-list/detail
+-->
+<template>
+ <view class="oa-detail-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <scroll-view v-if="displayReady"
+ class="oa-detail-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <ReimburseInstanceDetailBody v-if="isReimburse"
+ :reimburse-row="reimburseRow"
+ :module-key="detailModuleKey" />
+ <ApproveInstanceDetailBody v-else
+ :row="row"
+ :module-key="detailModuleKey" />
+ </scroll-view>
+
+ <view v-else
+ class="oa-empty">
+ <up-empty mode="data"
+ :text="loading ? '鍔犺浇涓�' : '鏈幏鍙栧埌瀹℃壒鏁版嵁'" />
+ </view>
+
+ <view v-if="displayReady"
+ class="oa-page-footer">
+ <text class="oa-footer-btn btn-default"
+ @click="goBack">杩斿洖</text>
+ <text v-if="showEdit"
+ class="oa-footer-btn btn-warn"
+ @click="goEdit">淇敼</text>
+ <text v-if="showApprove && !fromBusiness"
+ class="oa-footer-btn btn-primary"
+ @click="goApprove">鍘诲鎵�</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ApproveInstanceDetailBody from "./_components/ApproveInstanceDetailBody.vue";
+ import ReimburseInstanceDetailBody from "../../ReimburseManage/_components/ReimburseInstanceDetailBody.vue";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import useUserStore from "@/store/modules/user";
+ import {
+ canApproveInstance,
+ canEditBusinessInstanceRow,
+ canModifyInstance,
+ loadInstanceRow,
+ stashInstanceRow,
+ } from "../../_utils/approveListUtils.js";
+ import { inferModuleKeyFromRow } from "../../_utils/approvalModuleApplyExtras.js";
+ import {
+ inferReimburseModuleKeyFromInstance,
+ isReimburseApprovalInstance,
+ loadReimburseDetailForInstance,
+ resolveFinReimbursementIdFromInstance,
+ stashReimburseEditFromApprove,
+ } from "../../_utils/reimburseApproveBridge.js";
+ import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js";
+ import { canEditReimbursementRow } from "../../_utils/finReimbursementMappers.js";
+
+ const userStore = useUserStore();
+ const fromBusiness = ref(false);
+ const row = ref(null);
+ const reimburseRow = ref(null);
+ const loading = ref(false);
+
+ const detailModuleKey = computed(() => {
+ if (isReimburse.value) {
+ return (
+ reimburseRow.value?.moduleKey ||
+ inferReimburseModuleKeyFromInstance(row.value)
+ );
+ }
+ return inferModuleKeyFromRow(row.value);
+ });
+
+ const isReimburse = computed(() => isReimburseApprovalInstance(row.value));
+
+ const pageTitle = computed(() => {
+ if (isReimburse.value) {
+ return getApprovalModuleConfig(detailModuleKey.value)?.label
+ ? `${getApprovalModuleConfig(detailModuleKey.value).label}璇︽儏`
+ : "鎶ラ攢璇︽儏";
+ }
+ return "瀹℃壒璇︽儏";
+ });
+
+ const displayReady = computed(() =>
+ isReimburse.value ? Boolean(reimburseRow.value) : Boolean(row.value)
+ );
+
+ const showEdit = computed(() => {
+ if (isReimburse.value) {
+ return canEditReimbursementRow(reimburseRow.value);
+ }
+ if (fromBusiness.value) {
+ return canEditBusinessInstanceRow(row.value);
+ }
+ return canModifyInstance(row.value, userStore);
+ });
+
+ const showApprove = computed(() => canApproveInstance(row.value));
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ if (!showEdit.value) return;
+ if (isReimburse.value) {
+ const mk = detailModuleKey.value;
+ const rid = resolveFinReimbursementIdFromInstance(row.value);
+ if (rid == null) {
+ uni.showToast({ title: "鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID", icon: "none" });
+ return;
+ }
+ stashReimburseEditFromApprove(mk, rid);
+ uni.navigateTo({
+ url: `${OA_NAV.reimburseForm}?moduleKey=${mk}&mode=edit&reimbursementId=${rid}`,
+ });
+ return;
+ }
+ if (!row.value?.id) return;
+ const mk = detailModuleKey.value;
+ const q = mk ? `&moduleKey=${mk}` : "";
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApply}?id=${row.value.id}${q}`,
+ });
+ };
+
+ const goApprove = () => {
+ if (!row.value?.id) return;
+ stashInstanceRow(row.value);
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApprove}?id=${row.value.id}`,
+ });
+ };
+
+ onLoad(async options => {
+ fromBusiness.value = options?.from === "business";
+ if (!options?.id) {
+ uni.showToast({ title: "缂哄皯瀹℃壒 ID", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ const cached = loadInstanceRow(options.id);
+ if (!cached) {
+ uni.showToast({ title: "璇蜂粠鍒楄〃杩涘叆璇︽儏", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ row.value = cached;
+ if (isReimburseApprovalInstance(cached)) {
+ loading.value = true;
+ try {
+ const { reimburseRow: mapped } = await loadReimburseDetailForInstance(cached);
+ reimburseRow.value = mapped;
+ } catch {
+ uni.showToast({ title: "鍔犺浇鎶ラ攢璇︽儏澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "../../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/index.vue b/src/pages/oa/ApproveManage/approve-list/index.vue
new file mode 100644
index 0000000..40f9468
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/index.vue
@@ -0,0 +1,282 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 瀹℃壒鍒楄〃
+-->
+<template>
+ <view class="oa-approval-page">
+ <PageHeader title="瀹℃壒鍒楄〃"
+ @back="goBack" />
+
+ <view class="oa-toolbar">
+ <view class="oa-filter-chip active-search">
+ <up-icon name="search"
+ size="18"
+ color="#666" />
+ <up-input v-model="queryParams.keyword"
+ class="chip-input"
+ placeholder="瀹℃壒鏍囬 / 瀹℃壒缂栧彿"
+ clearable
+ border="none"
+ @confirm="handleSearch" />
+ </view>
+ <view class="oa-icon-btn"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="20"
+ color="#2979ff" />
+ </view>
+ </view>
+
+ <scroll-view class="oa-list-scroll"
+ scroll-y
+ :show-scrollbar="false"
+ :style="{ height: listScrollHeight + 'px' }"
+ @scrolltolower="loadMore">
+ <view v-if="list.length"
+ class="oa-card-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="oa-card"
+ @click="openDetail(item)">
+ <view class="oa-card-head">
+ <view class="oa-card-title-wrap">
+ <text class="oa-card-title">{{ item.title || item.instanceNo || "-" }}</text>
+ <text v-if="item.templateName"
+ class="oa-card-sub">{{ item.templateName }}</text>
+ </view>
+ <text :class="['oa-status', businessStatusClass(item.status)]">
+ {{ statusText(item.status) }}
+ </text>
+ </view>
+
+ <view class="oa-card-body">
+ <view class="oa-info-grid">
+ <view class="oa-info-row">
+ <text class="oa-info-label">瀹℃壒缂栧彿</text>
+ <text class="oa-info-value">{{ item.instanceNo || "-" }}</text>
+ </view>
+ <view v-if="item.businessName"
+ class="oa-info-row">
+ <text class="oa-info-label">涓氬姟鍚嶇О</text>
+ <text class="oa-info-value">{{ item.businessName }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠浜�</text>
+ <text class="oa-info-value">{{ item.applicantName || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">褰撳墠瀹℃壒浜�</text>
+ <text class="oa-info-value">{{ currentApproverName(item) }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠鏃堕棿</text>
+ <text class="oa-info-value">{{ formatDateTime(item.applyTime) }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="canModify(item) || item.isApprove"
+ class="oa-card-foot"
+ @click.stop>
+ <text v-if="canModify(item)"
+ class="oa-foot-btn btn-edit"
+ @click="goModify(item)">缂栬緫</text>
+ <text v-if="item.isApprove"
+ class="oa-foot-btn btn-approve"
+ @click="handleApprove(item)">瀹℃壒</text>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="oa-empty">
+ <up-empty mode="list"
+ text="鏆傛棤瀹℃壒鏁版嵁" />
+ </view>
+ </scroll-view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { onMounted, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { listApprovalInstancePage } from "@/api/oa/approvalInstance.js";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import useUserStore from "@/store/modules/user";
+ import { parseTime } from "@/utils/ruoyi";
+ import {
+ businessStatusClass,
+ businessStatusText,
+ canModifyInstance,
+ stashInstanceRow,
+ } from "../../_utils/approveListUtils.js";
+ import {
+ inferReimburseModuleKeyFromInstance,
+ resolveFinReimbursementIdFromInstance,
+ stashReimburseEditFromApprove,
+ } from "../../_utils/reimburseApproveBridge.js";
+
+ const userStore = useUserStore();
+ const queryParams = reactive({ keyword: "" });
+ const list = ref([]);
+ const pageStatus = ref("loadmore");
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const listScrollHeight = ref(400);
+
+ function calcListScrollHeight() {
+ const sys = uni.getSystemInfoSync();
+ const statusBar = sys.statusBarHeight || 0;
+ const navBar = 44;
+ const toolbar = 56;
+ const fabGap = 16;
+ listScrollHeight.value = Math.max(
+ 200,
+ sys.windowHeight - statusBar - navBar - toolbar - fabGap
+ );
+ }
+
+ const statusText = status => businessStatusText(status);
+
+ const formatDateTime = val => {
+ if (!val) return "-";
+ return parseTime(val, "{y}-{m}-{d} {h}:{i}:{s}") || String(val);
+ };
+
+ const canModify = item => canModifyInstance(item, userStore);
+
+ const currentApproverName = item => {
+ const tasks = item?.tasks;
+ if (!Array.isArray(tasks) || !tasks.length) return "-";
+ const pending = tasks.find(t => t.taskStatus === "PENDING");
+ if (pending?.approverName) return pending.approverName;
+ const names = [...new Set(tasks.map(t => t.approverName).filter(Boolean))];
+ return names.length ? names.join("銆�") : "-";
+ };
+
+ const buildListParams = () => {
+ const keyword = queryParams.keyword?.trim();
+ const dto = {};
+ if (keyword) {
+ if (/[\u4e00-\u9fa5]/.test(keyword)) {
+ dto.title = keyword;
+ } else {
+ dto.instanceNo = keyword;
+ }
+ }
+ return { current: page.current, size: page.size, ...dto };
+ };
+
+ const getList = () => {
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+ pageStatus.value = "loading";
+ listApprovalInstancePage(buildListParams())
+ .then(res => {
+ const pageData = res?.data || {};
+ const records = pageData.records || [];
+ const total = pageData.total ?? 0;
+ if (page.current === 1) {
+ list.value = records;
+ } else {
+ list.value = [...list.value, ...records];
+ }
+ page.total = total;
+ if (list.value.length >= total || records.length < page.size) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current += 1;
+ }
+ })
+ .catch(() => {
+ if (page.current === 1) list.value = [];
+ pageStatus.value = "loadmore";
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const loadMore = () => {
+ if (pageStatus.value === "loadmore") getList();
+ };
+
+ const goBack = () => uni.navigateBack();
+ const goAdd = () => uni.navigateTo({ url: OA_NAV.approveListTemplateSelect });
+
+ const openDetail = item => {
+ if (!item?.id) return;
+ stashInstanceRow(item);
+ uni.navigateTo({ url: `${OA_NAV.approveListDetail}?id=${item.id}` });
+ };
+
+ const goModify = item => {
+ if (!canModify(item)) {
+ uni.showToast({ title: "浠呰繘琛屼腑鐨勬湰浜虹敵璇峰彲缂栬緫", icon: "none" });
+ return;
+ }
+ const mk = inferReimburseModuleKeyFromInstance(item);
+ if (mk) {
+ const rid = resolveFinReimbursementIdFromInstance(item);
+ if (rid == null) {
+ uni.showToast({ title: "鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID", icon: "none" });
+ return;
+ }
+ stashReimburseEditFromApprove(mk, rid);
+ uni.navigateTo({
+ url: `${OA_NAV.reimburseForm}?moduleKey=${mk}&mode=edit&reimbursementId=${rid}`,
+ });
+ return;
+ }
+ if (!item?.id) return;
+ stashInstanceRow(item);
+ uni.navigateTo({ url: `${OA_NAV.approveListApply}?id=${item.id}` });
+ };
+
+ const handleApprove = item => {
+ if (!item?.id) return;
+ if (!item.isApprove) {
+ uni.showToast({ title: "褰撳墠瀹℃壒鏃犻渶鎮ㄥ鐞�", icon: "none" });
+ return;
+ }
+ stashInstanceRow(item);
+ uni.navigateTo({ url: `${OA_NAV.approveListApprove}?id=${item.id}` });
+ };
+
+ onMounted(() => calcListScrollHeight());
+
+ onShow(() => {
+ calcListScrollHeight();
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+ @import "../../_styles/oa-approval-list.scss";
+
+ .active-search {
+ padding-right: 4px;
+ }
+
+ .chip-input {
+ flex: 1;
+ font-size: 14px;
+ }
+
+ :deep(.chip-input .u-input__content) {
+ background: transparent !important;
+ padding: 0 !important;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-list/template-select.vue b/src/pages/oa/ApproveManage/approve-list/template-select.vue
new file mode 100644
index 0000000..073b556
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-list/template-select.vue
@@ -0,0 +1,378 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 閫夋嫨瀹℃壒妯℃澘
+ 璺敱锛�/pages/oa/ApproveManage/approve-list/template-select
+ Tab锛歍ypeEnums 鈫� businessType锛涘垪琛細GET /approvalTemplate/list/1锛堣嚜瀹氫箟宸插惎鐢級鍚庢寜 businessType 绛涢��
+-->
+<template>
+ <view class="template-select-page sales-account">
+ <PageHeader :title="pageHeaderTitle"
+ @back="goBack" />
+
+ <view v-if="typeOptions.length && !moduleKey"
+ class="step-section">
+ <view class="tabs-wrap">
+ <up-tabs :list="tabList"
+ :current="activeTab"
+ line-color="#2979ff"
+ @click="onTabClick" />
+ </view>
+ </view>
+
+ <view v-if="useAllTemplatesFallback && allTemplates.length"
+ class="fallback-hint">
+ <text>褰撳墠绫诲瀷涓嬫棤鍖归厤妯℃澘锛屽凡鏄剧ず鍏ㄩ儴 {{ allTemplates.length }} 涓彲鐢ㄦā鏉�</text>
+ </view>
+
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input v-model="keyword"
+ class="search-text"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ clearable />
+ </view>
+ <view class="filter-button">
+ <up-icon name="search"
+ size="24"
+ color="#999" />
+ </view>
+ </view>
+ </view>
+
+ <scroll-view class="list-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view v-if="loading"
+ class="loading-wrap">
+ <up-loading-icon mode="circle" />
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+ <view v-else-if="displayList.length"
+ class="ledger-list">
+ <view v-for="item in displayList"
+ :key="item.id"
+ class="ledger-item ledger-item--clickable"
+ @click="selectTemplate(item)">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff" />
+ </view>
+ <text class="item-id">{{ item.templateName || "-" }}</text>
+ </view>
+ <u-tag :type="enabledTagType(item.enabled)"
+ :text="enabledText(item.enabled)" />
+ </view>
+ <up-divider />
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">瀹℃壒绫诲瀷</text>
+ <text class="detail-value">{{ businessTypeText(item.businessType) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹℃壒鑺傜偣</text>
+ <text class="detail-value">{{ nodeCount(item) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妯℃澘璇存槑</text>
+ <text class="detail-value">{{ item.description || "-" }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="list"
+ :text="emptyText" />
+ </view>
+ </scroll-view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import {
+ buildTypeLabelMap,
+ fetchApprovalTemplateTypes,
+ buildTypeOptionsFromTemplates,
+ FALLBACK_BUSINESS_TYPE_OPTIONS,
+ fetchEnabledApprovalTemplates,
+ filterTemplatesByBusinessType,
+ filterTemplatesByBusinessTypes,
+ getBusinessTypeLabel,
+ pickTabIndexWithTemplates,
+ } from "../../_utils/approvalTemplateType.js";
+ import {
+ getApprovalModuleConfig,
+ getModuleMatchingBusinessTypes,
+ resolveModuleBusinessType,
+ } from "../../_utils/approvalModuleRegistry.js";
+
+ const moduleKey = ref("");
+ const typeOptions = ref([]);
+ const typeLabelMap = ref({});
+ /** 鍏ㄩ儴鑷畾涔夊凡鍚敤妯℃澘锛坙ist/1 涓�娆℃媺鍙栵級 */
+ const allTemplates = ref([]);
+ const activeTab = ref(0);
+ const keyword = ref("");
+ const loading = ref(false);
+
+ const tabList = computed(() =>
+ typeOptions.value.map(opt => ({ name: opt.name }))
+ );
+
+ const moduleConfig = computed(() =>
+ moduleKey.value ? getApprovalModuleConfig(moduleKey.value) : null
+ );
+
+ const pageHeaderTitle = computed(() => {
+ if (moduleConfig.value?.label) {
+ return `閫夋嫨${moduleConfig.value.label}妯℃澘`;
+ }
+ return "閫夋嫨瀹℃壒妯℃澘";
+ });
+
+ const moduleBusinessTypes = computed(() => {
+ if (!moduleKey.value) return [];
+ return getModuleMatchingBusinessTypes(moduleKey.value, typeOptions.value);
+ });
+
+ const currentTypeOption = computed(() => typeOptions.value[activeTab.value]);
+
+ /** 鏃� moduleKey 涓斿綋鍓� Tab 绛涗笉鍒版椂锛屽睍绀哄叏閮ㄦā鏉块伩鍏嶃�屾湁鏁版嵁鍗寸┖鐧姐�� */
+ const useAllTemplatesFallback = computed(() => {
+ if (moduleKey.value) return false;
+ if (!allTemplates.value.length) return false;
+ const businessType = currentTypeOption.value?.value;
+ if (businessType == null || businessType === "") return true;
+ return filterTemplatesByBusinessType(allTemplates.value, businessType).length === 0;
+ });
+
+ const currentSource = computed(() => {
+ if (moduleKey.value && moduleBusinessTypes.value.length) {
+ const filtered = filterTemplatesByBusinessTypes(
+ allTemplates.value,
+ moduleBusinessTypes.value
+ );
+ if (filtered.length) return filtered;
+ return allTemplates.value;
+ }
+ if (useAllTemplatesFallback.value) {
+ return allTemplates.value;
+ }
+ const businessType = currentTypeOption.value?.value;
+ return filterTemplatesByBusinessType(allTemplates.value, businessType);
+ });
+
+ const displayList = computed(() => {
+ const kw = keyword.value?.trim().toLowerCase();
+ if (!kw) return currentSource.value;
+ return currentSource.value.filter(item =>
+ (item.templateName || "").toLowerCase().includes(kw)
+ );
+ });
+
+ const emptyText = computed(() => {
+ if (allTemplates.value.length === 0) {
+ return "鏆傛棤宸插惎鐢ㄧ殑瀹℃壒妯℃澘锛岃鍏堝湪銆屽鎵规ā鏉裤�嶄腑鍒涘缓骞跺惎鐢�";
+ }
+ if (moduleConfig.value?.label) {
+ return `鏆傛棤${moduleConfig.value.label}鍙敤妯℃澘`;
+ }
+ if (useAllTemplatesFallback.value) {
+ return "褰撳墠绫诲瀷涓嬫棤鍖归厤妯℃澘";
+ }
+ const typeName = currentTypeOption.value?.name || "璇ュ鎵圭被鍨�";
+ return `鏆傛棤${typeName}涓嬬殑妯℃澘锛堝彲鍒囨崲涓婃柟绫诲瀷锛塦;
+ });
+
+ const businessTypeText = type =>
+ getBusinessTypeLabel(type, typeLabelMap.value);
+
+ const enabledText = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "鍚敤";
+ if (val === "0") return "鍋滅敤";
+ return "-";
+ };
+
+ const enabledTagType = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "success";
+ if (val === "0") return "info";
+ return "info";
+ };
+
+ const nodeCount = item => {
+ const count = item?.nodes?.length;
+ return count != null ? `${count} 涓猔 : "-";
+ };
+
+ const initPage = async () => {
+ loading.value = true;
+ keyword.value = "";
+ allTemplates.value = [];
+ try {
+ const [opts, templates] = await Promise.all([
+ fetchApprovalTemplateTypes(),
+ fetchEnabledApprovalTemplates(),
+ ]);
+ let resolvedOpts = opts?.length ? opts : buildTypeOptionsFromTemplates(templates);
+ if (!resolvedOpts.length) {
+ resolvedOpts = [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+ }
+ typeOptions.value = resolvedOpts;
+ typeLabelMap.value = buildTypeLabelMap(resolvedOpts);
+ allTemplates.value = templates;
+
+ if (!templates.length) {
+ uni.showToast({
+ title: "鏈幏鍙栧埌宸插惎鐢ㄦā鏉�",
+ icon: "none",
+ duration: 2500,
+ });
+ }
+
+ if (moduleKey.value) {
+ const resolved = resolveModuleBusinessType(moduleKey.value, opts);
+ const idx = opts.findIndex(
+ opt => String(opt.value) === String(resolved)
+ );
+ activeTab.value =
+ idx >= 0 ? idx : pickTabIndexWithTemplates(resolvedOpts, templates);
+ } else {
+ activeTab.value = pickTabIndexWithTemplates(resolvedOpts, templates);
+ }
+ } catch {
+ typeOptions.value = [];
+ typeLabelMap.value = {};
+ allTemplates.value = [];
+ uni.showToast({ title: "鍔犺浇妯℃澘澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const onTabClick = item => {
+ activeTab.value = item?.index ?? 0;
+ keyword.value = "";
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const selectTemplate = item => {
+ if (!item?.id) return;
+ if (String(item.enabled) === "0") {
+ uni.showToast({ title: "璇ユā鏉垮凡鍋滅敤", icon: "none" });
+ return;
+ }
+ const base = `${OA_NAV.approveListApply}?templateId=${item.id}`;
+ uni.navigateTo({
+ url: moduleKey.value ? `${base}&moduleKey=${moduleKey.value}` : base,
+ });
+ };
+
+ onLoad(options => {
+ moduleKey.value = options?.moduleKey || "";
+ initPage();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .template-select-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ }
+
+ .fallback-hint {
+ margin: 8px 12px 0;
+ padding: 8px 12px;
+ font-size: 12px;
+ color: #e6a23c;
+ background: #fdf6ec;
+ border-radius: 6px;
+ line-height: 1.5;
+ }
+
+ .step-section {
+ background: #fff;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .step-label {
+ display: block;
+ padding: 10px 16px 0;
+ font-size: 13px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .step-hint {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ padding: 10px 16px 4px;
+ gap: 8px;
+ }
+
+ .step-desc {
+ flex-shrink: 0;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .tabs-wrap {
+ padding: 0 12px 4px;
+ }
+
+ .list-scroll {
+ flex: 1;
+ height: 0;
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+
+ .loading-wrap {
+ padding: 48px 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .loading-text {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .empty-wrap {
+ padding: 48px 20px;
+ }
+
+ .ledger-item--clickable:active {
+ opacity: 0.92;
+ }
+
+ .card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px dashed #e8ecf0;
+ }
+
+ .card-footer-tip {
+ font-size: 13px;
+ color: #2979ff;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-template/detail.vue b/src/pages/oa/ApproveManage/approve-template/detail.vue
new file mode 100644
index 0000000..40a8958
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-template/detail.vue
@@ -0,0 +1,419 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 瀹℃壒妯℃澘璇︽儏
+ 璺敱锛�/pages/oa/ApproveManage/approve-template/detail
+-->
+<template>
+ <view class="template-detail-page">
+ <PageHeader title="妯℃澘璇︽儏"
+ @back="goBack" />
+
+ <scroll-view class="detail-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view v-if="loading"
+ class="loading-wrap">
+ <up-loading-icon mode="circle" />
+ <text class="loading-text">鍔犺浇涓�...</text>
+ </view>
+ <template v-else-if="detail">
+ <view class="section">
+ <view class="section-title">鍩烘湰淇℃伅</view>
+ <view class="info-list">
+ <view class="info-item">
+ <text class="info-label">妯℃澘鍚嶇О</text>
+ <text class="info-value">{{ detail.templateName || "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">瀹℃壒绫诲瀷</text>
+ <text class="info-value">{{ businessTypeText(detail.businessType) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鍚敤鐘舵��</text>
+ <text class="info-value"
+ :class="enabledClass(detail.enabled)">
+ {{ enabledText(detail.enabled) }}
+ </text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">妯℃澘璇存槑</text>
+ <text class="info-value">{{ detail.description || "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鍒涘缓浜�</text>
+ <text class="info-value">{{ detail.createdUserName || "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鍒涘缓鏃堕棿</text>
+ <text class="info-value">{{ detail.createTime || "-" }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view class="section">
+ <view class="section-title">濉姤閰嶇疆</view>
+ <view class="info-list">
+ <view class="info-item">
+ <text class="info-label">濉姤鎻愮ず</text>
+ <text class="info-value">{{ formConfigData.prompt || "-" }}</text>
+ </view>
+ </view>
+ <view v-if="formConfigData.fields.length"
+ class="field-block">
+ <view v-for="(field, index) in formConfigData.fields"
+ :key="field.key || index"
+ class="field-card">
+ <view class="field-card-head">
+ <text class="field-card-name">{{ field.label }}</text>
+ <text class="field-tag">{{ fieldTypeLabel(field.type) }}</text>
+ <text v-if="field.required"
+ class="field-tag field-tag--req">蹇呭~</text>
+ </view>
+ <text v-if="field.defaultValue"
+ class="field-card-default">
+ 榛樿鍊硷細{{ field.defaultValue }}
+ </text>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤濉姤椤�</view>
+ </view>
+
+ <view class="section">
+ <view class="section-title">瀹℃壒娴佺▼</view>
+ <view v-if="detail.nodes?.length"
+ class="flow-list">
+ <view v-for="(node, index) in detail.nodes"
+ :key="node.id || index"
+ class="flow-card">
+ <view class="flow-card-head">
+ <text class="flow-level">绗瑊{ levelLabel(node.levelNo || index + 1) }}绾�</text>
+ <text class="flow-type">{{ approveTypeText(node.approveType) }}</text>
+ </view>
+ <view class="approver-tags">
+ <text v-for="(approver, aIdx) in node.approvers || []"
+ :key="approver.id || aIdx"
+ class="approver-tag">
+ {{ approver.approverName || "-" }}
+ </text>
+ <text v-if="!(node.approvers || []).length"
+ class="empty-hint inline">鏆傛棤瀹℃壒浜�</text>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="empty-hint">鏆傛棤瀹℃壒鑺傜偣</view>
+ </view>
+ </template>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="data"
+ text="鏈幏鍙栧埌妯℃澘璇︽儏" />
+ </view>
+ </scroll-view>
+
+ <FooterButtons v-if="!loading && detail"
+ cancel-text="杩斿洖"
+ confirm-text="缂栬緫"
+ @cancel="goBack"
+ @confirm="goEdit" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import { getApprovalTemplateDetail } from "@/api/oa/approvalTemplate.js";
+ import { getFieldEditorTypeLabel } from "../../_utils/approvalFormField.js";
+ import {
+ buildTypeLabelMap,
+ fetchApprovalTemplateTypes,
+ getTemplateTypeLabel,
+ } from "../../_utils/approvalTemplateType.js";
+
+ const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+ const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+ const templateId = ref("");
+ const detail = ref(null);
+ const loading = ref(false);
+ const typeLabelMap = ref({});
+
+ const formConfigData = computed(() => {
+ const raw = detail.value?.formConfig;
+ if (!raw) return { prompt: "", fields: [] };
+ try {
+ const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
+ return {
+ prompt: obj?.prompt || "",
+ fields: Array.isArray(obj?.fields) ? obj.fields : [],
+ };
+ } catch {
+ return { prompt: "", fields: [] };
+ }
+ });
+
+ const levelLabel = n => LEVEL_TEXT[Number(n)] || String(n);
+
+ const businessTypeText = type =>
+ getTemplateTypeLabel(type, typeLabelMap.value);
+
+ const enabledText = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "鍚敤";
+ if (val === "0") return "鍋滅敤";
+ return "-";
+ };
+
+ const enabledClass = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "status-on";
+ if (val === "0") return "status-off";
+ return "";
+ };
+
+ const fieldTypeLabel = type => getFieldEditorTypeLabel(type);
+
+ const approveTypeText = type => (type === "OR" ? "鎴栫" : "浼氱");
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const goEdit = () => {
+ if (!templateId.value || !detail.value) return;
+ uni.setStorageSync(EDIT_STORAGE_KEY, detail.value);
+ uni.navigateTo({
+ url: `/pages/oa/ApproveManage/approve-template/edit?id=${templateId.value}`,
+ });
+ };
+
+ const loadDetail = () => {
+ if (!templateId.value) return;
+ loading.value = true;
+ detail.value = null;
+ getApprovalTemplateDetail(templateId.value)
+ .then(res => {
+ detail.value = res?.data || null;
+ if (!detail.value) {
+ uni.showToast({ title: "鏈幏鍙栧埌璇︽儏", icon: "none" });
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ fetchApprovalTemplateTypes()
+ .then(opts => {
+ typeLabelMap.value = buildTypeLabelMap(opts);
+ })
+ .catch(() => {});
+ if (options?.id) {
+ templateId.value = options.id;
+ loadDetail();
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ .template-detail-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background: #f0f3f8;
+ }
+
+ .detail-scroll {
+ flex: 1;
+ height: 0;
+ padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
+ }
+
+ .loading-wrap {
+ padding: 48px 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .loading-text {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .section {
+ background: #fff;
+ border-radius: 12px;
+ margin-bottom: 10px;
+ overflow: hidden;
+ box-shadow: 0 2px 12px rgba(31, 45, 61, 0.05);
+ }
+
+ .section-title {
+ padding: 12px 16px;
+ font-size: 15px;
+ font-weight: 600;
+ color: #1f2d3d;
+ border-bottom: 1px solid #f2f4f7;
+ border-left: 3px solid #2979ff;
+ padding-left: 13px;
+ }
+
+ .info-list {
+ padding: 4px 0;
+ }
+
+ .info-item {
+ display: flex;
+ align-items: flex-start;
+ padding: 11px 16px;
+ border-bottom: 1px solid #f5f7fa;
+ gap: 12px;
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .info-label {
+ flex-shrink: 0;
+ width: 88px;
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .info-value {
+ flex: 1;
+ font-size: 14px;
+ color: #303133;
+ text-align: right;
+ word-break: break-all;
+ }
+
+ .status-on {
+ color: #18a058;
+ }
+
+ .status-off {
+ color: #909399;
+ }
+
+ .field-block {
+ padding: 0 12px 12px;
+ }
+
+ .field-card {
+ padding: 10px 12px;
+ margin-bottom: 8px;
+ background: #f8fafc;
+ border-radius: 8px;
+ border: 1px solid #eef2f6;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .field-card-head {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+
+ .field-card-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: #303133;
+ }
+
+ .field-tag {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ color: #2979ff;
+ background: #ecf5ff;
+
+ &--req {
+ color: #f56c6c;
+ background: #fef0f0;
+ }
+ }
+
+ .field-card-default {
+ display: block;
+ margin-top: 6px;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .flow-list {
+ padding: 12px;
+ }
+
+ .flow-card {
+ padding: 12px;
+ margin-bottom: 8px;
+ background: #f8fafc;
+ border-radius: 8px;
+ border: 1px solid #eef2f6;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .flow-card-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ }
+
+ .flow-level {
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .flow-type {
+ font-size: 13px;
+ color: #2979ff;
+ }
+
+ .approver-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .approver-tag {
+ padding: 4px 10px;
+ font-size: 13px;
+ color: #303133;
+ background: #fff;
+ border: 1px solid #dce8f8;
+ border-radius: 16px;
+ }
+
+ .empty-hint {
+ padding: 12px 16px 16px;
+ font-size: 13px;
+ color: #909399;
+
+ &.inline {
+ padding: 0;
+ }
+ }
+
+ .empty-wrap {
+ padding: 48px 20px;
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-template/edit.vue b/src/pages/oa/ApproveManage/approve-template/edit.vue
new file mode 100644
index 0000000..53a2d2e
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-template/edit.vue
@@ -0,0 +1,2634 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 鏂板缓瀹℃壒妯℃澘
+ 璺敱锛�/pages/oa/ApproveManage/approve-template/edit
+-->
+<template>
+ <view class="template-edit-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <scroll-view class="form-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="88"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="鍩烘湰淇℃伅"
+ class="form-section">
+ <up-form-item label="妯℃澘鍚嶇О"
+ prop="templateName"
+ required
+ class="form-item-name">
+ <up-input v-model="form.templateName"
+ class="name-input-inline"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ maxlength="50"
+ :disabled="isSystemTemplate"
+ :clearable="!isSystemTemplate" />
+ </up-form-item>
+ <up-form-item label="瀹℃壒绫诲瀷"
+ prop="businessType"
+ required
+ class="form-item-select"
+ :class="{ 'form-item-select--disabled': isSystemTemplate }"
+ @click="openBusinessTypeSheet">
+ <up-input :model-value="businessTypeText"
+ placeholder="璇烽�夋嫨瀹℃壒绫诲瀷"
+ readonly
+ :disabled="isSystemTemplate" />
+ <template v-if="!isSystemTemplate"
+ #right>
+ <up-icon name="arrow-right"
+ @click.stop="openBusinessTypeSheet" />
+ </template>
+ </up-form-item>
+ <up-form-item label="鍚敤鐘舵��"
+ class="form-item-switch">
+ <view class="switch-wrap">
+ <up-switch v-model="enabledBool" />
+ </view>
+ </up-form-item>
+ <up-form-item label="妯℃澘璇存槑"
+ class="form-item-desc"
+ label-position="top">
+ <view class="desc-input-shell">
+ <up-textarea v-model="form.description"
+ placeholder="閫夊~"
+ maxlength="200"
+ border="none"
+ height="72" />
+ </view>
+ </up-form-item>
+ </u-cell-group>
+
+ <view class="section-card">
+ <view class="section-head section-head--between">
+ <view class="section-head-left">
+ <text class="section-title">濉姤椤归厤缃�</text>
+ <text class="section-count">鍏� {{ formConfig.fields.length }} 椤�</text>
+ </view>
+ <view class="head-actions">
+ <text class="head-link head-link--import"
+ :class="{ 'head-link--disabled': !canImportTemplate }"
+ @click="openTemplateImport">浠庡凡鏈夋ā鏉垮鍏�</text>
+ <text class="head-link head-link--primary"
+ @click="openFieldEditor()">+ 娣诲姞濉姤椤�</text>
+ </view>
+ </view>
+ <view class="section-body">
+ <view class="prompt-row">
+ <text class="prompt-label">濉姤鎻愮ず</text>
+ <up-input v-model="formConfig.prompt"
+ class="prompt-input"
+ placeholder="閫夊~"
+ maxlength="200"
+ clearable />
+ </view>
+ <view v-if="formConfig.fields.length"
+ class="field-list">
+ <view v-for="(field, index) in formConfig.fields"
+ :key="field.key"
+ class="field-item"
+ :class="{ 'field-item--locked': isFieldLocked(field) }"
+ @click="onFieldItemClick(field, index)">
+ <view class="field-order">{{ index + 1 }}</view>
+ <view class="field-main">
+ <view class="field-title-row">
+ <text class="field-name">{{ field.label }}</text>
+ <view class="field-tags">
+ <text class="type-tag"
+ :class="fieldTypeTagClass(field.type)">
+ {{ fieldTypeLabel(field.type) }}
+ </text>
+ <text v-if="field.required"
+ class="req-tag">蹇呭~</text>
+ </view>
+ </view>
+ <text class="field-key">{{ field.key }}</text>
+ <text v-if="field.defaultValue"
+ class="field-default">
+ 榛樿锛歿{ formatFieldDefaultPreview(field) }}
+ </text>
+ </view>
+ <view v-if="!isFieldLocked(field)"
+ class="field-actions"
+ @click.stop>
+ <view class="icon-btn icon-btn--edit"
+ @click.stop="openFieldEditor(field, index)">
+ <up-icon name="edit-pen"
+ size="16"
+ color="#2979ff" />
+ </view>
+ <view class="icon-btn icon-btn--del"
+ @click.stop="removeField(index)">
+ <up-icon name="trash"
+ size="16"
+ color="#f56c6c" />
+ </view>
+ </view>
+ <view v-else
+ class="field-lock-tag">鍐呯疆</view>
+ </view>
+ </view>
+ <view v-else
+ class="empty-mini">
+ <text>鏆傛棤濉姤椤�</text>
+ </view>
+ </view>
+ </view>
+
+ <view class="section-card">
+ <view class="section-head">
+ <text class="section-title">瀹℃壒娴佺▼</text>
+ </view>
+ <view class="flow-wrap">
+ <view v-for="(node, nodeIndex) in flowNodes"
+ :key="node._key"
+ class="flow-node-block">
+ <view class="flow-node-card">
+ <view class="node-header">
+ <view class="node-level-badge">{{ nodeIndex + 1 }}</view>
+ <text class="node-level-text">绗瑊{ levelLabel(nodeIndex + 1) }}绾�</text>
+ <view v-if="flowNodes.length > 1"
+ class="node-delete"
+ @click="removeNode(nodeIndex)">
+ <up-icon name="trash"
+ size="16"
+ color="#f56c6c" />
+ </view>
+ </view>
+ <view class="approve-type-row">
+ <view class="type-btn"
+ :class="{ active: node.approveType === 'AND' }"
+ @click="node.approveType = 'AND'">
+ 浼氱
+ </view>
+ <view class="type-btn"
+ :class="{ active: node.approveType === 'OR' }"
+ @click="node.approveType = 'OR'">
+ 鎴栫
+ </view>
+ </view>
+ <view class="approver-list">
+ <view v-for="(approver, approverIndex) in node.approvers"
+ :key="`${node._key}-${approver.approverId}-${approverIndex}`"
+ class="approver-chip">
+ <view class="approver-avatar">{{ (approver.approverName || "?").charAt(0) }}</view>
+ <text class="approver-name">{{ approver.approverName }}</text>
+ <view class="approver-remove"
+ hover-class="approver-remove--active"
+ @tap.stop="removeApprover(nodeIndex, approverIndex)"
+ @click.stop="removeApprover(nodeIndex, approverIndex)">
+ <text class="remove-icon">脳</text>
+ </view>
+ </view>
+ <view class="add-approver"
+ @click="openUserPicker(nodeIndex)">
+ <up-icon name="plus"
+ size="14"
+ color="#2979ff" />
+ <text>娣诲姞</text>
+ </view>
+ </view>
+ </view>
+ <view v-if="nodeIndex < flowNodes.length - 1"
+ class="flow-connector">
+ <view class="flow-connector-line" />
+ </view>
+ </view>
+ <view class="add-node-bar"
+ @click="addNode">
+ <up-icon name="plus-circle"
+ size="20"
+ color="#2979ff" />
+ <text>娣诲姞绾ф</text>
+ </view>
+ </view>
+ </view>
+ </up-form>
+ </scroll-view>
+
+ <FooterButtons :loading="submitting"
+ confirm-text="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+
+ <up-action-sheet :show="showTemplateImportSheet"
+ title="浠庡凡鏈夋ā鏉垮鍏�"
+ :actions="templateImportActions"
+ @select="onSelectImportTemplate"
+ @close="showTemplateImportSheet = false" />
+
+ <up-popup :show="showFieldEditor"
+ mode="bottom"
+ round="16"
+ @close="closeFieldEditor">
+ <view class="field-editor">
+ <view class="sheet-handle" />
+ <view class="editor-header">
+ <text class="editor-title">{{ editingFieldIndex >= 0 ? "缂栬緫濉姤椤�" : "娣诲姞濉姤椤�" }}</text>
+ <text class="editor-subtitle">閰嶇疆瀛楁灞炴�с�佹牎楠屼笌榛樿鍊�</text>
+ </view>
+ <scroll-view class="editor-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view class="editor-form">
+ <view class="editor-section-card">
+ <view class="editor-section-head">
+ <text class="editor-section-title">鍩虹淇℃伅</text>
+ </view>
+ <view class="editor-cell">
+ <text class="editor-label required">鏄剧ず鍚嶇О</text>
+ <view class="editor-input-box">
+ <up-input v-model="fieldDraft.label"
+ placeholder="濡傦細鎶ラ攢璇存槑"
+ border="none"
+ clearable />
+ </view>
+ </view>
+ <view class="editor-cell">
+ <text class="editor-label required">瀛楁鏍囪瘑</text>
+ <view class="editor-input-box">
+ <up-input v-model="fieldDraft.key"
+ placeholder="濡傦細summary"
+ border="none"
+ clearable />
+ </view>
+ </view>
+ <view class="editor-cell editor-cell--tap"
+ @click="openFieldTypePicker">
+ <text class="editor-label required">鎺т欢绫诲瀷</text>
+ <view class="picker-value-row">
+ <text class="picker-value"
+ :class="{ 'picker-value--placeholder': !fieldDraft.type }">
+ {{ fieldDraftTypeText || "璇烽�夋嫨" }}
+ </text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#b0b8c4" />
+ </view>
+ </view>
+ </view>
+
+ <view class="editor-section-card">
+ <view class="editor-section-head">
+ <text class="editor-section-title">鏍¢獙涓庢牸寮�</text>
+ </view>
+ <view class="editor-cell editor-cell--switch">
+ <view class="switch-label-wrap">
+ <text class="editor-label">鏄惁蹇呭~</text>
+ <text class="switch-hint">鎻愪氦瀹℃壒鏃堕』濉啓璇ラ」</text>
+ </view>
+ <up-switch v-model="fieldDraft.required"
+ active-color="#2979ff" />
+ </view>
+ </view>
+
+ <view v-if="isSelectDraft"
+ class="editor-section-card">
+ <view class="editor-section-head">
+ <text class="editor-section-title">涓嬫媺閫夐」</text>
+ </view>
+ <view class="editor-cell editor-cell--tap"
+ @click="openOptionSourcePicker">
+ <text class="editor-label">閫夐」鏉ユ簮</text>
+ <view class="picker-value-row">
+ <text class="picker-value">{{ fieldDraftOptionSourceText }}</text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#b0b8c4" />
+ </view>
+ </view>
+ <view v-if="fieldDraft.optionSource === 'manual'"
+ class="manual-options">
+ <text class="manual-options-title">鎵嬪姩閫夐」</text>
+ <view class="manual-options-table">
+ <view class="option-table-head">
+ <text class="option-col option-col--idx" />
+ <text class="option-col option-col--label">鏄剧ず鏂囨湰</text>
+ <text class="option-col option-col--value">閫夐」鍊�</text>
+ <text class="option-col option-col--action" />
+ </view>
+ <view v-for="(opt, optIndex) in fieldDraft.options"
+ :key="optIndex"
+ class="option-card">
+ <text class="option-idx">{{ optIndex + 1 }}</text>
+ <view class="option-input-wrap">
+ <up-input v-model="opt.label"
+ placeholder="濡傦細宸ヤ綔鏃ュ姞鐝�"
+ border="none"
+ clearable />
+ </view>
+ <view class="option-input-wrap option-input-wrap--value">
+ <up-input v-model="opt.value"
+ placeholder="濡傦細0"
+ border="none"
+ clearable />
+ </view>
+ <view class="option-del"
+ hover-class="option-del--active"
+ @click.stop="removeDraftOption(optIndex)">
+ <up-icon name="trash"
+ size="16"
+ color="#f56c6c" />
+ </view>
+ </view>
+ </view>
+ <view class="add-option-btn"
+ hover-class="add-option-btn--active"
+ @click="addDraftOption">
+ <up-icon name="plus-circle"
+ size="16"
+ color="#2979ff" />
+ <text>娣诲姞閫夐」</text>
+ </view>
+ </view>
+ <view v-else
+ class="option-source-tip">
+ <up-icon name="info-circle"
+ size="14"
+ color="#909399" />
+ <text>鍙戣捣瀹℃壒鏃跺皢鑷姩鍔犺浇{{ fieldDraftOptionSourceText }}</text>
+ </view>
+ </view>
+
+ <view class="editor-section-card">
+ <view class="editor-section-head">
+ <text class="editor-section-title">榛樿鍊�</text>
+ </view>
+ <text class="default-hint">
+ 閫夋嫨璇ユā鏉挎彁浜ゅ鎵规椂鑷姩棰勫~锛岀敤鎴蜂粛鍙慨鏀�
+ </text>
+ <view class="editor-cell editor-cell--value">
+ <up-textarea v-if="fieldDraft.type === 'textarea'"
+ v-model="fieldDraft.defaultValue"
+ placeholder="閫夊~"
+ maxlength="500"
+ border="surround"
+ height="72" />
+ <view v-else-if="fieldDraft.type === 'date'"
+ class="picker-value-row picker-value-row--tap"
+ @click="openDefaultDatePicker">
+ <text class="picker-value"
+ :class="{ 'picker-value--placeholder': !fieldDraft.defaultValue }">
+ {{ fieldDraft.defaultValue || "閫夋嫨鏃ユ湡" }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#909399" />
+ </view>
+ <view v-else-if="isDatetimerangeDraft"
+ class="daterange-default-wrap">
+ <view class="daterange-default-item"
+ @click="openDefaultRangePicker('start')">
+ <text class="daterange-default-label">寮�濮嬫椂闂�</text>
+ <view class="picker-value-row picker-value-row--tap">
+ <text class="picker-value"
+ :class="{ 'picker-value--placeholder': !defaultRangeStart }">
+ {{ defaultRangeStart || "閫夋嫨寮�濮嬫椂闂�" }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#909399" />
+ </view>
+ </view>
+ <view class="daterange-default-item"
+ @click="openDefaultRangePicker('end')">
+ <text class="daterange-default-label">缁撴潫鏃堕棿</text>
+ <view class="picker-value-row picker-value-row--tap">
+ <text class="picker-value"
+ :class="{ 'picker-value--placeholder': !defaultRangeEnd }">
+ {{ defaultRangeEnd || "閫夋嫨缁撴潫鏃堕棿" }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#909399" />
+ </view>
+ </view>
+ </view>
+ <view v-else-if="isSelectDraft"
+ class="picker-value-row picker-value-row--tap"
+ @click="openDefaultSelectSheet">
+ <text class="picker-value"
+ :class="{ 'picker-value--placeholder': !fieldDraft.defaultValue }">
+ {{ defaultSelectDisplayText || "閫夊~" }}
+ </text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#b0b8c4" />
+ </view>
+ <view v-else
+ class="editor-input-box">
+ <up-input v-model="fieldDraft.defaultValue"
+ :type="fieldDraft.type === 'number' ? 'digit' : 'text'"
+ placeholder="閫夊~"
+ border="none"
+ clearable />
+ </view>
+ </view>
+ </view>
+ </view>
+ </scroll-view>
+ <view class="editor-footer">
+ <view class="editor-btn editor-btn--cancel"
+ @click="closeFieldEditor">鍙栨秷</view>
+ <view class="editor-btn editor-btn--confirm"
+ @click="confirmFieldEditor">纭畾</view>
+ </view>
+
+ <view v-if="inlinePickerShow"
+ class="editor-picker-layer">
+ <view class="editor-picker-mask"
+ @click="closeInlinePicker" />
+ <view class="editor-picker-panel">
+ <view class="editor-picker-head">
+ <text class="editor-picker-cancel"
+ @click="closeInlinePicker">鍙栨秷</text>
+ <text class="editor-picker-title">{{ inlinePickerTitle }}</text>
+ <text class="editor-picker-placeholder" />
+ </view>
+ <scroll-view class="editor-picker-scroll"
+ scroll-y>
+ <view v-for="(item, pickerIndex) in inlinePickerOptions"
+ :key="`${inlinePickerMode}-${pickerIndex}-${item.value}`"
+ class="editor-picker-item"
+ :class="{ 'editor-picker-item--active': isInlinePickerItemActive(item) }"
+ @click="onInlinePickerSelect(item)">
+ <text>{{ item.name }}</text>
+ <up-icon v-if="isInlinePickerItemActive(item)"
+ name="checkmark"
+ size="18"
+ color="#2979ff" />
+ </view>
+ </scroll-view>
+ </view>
+ </view>
+ </view>
+ </up-popup>
+
+ <up-popup :show="showDefaultDatePicker"
+ mode="bottom"
+ @close="closeDefaultDatePicker">
+ <up-datetime-picker :show="true"
+ v-model="defaultDateTs"
+ :mode="defaultDatePickerMode"
+ @confirm="onDefaultDatePickerConfirm"
+ @cancel="closeDefaultDatePicker" />
+ </up-popup>
+
+ <up-popup :show="showUserPicker"
+ mode="bottom"
+ round="16"
+ @close="closeUserPicker">
+ <view class="user-picker">
+ <view class="sheet-handle" />
+ <view class="picker-head">
+ <text class="picker-cancel"
+ @click="closeUserPicker">鍙栨秷</text>
+ <text class="picker-title">閫夋嫨瀹℃壒浜�</text>
+ <text class="picker-confirm"
+ @click="confirmUserPicker">
+ 纭畾{{ pickerSelectedIds.length ? `(${pickerSelectedIds.length})` : "" }}
+ </text>
+ </view>
+ <scroll-view class="user-scroll"
+ scroll-y>
+ <view v-for="user in availableUsers"
+ :key="user.userId"
+ class="user-item"
+ :class="{ selected: isUserSelected(user.userId) }"
+ @click="toggleUser(user)">
+ <view class="user-avatar">{{ (user.nickName || "?").charAt(0) }}</view>
+ <text class="user-name">{{ user.nickName }}</text>
+ <view class="user-check"
+ :class="{ checked: isUserSelected(user.userId) }">
+ <up-icon v-if="isUserSelected(user.userId)"
+ name="checkmark"
+ size="14"
+ color="#fff" />
+ </view>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+
+ <up-action-sheet :show="showBusinessTypeSheet"
+ title="閫夋嫨瀹℃壒绫诲瀷"
+ :actions="businessTypeActions"
+ @select="onSelectBusinessType"
+ @close="showBusinessTypeSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import {
+ addApprovalTemplate,
+ getApprovalTemplateDetail,
+ listApprovalTemplatePage,
+ updateApprovalTemplate,
+ } from "@/api/oa/approvalTemplate.js";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess.js";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import { formatDateToYMD } from "@/utils/ruoyi";
+ import {
+ buildFieldConfigPayload,
+ createEmptyFieldOption,
+ parseApprovalFormConfig,
+ FIELD_EDITOR_TYPE_OPTIONS,
+ FIELD_OPTION_SOURCE_OPTIONS,
+ getFieldEditorTypeLabel,
+ getFieldOptionLabel,
+ getFieldOptionSource,
+ getFieldOptionSourceLabel,
+ isDatetimerangeField,
+ isSelectField,
+ formatDatetimerangeDisplay,
+ formatFieldDateValue,
+ joinDatetimerangeValue,
+ parseDatetimerangeValue,
+ parseFieldDateToTs,
+ resolveFieldOptions,
+ } from "../../_utils/approvalFormField.js";
+ import {
+ fetchApprovalTemplateTypes,
+ isSystemApprovalTemplate,
+ } from "../../_utils/approvalTemplateType.js";
+
+ const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+
+ const LEVEL_TEXT = ["", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�", "鍗�"];
+
+ const formRef = ref();
+ const submitting = ref(false);
+ const userList = ref([]);
+ const templateId = ref(null);
+
+ const showTemplateImportSheet = ref(false);
+ const importTemplateList = ref([]);
+ const showFieldEditor = ref(false);
+ const inlinePickerShow = ref(false);
+ const inlinePickerTitle = ref("");
+ const inlinePickerOptions = ref([]);
+ const inlinePickerMode = ref("");
+ const showUserPicker = ref(false);
+ const showDefaultDatePicker = ref(false);
+ const defaultDatePickerMode = ref("date");
+ const defaultRangePickerPart = ref("start");
+ const defaultDateTs = ref(Date.now());
+ const deptList = ref([]);
+
+ const editingFieldIndex = ref(-1);
+ const editingNodeIndex = ref(-1);
+ const pickerSelectedIds = ref([]);
+ /** 绯荤粺妯℃澘鍔犺浇鏃堕攣瀹氱殑濉姤椤� key锛屼笉鍙紪杈�/鍒犻櫎 */
+ const lockedFieldKeys = ref(new Set());
+
+ const form = reactive({
+ templateName: "",
+ businessType: null,
+ templateType: 1,
+ enabled: "1",
+ description: "",
+ });
+
+ const formConfig = reactive({
+ prompt: "",
+ fields: [],
+ });
+
+ const fieldDraft = reactive({
+ label: "",
+ key: "",
+ type: "text",
+ defaultValue: "",
+ required: true,
+ optionSource: "manual",
+ options: [createEmptyFieldOption()],
+ });
+
+ let nodeKeySeed = 1;
+
+ const createNode = () => ({
+ _key: `node_${nodeKeySeed++}`,
+ approveType: "AND",
+ approvers: [],
+ });
+
+ const flowNodes = ref([createNode()]);
+
+ const rules = {
+ templateName: [{ required: true, message: "璇疯緭鍏ユā鏉垮悕绉�", trigger: "blur" }],
+ businessType: [
+ {
+ validator: (_rule, value, callback) => {
+ if (value === "" || value === null || value === undefined) {
+ callback(new Error("璇烽�夋嫨瀹℃壒绫诲瀷"));
+ return;
+ }
+ callback();
+ },
+ trigger: "change",
+ },
+ ],
+ };
+
+ const businessTypeOptions = ref([]);
+ const showBusinessTypeSheet = ref(false);
+
+ const businessTypeActions = computed(() =>
+ businessTypeOptions.value.map(opt => ({
+ name: opt.name,
+ value: opt.value,
+ }))
+ );
+
+ const businessTypeText = computed(() => {
+ const matched = businessTypeOptions.value.find(
+ opt => String(opt.value) === String(form.businessType)
+ );
+ return matched?.name || "";
+ });
+
+ const canImportTemplate = computed(() => !isSystemTemplate.value);
+
+ const templateImportActions = computed(() =>
+ importTemplateList.value.map(item => {
+ const typeTag = isSystemApprovalTemplate(item) ? "绯荤粺" : "鑷畾涔�";
+ return {
+ name: `銆�${typeTag}銆�${item.templateName || `妯℃澘${item.id}`}`,
+ value: String(item.id),
+ };
+ })
+ );
+
+ const isSelectDraft = computed(() => isSelectField(fieldDraft));
+
+ const isDatetimerangeDraft = computed(() => isDatetimerangeField(fieldDraft));
+
+ const defaultRangeParts = computed(() =>
+ parseDatetimerangeValue(fieldDraft.defaultValue)
+ );
+
+ const defaultRangeStart = computed(() => defaultRangeParts.value.start);
+
+ const defaultRangeEnd = computed(() => defaultRangeParts.value.end);
+
+ const fieldDraftTypeText = computed(() => getFieldEditorTypeLabel(fieldDraft.type));
+
+ const fieldDraftOptionSourceText = computed(() =>
+ getFieldOptionSourceLabel(fieldDraft.optionSource)
+ );
+
+ const defaultSelectActions = computed(() => {
+ const options = resolveFieldOptions(fieldDraft, {
+ users: userList.value,
+ depts: deptList.value,
+ });
+ return [
+ { name: "涓嶈缃�", value: "" },
+ ...options.map(opt => ({
+ name: opt.label,
+ value: opt.value,
+ })),
+ ];
+ });
+
+ const defaultSelectDisplayText = computed(() => {
+ if (!fieldDraft.defaultValue) return "";
+ return (
+ getFieldOptionLabel(fieldDraft, fieldDraft.defaultValue) ||
+ String(fieldDraft.defaultValue)
+ );
+ });
+
+ const enabledBool = computed({
+ get: () => form.enabled === "1",
+ set: val => {
+ form.enabled = val ? "1" : "0";
+ },
+ });
+
+ const isEditMode = computed(() => templateId.value != null && templateId.value !== "");
+
+ const isSystemTemplate = computed(() => isSystemApprovalTemplate(form));
+
+ const isFieldLocked = field =>
+ isSystemTemplate.value && lockedFieldKeys.value.has(field?.key);
+
+ const pageTitle = computed(() =>
+ isEditMode.value ? "缂栬緫瀹℃壒妯℃澘" : "鏂板缓瀹℃壒妯℃澘"
+ );
+
+ const mapNodesFromRow = nodes => {
+ if (!Array.isArray(nodes) || !nodes.length) {
+ return [createNode()];
+ }
+ return nodes.map(node => ({
+ _key: `node_${nodeKeySeed++}`,
+ id: node.id,
+ templateId: node.templateId,
+ approveType: node.approveType || "AND",
+ approvers: (node.approvers || []).map((approver, idx) => ({
+ id: approver.id,
+ nodeId: approver.nodeId,
+ templateId: approver.templateId,
+ approverId: approver.approverId,
+ approverName: approver.approverName,
+ sortNo: approver.sortNo ?? idx + 1,
+ })),
+ }));
+ };
+
+ const fillFormFromRow = row => {
+ if (!row) return;
+ templateId.value = row.id;
+ form.templateName = row.templateName || "";
+ const parsedBusiness = Number(row.businessType);
+ form.businessType = Number.isNaN(parsedBusiness)
+ ? row.businessType
+ : parsedBusiness;
+ const parsedTemplateType = Number(row.templateType);
+ form.templateType = Number.isNaN(parsedTemplateType) ? 1 : parsedTemplateType;
+ form.enabled = String(row.enabled ?? "1");
+ form.description = row.description || "";
+
+ const config = parseApprovalFormConfig(row.formConfig);
+ formConfig.prompt = config.prompt;
+ formConfig.fields = config.fields;
+ lockedFieldKeys.value = isSystemApprovalTemplate(row)
+ ? new Set(config.fields.map(f => f.key).filter(Boolean))
+ : new Set();
+ flowNodes.value = mapNodesFromRow(row.nodes);
+ };
+
+ const availableUsers = computed(() => {
+ const node = flowNodes.value[editingNodeIndex.value];
+ if (!node) return userList.value;
+ const selectedIds = new Set(node.approvers.map(a => a.approverId));
+ return userList.value.filter(user => !selectedIds.has(user.userId));
+ });
+
+ const levelLabel = n => LEVEL_TEXT[n] || String(n);
+
+ const fieldTypeLabel = type => getFieldEditorTypeLabel(type);
+
+ const formatFieldDefaultPreview = field => {
+ if (isDatetimerangeField(field)) {
+ return formatDatetimerangeDisplay(field.defaultValue) || field.defaultValue;
+ }
+ return field.defaultValue;
+ };
+
+ const fieldTypeTagClass = type => {
+ const map = {
+ text: "type-tag--text",
+ textarea: "type-tag--area",
+ number: "type-tag--num",
+ date: "type-tag--date",
+ datetimerange: "type-tag--date",
+ select: "type-tag--select",
+ };
+ return map[type] || "type-tag--text";
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const openBusinessTypeSheet = () => {
+ if (isSystemTemplate.value) return;
+ if (!businessTypeOptions.value.length) {
+ uni.showToast({ title: "瀹℃壒绫诲瀷鍔犺浇涓�", icon: "none" });
+ return;
+ }
+ showBusinessTypeSheet.value = true;
+ };
+
+ const onSelectBusinessType = action => {
+ form.businessType = action.value;
+ showBusinessTypeSheet.value = false;
+ formRef.value?.validateField?.("businessType");
+ };
+
+ const applyImportedFormConfig = (config, sourceName = "") => {
+ const parsed = {
+ prompt: config?.prompt || "",
+ fields: (config?.fields || []).map(field => ({ ...field })),
+ };
+ formConfig.prompt = parsed.prompt;
+ formConfig.fields = parsed.fields;
+ const tip = sourceName ? `宸插鍏ャ��${sourceName}銆峘 : "宸插鍏ュ~鎶ラ厤缃�";
+ uni.showToast({ title: tip, icon: "success" });
+ };
+
+ const doImportFormConfig = (config, sourceName) => {
+ const hasExisting =
+ !!formConfig.prompt?.trim() || formConfig.fields.length > 0;
+ if (!hasExisting) {
+ applyImportedFormConfig(config, sourceName);
+ return;
+ }
+ uni.showModal({
+ title: "瀵煎叆纭",
+ content: `灏嗕娇鐢ㄣ��${sourceName}銆嶇殑濉姤閰嶇疆瑕嗙洊褰撳墠鍐呭锛屾槸鍚︾户缁紵`,
+ success: res => {
+ if (res.confirm) {
+ applyImportedFormConfig(config, sourceName);
+ }
+ },
+ });
+ };
+
+ const applyTemplateImport = templateIdValue => {
+ const row = importTemplateList.value.find(
+ item => String(item.id) === String(templateIdValue)
+ );
+ const sourceName = row?.templateName || "鎵�閫夋ā鏉�";
+ const applyFromDetail = detail => {
+ const config = parseApprovalFormConfig(detail?.formConfig);
+ if (!config.fields.length && !config.prompt) {
+ uni.showToast({ title: "璇ユā鏉挎棤濉姤閰嶇疆", icon: "none" });
+ return;
+ }
+ doImportFormConfig(config, sourceName);
+ };
+
+ if (row?.formConfig) {
+ applyFromDetail(row);
+ return;
+ }
+
+ uni.showLoading({ title: "鍔犺浇閰嶇疆...", mask: true });
+ getApprovalTemplateDetail(templateIdValue)
+ .then(res => applyFromDetail(res?.data))
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇妯℃澘閰嶇疆澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ };
+
+ const openTemplateImport = () => {
+ if (!canImportTemplate.value) {
+ uni.showToast({ title: "绯荤粺鍐呯疆妯℃澘涓嶅彲瀵煎叆", icon: "none" });
+ return;
+ }
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+ listApprovalTemplatePage({
+ page: { current: 1, size: 200 },
+ approvalTemplateDto: {},
+ })
+ .then(res => {
+ const records = res?.data?.records || [];
+ importTemplateList.value = records.filter(
+ item =>
+ item?.id != null && String(item.id) !== String(templateId.value)
+ );
+ if (!importTemplateList.value.length) {
+ uni.showToast({ title: "鏆傛棤鍙鍏ョ殑妯℃澘", icon: "none" });
+ return;
+ }
+ showTemplateImportSheet.value = true;
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍔犺浇妯℃澘鍒楄〃澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ };
+
+ const onSelectImportTemplate = action => {
+ showTemplateImportSheet.value = false;
+ const value = String(action?.value ?? "");
+ if (!value) return;
+ applyTemplateImport(value);
+ };
+
+ const resetFieldDraft = () => {
+ fieldDraft.label = "";
+ fieldDraft.key = "";
+ fieldDraft.type = "text";
+ fieldDraft.defaultValue = "";
+ fieldDraft.required = true;
+ fieldDraft.optionSource = "manual";
+ fieldDraft.options = [createEmptyFieldOption()];
+ };
+
+ const resolveActionValue = (action, options) => {
+ if (action?.value !== undefined && action?.value !== null) {
+ return action.value;
+ }
+ const name = action?.name;
+ if (name == null) return undefined;
+ return options.find(opt => opt.name === name)?.value;
+ };
+
+ const onSelectFieldType = action => {
+ const nextType = resolveActionValue(action, FIELD_EDITOR_TYPE_OPTIONS);
+ if (!nextType || fieldDraft.type === nextType) return;
+ fieldDraft.type = nextType;
+ fieldDraft.defaultValue = "";
+ if (!isSelectField(fieldDraft)) {
+ fieldDraft.optionSource = "manual";
+ fieldDraft.options = [createEmptyFieldOption()];
+ } else if (!fieldDraft.options?.length) {
+ fieldDraft.options = [createEmptyFieldOption()];
+ }
+ };
+
+ const openInlinePicker = (title, options, mode) => {
+ inlinePickerTitle.value = title;
+ inlinePickerOptions.value = options;
+ inlinePickerMode.value = mode;
+ inlinePickerShow.value = true;
+ };
+
+ const closeInlinePicker = () => {
+ inlinePickerShow.value = false;
+ inlinePickerMode.value = "";
+ inlinePickerOptions.value = [];
+ };
+
+ const isInlinePickerItemActive = item => {
+ if (inlinePickerMode.value === "fieldType") {
+ return String(fieldDraft.type) === String(item.value);
+ }
+ if (inlinePickerMode.value === "optionSource") {
+ return String(fieldDraft.optionSource) === String(item.value);
+ }
+ if (inlinePickerMode.value === "defaultValue") {
+ const val = fieldDraft.defaultValue;
+ if (val === "" || val === undefined || val === null) {
+ return item.value === "" || item.value === undefined || item.value === null;
+ }
+ return String(val) === String(item.value);
+ }
+ return false;
+ };
+
+ const onInlinePickerSelect = item => {
+ if (inlinePickerMode.value === "fieldType") {
+ onSelectFieldType(item);
+ } else if (inlinePickerMode.value === "optionSource") {
+ onSelectOptionSource(item);
+ } else if (inlinePickerMode.value === "defaultValue") {
+ onSelectDefaultOption(item);
+ }
+ closeInlinePicker();
+ };
+
+ const openFieldTypePicker = () => {
+ openInlinePicker(
+ "鎺т欢绫诲瀷",
+ FIELD_EDITOR_TYPE_OPTIONS.map(item => ({
+ name: item.name,
+ value: item.value,
+ })),
+ "fieldType"
+ );
+ };
+
+ const onSelectOptionSource = action => {
+ const nextSource = resolveActionValue(action, FIELD_OPTION_SOURCE_OPTIONS);
+ if (!nextSource) return;
+ fieldDraft.optionSource = nextSource;
+ fieldDraft.defaultValue = "";
+ if (nextSource === "manual" && !fieldDraft.options?.length) {
+ fieldDraft.options = [createEmptyFieldOption()];
+ }
+ };
+
+ const openOptionSourcePicker = () => {
+ openInlinePicker(
+ "閫夐」鏉ユ簮",
+ FIELD_OPTION_SOURCE_OPTIONS.map(item => ({
+ name: item.name,
+ value: item.value,
+ })),
+ "optionSource"
+ );
+ };
+
+ const addDraftOption = () => {
+ fieldDraft.options.push(createEmptyFieldOption());
+ };
+
+ const removeDraftOption = index => {
+ if (fieldDraft.options.length <= 1) {
+ fieldDraft.options[0] = createEmptyFieldOption();
+ return;
+ }
+ fieldDraft.options.splice(index, 1);
+ };
+
+ const openDefaultSelectSheet = () => {
+ const options = resolveFieldOptions(fieldDraft, {
+ users: userList.value,
+ depts: deptList.value,
+ });
+ if (!options.length) {
+ uni.showToast({ title: "璇峰厛閰嶇疆涓嬫媺閫夐」", icon: "none" });
+ return;
+ }
+ openInlinePicker("榛樿鍊�", defaultSelectActions.value, "defaultValue");
+ };
+
+ const onSelectDefaultOption = action => {
+ fieldDraft.defaultValue =
+ action.value === undefined || action.value === null
+ ? ""
+ : String(action.value);
+ };
+
+ const closeDefaultDatePicker = () => {
+ showDefaultDatePicker.value = false;
+ defaultDatePickerMode.value = "date";
+ defaultRangePickerPart.value = "start";
+ };
+
+ const openDefaultDatePicker = () => {
+ defaultDatePickerMode.value = "date";
+ const parsed = Date.parse(fieldDraft.defaultValue);
+ defaultDateTs.value = Number.isNaN(parsed) ? Date.now() : parsed;
+ showDefaultDatePicker.value = true;
+ };
+
+ const openDefaultRangePicker = part => {
+ defaultDatePickerMode.value = "datetime";
+ defaultRangePickerPart.value = part;
+ const parts = parseDatetimerangeValue(fieldDraft.defaultValue);
+ const val = part === "start" ? parts.start : parts.end;
+ defaultDateTs.value = parseFieldDateToTs(val) ?? Date.now();
+ showDefaultDatePicker.value = true;
+ };
+
+ const onDefaultDatePickerConfirm = e => {
+ const ts = e?.value ?? defaultDateTs.value;
+ if (defaultDatePickerMode.value === "datetime") {
+ const parts = parseDatetimerangeValue(fieldDraft.defaultValue);
+ const formatted = formatFieldDateValue({ type: "datetime" }, ts);
+ fieldDraft.defaultValue = joinDatetimerangeValue(
+ defaultRangePickerPart.value === "start" ? formatted : parts.start,
+ defaultRangePickerPart.value === "end" ? formatted : parts.end
+ );
+ } else {
+ fieldDraft.defaultValue = formatDateToYMD(ts);
+ }
+ closeDefaultDatePicker();
+ };
+
+ const onFieldItemClick = (field, index) => {
+ if (isFieldLocked(field)) return;
+ openFieldEditor(field, index);
+ };
+
+ const openFieldEditor = (field, index = -1) => {
+ if (field && isFieldLocked(field)) {
+ uni.showToast({ title: "绯荤粺鍐呯疆濉姤椤逛笉鍙慨鏀�", icon: "none" });
+ return;
+ }
+ editingFieldIndex.value = index;
+ if (field) {
+ fieldDraft.label = field.label || "";
+ fieldDraft.key = field.key || "";
+ fieldDraft.type = field.type || "text";
+ fieldDraft.defaultValue = field.defaultValue ?? "";
+ fieldDraft.required = !!field.required;
+ fieldDraft.optionSource = getFieldOptionSource(field);
+ fieldDraft.options = normalizeDraftOptions(field);
+ } else {
+ resetFieldDraft();
+ }
+ defaultDateTs.value = Date.now();
+ showFieldEditor.value = true;
+ };
+
+ const closeFieldEditor = () => {
+ closeInlinePicker();
+ showFieldEditor.value = false;
+ editingFieldIndex.value = -1;
+ };
+
+ const normalizeDraftOptions = field => {
+ const options = field?.options;
+ if (!Array.isArray(options) || !options.length) {
+ return [createEmptyFieldOption()];
+ }
+ return options.map(opt => ({
+ label: opt?.label ?? "",
+ value: opt?.value != null ? String(opt.value) : "",
+ }));
+ };
+
+ const buildFieldKey = label => {
+ const base = (label || "field")
+ .trim()
+ .replace(/\s+/g, "_")
+ .replace(/[^\w\u4e00-\u9fa5]/g, "");
+ let key = base || "field";
+ let index = 1;
+ while (formConfig.fields.some((item, idx) => item.key === key && idx !== editingFieldIndex.value)) {
+ key = `${base}_${index++}`;
+ }
+ return key;
+ };
+
+ const confirmFieldEditor = () => {
+ if (
+ editingFieldIndex.value >= 0 &&
+ isFieldLocked(formConfig.fields[editingFieldIndex.value])
+ ) {
+ uni.showToast({ title: "绯荤粺鍐呯疆濉姤椤逛笉鍙慨鏀�", icon: "none" });
+ return;
+ }
+ if (!fieldDraft.label?.trim()) {
+ uni.showToast({ title: "璇疯緭鍏ユ樉绀哄悕绉�", icon: "none" });
+ return;
+ }
+ const existingKey =
+ editingFieldIndex.value >= 0
+ ? formConfig.fields[editingFieldIndex.value]?.key
+ : null;
+ const draftKey = fieldDraft.key?.trim() || existingKey || buildFieldKey(fieldDraft.label);
+ if (!draftKey) {
+ uni.showToast({ title: "璇疯緭鍏ュ瓧娈垫爣璇�", icon: "none" });
+ return;
+ }
+ const duplicateKey = formConfig.fields.some(
+ (item, idx) => item.key === draftKey && idx !== editingFieldIndex.value
+ );
+ if (duplicateKey) {
+ uni.showToast({ title: "瀛楁鏍囪瘑宸插瓨鍦�", icon: "none" });
+ return;
+ }
+ if (isSelectField(fieldDraft) && fieldDraft.optionSource === "manual") {
+ const validOptions = (fieldDraft.options || []).filter(
+ opt => opt.label?.trim() && opt.value?.trim()
+ );
+ if (!validOptions.length) {
+ uni.showToast({ title: "璇疯嚦灏戦厤缃竴涓笅鎷夐�夐」", icon: "none" });
+ return;
+ }
+ }
+ const payload = buildFieldConfigPayload(
+ { ...fieldDraft, key: draftKey },
+ existingKey
+ );
+ if (editingFieldIndex.value >= 0) {
+ formConfig.fields.splice(editingFieldIndex.value, 1, payload);
+ } else {
+ formConfig.fields.push(payload);
+ }
+ closeFieldEditor();
+ };
+
+ const removeField = index => {
+ const field = formConfig.fields[index];
+ if (isFieldLocked(field)) {
+ uni.showToast({ title: "绯荤粺鍐呯疆濉姤椤逛笉鍙垹闄�", icon: "none" });
+ return;
+ }
+ formConfig.fields.splice(index, 1);
+ };
+
+ const addNode = () => {
+ flowNodes.value.push(createNode());
+ };
+
+ const removeNode = index => {
+ if (flowNodes.value.length <= 1) {
+ uni.showToast({ title: "鑷冲皯淇濈暀涓�涓鎵硅妭鐐�", icon: "none" });
+ return;
+ }
+ flowNodes.value.splice(index, 1);
+ };
+
+ const openUserPicker = nodeIndex => {
+ editingNodeIndex.value = nodeIndex;
+ pickerSelectedIds.value = [];
+ showUserPicker.value = true;
+ };
+
+ const closeUserPicker = () => {
+ showUserPicker.value = false;
+ editingNodeIndex.value = -1;
+ pickerSelectedIds.value = [];
+ };
+
+ const isUserSelected = userId => pickerSelectedIds.value.includes(userId);
+
+ const toggleUser = user => {
+ const ids = pickerSelectedIds.value;
+ const index = ids.indexOf(user.userId);
+ if (index >= 0) {
+ ids.splice(index, 1);
+ } else {
+ ids.push(user.userId);
+ }
+ };
+
+ const confirmUserPicker = () => {
+ const node = flowNodes.value[editingNodeIndex.value];
+ if (!node) {
+ closeUserPicker();
+ return;
+ }
+ const selectedUsers = userList.value.filter(user =>
+ pickerSelectedIds.value.includes(user.userId)
+ );
+ if (!selectedUsers.length) {
+ uni.showToast({ title: "璇烽�夋嫨瀹℃壒浜�", icon: "none" });
+ return;
+ }
+ const startSort = node.approvers.length;
+ selectedUsers.forEach((user, idx) => {
+ node.approvers.push({
+ approverId: user.userId,
+ approverName: user.nickName,
+ sortNo: startSort + idx + 1,
+ });
+ });
+ closeUserPicker();
+ };
+
+ const removeApprover = (nodeIndex, approverIndex) => {
+ const node = flowNodes.value[nodeIndex];
+ if (!node?.approvers?.length) return;
+ const next = node.approvers
+ .filter((_, idx) => idx !== approverIndex)
+ .map((item, idx) => ({
+ ...item,
+ sortNo: idx + 1,
+ }));
+ node.approvers = next;
+ };
+
+ const validateFlow = () => {
+ if (!flowNodes.value.length) {
+ uni.showToast({ title: "璇烽厤缃鎵规祦绋�", icon: "none" });
+ return false;
+ }
+ const emptyNode = flowNodes.value.find(node => !node.approvers.length);
+ if (emptyNode) {
+ uni.showToast({ title: "璇蜂负姣忎釜瀹℃壒鑺傜偣娣诲姞瀹℃壒浜�", icon: "none" });
+ return false;
+ }
+ return true;
+ };
+
+ const buildSubmitPayload = () => {
+ const tid = templateId.value;
+ const payload = {
+ templateName: form.templateName.trim(),
+ enabled: form.enabled,
+ description: form.description?.trim() || "",
+ businessType: form.businessType,
+ templateType: form.templateType,
+ formConfig: JSON.stringify({
+ prompt: formConfig.prompt?.trim() || "",
+ fields: formConfig.fields,
+ }),
+ nodes: flowNodes.value.map((node, index) => {
+ const nodePayload = {
+ levelNo: index + 1,
+ approveType: node.approveType,
+ approvers: node.approvers.map((approver, approverIndex) => {
+ const approverPayload = {
+ approverId: approver.approverId,
+ approverName: approver.approverName,
+ sortNo: approverIndex + 1,
+ };
+ if (isEditMode.value) {
+ if (approver.id != null) approverPayload.id = approver.id;
+ if (approver.nodeId != null) approverPayload.nodeId = approver.nodeId;
+ else if (node.id != null) approverPayload.nodeId = node.id;
+ if (approver.templateId != null) approverPayload.templateId = approver.templateId;
+ else if (tid != null) approverPayload.templateId = tid;
+ }
+ return approverPayload;
+ }),
+ };
+ if (isEditMode.value) {
+ if (node.id != null) nodePayload.id = node.id;
+ if (node.templateId != null) nodePayload.templateId = node.templateId;
+ else if (tid != null) nodePayload.templateId = tid;
+ }
+ return nodePayload;
+ }),
+ };
+
+ if (isEditMode.value) {
+ payload.id = tid;
+ }
+
+ return payload;
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid || !validateFlow()) return;
+
+ submitting.value = true;
+ const submitApi = isEditMode.value ? updateApprovalTemplate : addApprovalTemplate;
+ submitApi(buildSubmitPayload())
+ .then(() => {
+ uni.showToast({
+ title: isEditMode.value ? "淇敼鎴愬姛" : "淇濆瓨鎴愬姛",
+ icon: "success",
+ });
+ uni.removeStorageSync(EDIT_STORAGE_KEY);
+ setTimeout(() => {
+ uni.navigateBack();
+ }, 300);
+ })
+ .catch(() => {
+ uni.showToast({
+ title: isEditMode.value ? "淇敼澶辫触" : "淇濆瓨澶辫触",
+ icon: "none",
+ });
+ })
+ .finally(() => {
+ submitting.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ const row = uni.getStorageSync(EDIT_STORAGE_KEY);
+ if (row && String(row.id) === String(options.id)) {
+ fillFormFromRow(row);
+ } else {
+ templateId.value = options.id;
+ uni.showToast({ title: "鏈幏鍙栧埌妯℃澘鏁版嵁", icon: "none" });
+ }
+ uni.removeStorageSync(EDIT_STORAGE_KEY);
+ }
+ });
+
+ const loadTemplateTypes = () =>
+ fetchApprovalTemplateTypes()
+ .then(opts => {
+ businessTypeOptions.value = opts;
+ if (!templateId.value && opts.length) {
+ const matched = opts.some(
+ opt => String(opt.value) === String(form.businessType)
+ );
+ if (!matched) {
+ form.businessType = opts[0].value;
+ }
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇瀹℃壒绫诲瀷澶辫触", icon: "none" });
+ });
+
+ onMounted(() => {
+ loadTemplateTypes();
+ userListNoPageByTenantId()
+ .then(res => {
+ userList.value = res?.data || [];
+ })
+ .catch(() => {
+ userList.value = [];
+ });
+ getDept()
+ .then(res => {
+ deptList.value = res?.data || [];
+ })
+ .catch(() => {
+ deptList.value = [];
+ });
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ $primary: #2979ff;
+ $primary-light: #ecf5ff;
+ $text: #1f2d3d;
+ $text-secondary: #606266;
+ $text-muted: #909399;
+ $border: #ebeef5;
+ $bg-page: #f0f3f8;
+ $radius-lg: 12px;
+ $radius-md: 10px;
+ $shadow-card: 0 2px 12px rgba(31, 45, 61, 0.05);
+
+ .template-edit-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ background: $bg-page;
+ }
+
+ .form-scroll {
+ flex: 1;
+ height: 0;
+ padding: 10px 12px calc(96px + env(safe-area-inset-bottom));
+ }
+
+ .section-card {
+ margin-bottom: 10px;
+ background: #fff;
+ border-radius: $radius-lg;
+ overflow: hidden;
+ box-shadow: $shadow-card;
+ }
+
+ .section-head {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f7;
+
+ &--between {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ }
+
+ .section-head-left {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .section-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: $text;
+ padding-left: 10px;
+ border-left: 3px solid $primary;
+ line-height: 1.2;
+ }
+
+ .section-count {
+ font-size: 12px;
+ color: $text-muted;
+ padding-left: 13px;
+ }
+
+ .head-actions {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ .head-link {
+ font-size: 14px;
+ color: $text-secondary;
+
+ &--import {
+ color: $text-secondary;
+ padding: 6px 12px;
+ border: 1px solid #dce3ed;
+ border-radius: 8px;
+ background: #fff;
+ font-size: 13px;
+ }
+
+ &--disabled {
+ color: #c0c4cc;
+ border-color: #ebeef5;
+ background: #f5f7fa;
+ }
+
+ &--primary {
+ color: #fff;
+ font-weight: 500;
+ padding: 6px 14px;
+ border: none;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #4d8dff 0%, #2979ff 100%);
+ box-shadow: 0 2px 8px rgba(41, 121, 255, 0.25);
+ }
+ }
+
+ :deep(.form-item-select--disabled .u-form-item__body) {
+ opacity: 0.65;
+ }
+
+ .section-body {
+ padding: 2px 16px 14px;
+ }
+
+ .form-section {
+ margin-bottom: 10px;
+ border-radius: $radius-lg;
+ overflow: hidden;
+ box-shadow: $shadow-card;
+ }
+
+ :deep(.form-section .u-cell-group__title) {
+ padding: 12px 16px 8px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: $text !important;
+ background: #fff !important;
+ }
+
+ :deep(.form-section .u-form-item) {
+ padding: 0 16px !important;
+ }
+
+ :deep(.form-section .u-form-item__body) {
+ padding: 10px 0 !important;
+ min-height: auto !important;
+ }
+
+ :deep(.form-item-name .u-form-item__body) {
+ flex-direction: row !important;
+ align-items: center !important;
+ }
+
+ :deep(.form-item-name .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ justify-content: flex-end !important;
+ }
+
+ :deep(.name-input-inline),
+ :deep(.name-input-inline .u-input__content) {
+ width: 100% !important;
+ flex: 1 !important;
+ }
+
+ :deep(.name-input-inline input),
+ :deep(.name-input-inline .u-input__content__field-wrapper__field) {
+ width: 100% !important;
+ text-align: right !important;
+ font-size: 15px !important;
+ }
+
+ :deep(.form-item-select .u-form-item__body) {
+ align-items: center !important;
+ }
+
+ :deep(.form-item-select .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ justify-content: flex-end !important;
+ }
+
+ :deep(.form-item-select .u-input__content__field-wrapper__field) {
+ text-align: right !important;
+ }
+
+ :deep(.form-item-switch .u-form-item__body) {
+ flex-direction: row !important;
+ align-items: center !important;
+ }
+
+ :deep(.form-item-switch .u-form-item__content) {
+ flex: 1 !important;
+ min-width: 0 !important;
+ display: flex !important;
+ justify-content: flex-end !important;
+ }
+
+ .switch-wrap {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ width: 100%;
+ }
+
+ :deep(.form-item-desc .u-form-item__body) {
+ flex-direction: column !important;
+ align-items: stretch !important;
+ padding: 10px 0 12px !important;
+ }
+
+ :deep(.form-item-desc .u-form-item__content) {
+ justify-content: stretch !important;
+ width: 100% !important;
+ }
+
+ .desc-input-shell {
+ width: 100%;
+ box-sizing: border-box;
+ padding: 8px 12px;
+ background: #fff;
+ border: 1px solid #dcdfe6;
+ border-radius: 6px;
+ }
+
+ :deep(.desc-input-shell .u-textarea),
+ :deep(.desc-input-shell textarea) {
+ width: 100% !important;
+ font-size: 15px !important;
+ text-align: left !important;
+ }
+
+ .form-row-item {
+ margin: 0 !important;
+ padding: 0 !important;
+ }
+
+ :deep(.form-row-item .u-form-item__body) {
+ padding: 0;
+ }
+
+ :deep(.form-row-item .u-form-item__body__right__message) {
+ margin-top: 4px;
+ padding-left: 0;
+ }
+
+ .form-row {
+ padding: 10px 0;
+ border-bottom: 1px solid #f5f7fa;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &--column {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 8px;
+ }
+
+ &--compact {
+ padding-top: 8px;
+ }
+ }
+
+ .form-row-label {
+ display: block;
+ font-size: 14px;
+ color: $text-secondary;
+ margin-bottom: 8px;
+
+ &.required::before {
+ content: "*";
+ color: #f56c6c;
+ margin-right: 3px;
+ }
+ }
+
+ .form-row--column .form-row-label {
+ margin-bottom: 0;
+ }
+
+ .prompt-row {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ margin-bottom: 4px;
+ border-bottom: 1px solid #f5f7fa;
+ gap: 8px;
+ }
+
+ .prompt-label {
+ flex-shrink: 0;
+ width: 88px;
+ font-size: 14px;
+ color: $text-secondary;
+ }
+
+ .prompt-input {
+ flex: 1;
+ min-width: 0;
+ }
+
+ :deep(.prompt-input),
+ :deep(.prompt-input .u-input__content) {
+ width: 100% !important;
+ }
+
+ :deep(.prompt-input input),
+ :deep(.prompt-input .u-input__content__field-wrapper__field) {
+ width: 100% !important;
+ text-align: right !important;
+ font-size: 15px !important;
+ }
+
+ .input-box,
+ .textarea-box {
+ background: #f7f9fc;
+ border-radius: 10px;
+ border: 1px solid #eef1f6;
+ overflow: hidden;
+ }
+
+ .textarea-box {
+ padding: 4px 0;
+ }
+
+ .field-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 8px;
+ }
+
+ .field-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px;
+ background: #fff;
+ border-radius: $radius-md;
+ border: 1px solid #e8eef5;
+ box-shadow: 0 1px 4px rgba(31, 45, 61, 0.04);
+ transition: border-color 0.2s, box-shadow 0.2s;
+
+ &:active:not(.field-item--locked) {
+ border-color: #c6daf5;
+ box-shadow: 0 2px 8px rgba(41, 121, 255, 0.08);
+ }
+
+ &--locked {
+ background: #fafbfd;
+ }
+ }
+
+ .field-order {
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ font-size: 13px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ .field-main {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .field-title-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 4px;
+ }
+
+ .field-name {
+ font-size: 15px;
+ font-weight: 600;
+ color: $text;
+ flex: 1;
+ min-width: 0;
+ }
+
+ .field-tags {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+ }
+
+ .field-key {
+ display: block;
+ font-size: 12px;
+ color: $text-muted;
+ font-family: ui-monospace, monospace;
+ }
+
+ .field-lock-tag {
+ flex-shrink: 0;
+ font-size: 11px;
+ color: #909399;
+ padding: 4px 8px;
+ background: #f0f2f5;
+ border-radius: 4px;
+ }
+
+ .type-tag {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 4px;
+
+ &--text {
+ color: #2979ff;
+ background: #ecf5ff;
+ }
+
+ &--area {
+ color: #7c5cfc;
+ background: #f3efff;
+ }
+
+ &--num {
+ color: #e6a23c;
+ background: #fdf6ec;
+ }
+
+ &--date {
+ color: #18a058;
+ background: #e8faf0;
+ }
+
+ &--select {
+ color: #9c27b0;
+ background: #f6edfc;
+ }
+ }
+
+ .req-tag {
+ font-size: 11px;
+ padding: 2px 6px;
+ color: #f56c6c;
+ background: #fef0f0;
+ border-radius: 4px;
+ }
+
+ .field-default {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+ color: $text-muted;
+ }
+
+ .field-actions {
+ display: flex;
+ gap: 6px;
+ flex-shrink: 0;
+ }
+
+ .icon-btn {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &--edit {
+ background: #ecf5ff;
+ }
+
+ &--del {
+ background: #fef0f0;
+ }
+ }
+
+ .empty-mini {
+ padding: 32px 16px;
+ text-align: center;
+ font-size: 13px;
+ color: $text-muted;
+ background: #fafbfd;
+ border: 1px dashed #dce8f5;
+ border-radius: 10px;
+ }
+
+ .flow-wrap {
+ padding: 10px 16px 14px;
+ }
+
+ .flow-node-block {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .flow-node-card {
+ background: #fafbfd;
+ border: 1px solid #e8eef5;
+ border-radius: $radius-md;
+ padding: 12px;
+ }
+
+ .node-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 10px;
+ }
+
+ .node-level-badge {
+ width: 26px;
+ height: 26px;
+ border-radius: 8px;
+ background: $primary;
+ color: #fff;
+ font-size: 14px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ .node-level-text {
+ flex: 1;
+ font-size: 15px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .node-delete {
+ padding: 6px;
+ flex-shrink: 0;
+ }
+
+ .approve-type-row {
+ display: flex;
+ background: #f0f3f8;
+ border-radius: 8px;
+ padding: 3px;
+ margin-bottom: 10px;
+ }
+
+ .type-btn {
+ flex: 1;
+ text-align: center;
+ padding: 8px 0;
+ font-size: 14px;
+ color: $text-secondary;
+ border-radius: 6px;
+
+ &.active {
+ background: #fff;
+ color: $primary;
+ font-weight: 500;
+ }
+ }
+
+ .approver-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .approver-chip {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 12px 6px 6px;
+ background: #fff;
+ border: 1px solid #dce8f8;
+ border-radius: 24px;
+ box-shadow: 0 2px 6px rgba(41, 121, 255, 0.06);
+ }
+
+ .approver-avatar {
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .approver-name {
+ font-size: 13px;
+ color: $text;
+ max-width: 80px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .approver-remove {
+ flex-shrink: 0;
+ width: 22px;
+ height: 22px;
+ margin-left: 2px;
+ border-radius: 50%;
+ background: #f2f3f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .approver-remove--active {
+ background: #fde2e2;
+ }
+
+ .remove-icon {
+ font-size: 16px;
+ line-height: 1;
+ color: #909399;
+ font-weight: 300;
+ }
+
+ .add-approver {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 8px 14px;
+ border: 1.5px dashed #a8cfff;
+ border-radius: 24px;
+ background: $primary-light;
+ color: $primary;
+ font-size: 13px;
+ }
+
+ .flow-connector {
+ display: flex;
+ justify-content: center;
+ padding: 4px 0;
+ }
+
+ .flow-connector-line {
+ width: 2px;
+ height: 14px;
+ background: #d0dff0;
+ }
+
+ .add-node-bar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ margin-top: 8px;
+ padding: 11px;
+ border: 1px dashed #c6daf5;
+ border-radius: $radius-md;
+ color: $primary;
+ font-size: 14px;
+ }
+
+ .sheet-handle {
+ width: 36px;
+ height: 4px;
+ margin: 10px auto 4px;
+ background: #d8dde6;
+ border-radius: 2px;
+ }
+
+ .field-editor .sheet-handle {
+ background: #c8ced8;
+ }
+
+ .field-editor {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ max-height: 88vh;
+ background: #f5f7fb;
+ border-radius: 16px 16px 0 0;
+ overflow: hidden;
+ }
+
+ .user-picker {
+ position: relative;
+ padding: 0 18px calc(18px + env(safe-area-inset-bottom));
+ background: #fff;
+ max-height: 85vh;
+ }
+
+ .editor-header {
+ padding: 4px 20px 12px;
+ background: #fff;
+ text-align: center;
+ border-bottom: 1px solid #f0f2f5;
+ }
+
+ .editor-subtitle {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+ color: $text-muted;
+ }
+
+ .editor-picker-layer {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ z-index: 20;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ }
+
+ .editor-picker-mask {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.45);
+ }
+
+ .editor-picker-panel {
+ position: relative;
+ z-index: 1;
+ background: #fff;
+ border-radius: 16px 16px 0 0;
+ max-height: 55vh;
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+
+ .editor-picker-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 18px;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .editor-picker-cancel {
+ font-size: 15px;
+ color: #909399;
+ min-width: 48px;
+ }
+
+ .editor-picker-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .editor-picker-placeholder {
+ min-width: 48px;
+ }
+
+ .editor-picker-scroll {
+ max-height: calc(55vh - 52px);
+ }
+
+ .editor-picker-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 18px;
+ font-size: 16px;
+ color: $text;
+ border-bottom: 1px solid #f5f7fa;
+
+ &--active {
+ color: $primary;
+ background: #f5f9ff;
+ }
+
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+
+ .editor-scroll {
+ flex: 1;
+ height: 0;
+ max-height: 62vh;
+ }
+
+ .editor-form {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px 16px 16px;
+ }
+
+ .editor-section-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 14px 14px 4px;
+ box-shadow: 0 1px 6px rgba(31, 45, 61, 0.05);
+ }
+
+ .editor-section-head {
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #f2f4f7;
+ }
+
+ .editor-section-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: $text;
+ padding-left: 8px;
+ border-left: 3px solid $primary;
+ line-height: 1.2;
+ }
+
+ .editor-cell {
+ margin-bottom: 14px;
+
+ &--tap:active .picker-value-row {
+ background: #eef4ff;
+ border-color: #c6daf5;
+ }
+
+ &--switch {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 4px 0 10px;
+ margin-bottom: 4px;
+ }
+
+ &--value {
+ margin-bottom: 10px;
+ }
+ }
+
+ .switch-label-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .switch-hint {
+ font-size: 12px;
+ color: $text-muted;
+ }
+
+ .editor-input-box {
+ background: #f7f9fc;
+ border: 1px solid #e8ecf2;
+ border-radius: 10px;
+ overflow: hidden;
+ }
+
+ :deep(.editor-input-box .u-input) {
+ background: transparent !important;
+ }
+
+ .default-hint {
+ display: block;
+ font-size: 12px;
+ color: $text-muted;
+ line-height: 1.5;
+ margin: -4px 0 10px;
+ padding: 0 2px;
+ }
+
+ .manual-options {
+ margin: 4px 0 12px;
+ padding-top: 4px;
+ }
+
+ .manual-options-title {
+ display: block;
+ font-size: 12px;
+ color: $text-muted;
+ margin-bottom: 10px;
+ }
+
+ .manual-options-table {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .option-table-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 4px 4px;
+ }
+
+ .option-col {
+ font-size: 12px;
+ color: $text-muted;
+ font-weight: 500;
+
+ &--idx {
+ width: 22px;
+ flex-shrink: 0;
+ }
+
+ &--label {
+ flex: 1.4;
+ min-width: 0;
+ }
+
+ &--value {
+ flex: 0.9;
+ min-width: 72px;
+ }
+
+ &--action {
+ width: 32px;
+ flex-shrink: 0;
+ }
+ }
+
+ .option-card {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ background: #f8fafc;
+ border: 1px solid #e8ecf2;
+ border-radius: 10px;
+ }
+
+ .option-idx {
+ width: 22px;
+ height: 22px;
+ flex-shrink: 0;
+ border-radius: 6px;
+ background: #eef2f8;
+ color: $text-muted;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 22px;
+ text-align: center;
+ }
+
+ .option-input-wrap {
+ flex: 1.4;
+ min-width: 0;
+ background: #fff;
+ border: 1px solid #e4e8ef;
+ border-radius: 8px;
+ overflow: hidden;
+
+ &--value {
+ flex: 0.9;
+ min-width: 72px;
+ }
+ }
+
+ :deep(.option-input-wrap .u-input) {
+ background: transparent !important;
+ }
+
+ :deep(.option-input-wrap input),
+ :deep(.option-input-wrap .u-input__content__field-wrapper__field) {
+ font-size: 14px !important;
+ height: 36px !important;
+ min-height: 36px !important;
+ padding: 0 10px !important;
+ }
+
+ .option-del {
+ flex-shrink: 0;
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ background: #fff;
+ border: 1px solid #fde2e2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .option-del--active {
+ background: #fef0f0;
+ }
+
+ .add-option-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ margin-top: 10px;
+ padding: 11px;
+ border: 1.5px dashed #b8d4ff;
+ border-radius: 10px;
+ background: linear-gradient(180deg, #f8fbff 0%, #f0f6ff 100%);
+ color: $primary;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .add-option-btn--active {
+ background: #e8f2ff;
+ border-color: $primary;
+ }
+
+ .option-source-tip {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ padding: 10px 12px;
+ margin-bottom: 10px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ font-size: 12px;
+ color: $text-muted;
+ line-height: 1.5;
+ }
+
+ .editor-title {
+ display: block;
+ font-size: 17px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .picker-value-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 44px;
+ padding: 0 14px;
+ background: #f7f9fc;
+ border: 1px solid #e8ecf2;
+ border-radius: 10px;
+ gap: 8px;
+ transition: background 0.15s, border-color 0.15s;
+
+ &--tap:active {
+ background: #eef4ff;
+ border-color: #c6daf5;
+ }
+ }
+
+ .picker-value {
+ flex: 1;
+ min-width: 0;
+ font-size: 15px;
+ color: $text;
+ text-align: left;
+ line-height: 1.4;
+
+ &--placeholder {
+ color: #c0c4cc;
+ }
+ }
+
+ .editor-label {
+ display: block;
+ font-size: 13px;
+ font-weight: 500;
+ color: $text-secondary;
+ margin-bottom: 8px;
+
+ &.required::before {
+ content: "*";
+ color: #f56c6c;
+ margin-right: 3px;
+ }
+ }
+
+ .editor-cell--switch .editor-label {
+ margin-bottom: 0;
+ }
+
+ .daterange-default-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .daterange-default-item {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .daterange-default-label {
+ font-size: 13px;
+ color: $text-secondary;
+ }
+
+ .type-chip-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ }
+
+ .type-chip {
+ text-align: center;
+ padding: 10px 6px;
+ font-size: 13px;
+ color: $text-secondary;
+ background: #f7f9fc;
+ border: 1px solid #eef1f6;
+ border-radius: 8px;
+
+ &.active {
+ color: $primary;
+ background: $primary-light;
+ border-color: $primary;
+ font-weight: 500;
+ }
+ }
+
+ .editor-footer {
+ display: flex;
+ gap: 12px;
+ padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
+ background: #fff;
+ border-top: 1px solid #eef0f4;
+ box-shadow: 0 -4px 12px rgba(31, 45, 61, 0.06);
+ }
+
+ .editor-btn {
+ flex: 1;
+ text-align: center;
+ padding: 12px 0;
+ border-radius: 10px;
+ font-size: 15px;
+ font-weight: 500;
+
+ &--cancel {
+ color: $text-secondary;
+ background: #f5f7fa;
+ border: 1px solid #e4e7ed;
+ }
+
+ &--confirm {
+ color: #fff;
+ background: linear-gradient(135deg, #4d8dff 0%, #2979ff 100%);
+ box-shadow: 0 4px 12px rgba(41, 121, 255, 0.35);
+ }
+
+ &--confirm:active {
+ opacity: 0.9;
+ }
+ }
+
+ .picker-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-bottom: 14px;
+ border-bottom: 1px solid #f5f7fa;
+ margin-bottom: 8px;
+ }
+
+ .picker-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: $text;
+ }
+
+ .picker-cancel {
+ font-size: 15px;
+ color: $text-muted;
+ min-width: 48px;
+ }
+
+ .picker-confirm {
+ font-size: 15px;
+ color: $primary;
+ font-weight: 600;
+ min-width: 48px;
+ text-align: right;
+ }
+
+ .user-scroll {
+ max-height: 52vh;
+ }
+
+ .user-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 4px;
+ border-bottom: 1px solid #f5f7fa;
+ border-radius: 10px;
+ margin-bottom: 4px;
+ transition: background 0.2s;
+
+ &.selected {
+ background: #f5f9ff;
+ }
+ }
+
+ .user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 12px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ font-size: 16px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ .user-name {
+ flex: 1;
+ font-size: 15px;
+ color: $text;
+ }
+
+ .user-check {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: 2px solid #dcdfe6;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ &.checked {
+ background: $primary;
+ border-color: $primary;
+ }
+ }
+</style>
diff --git a/src/pages/oa/ApproveManage/approve-template/index.vue b/src/pages/oa/ApproveManage/approve-template/index.vue
new file mode 100644
index 0000000..5215afa
--- /dev/null
+++ b/src/pages/oa/ApproveManage/approve-template/index.vue
@@ -0,0 +1,322 @@
+<!--
+ OA / 瀹℃壒绠$悊 / 瀹℃壒妯℃澘
+ 璺敱锛�/pages/oa/ApproveManage/approve-template/index
+-->
+<template>
+ <view class="approve-template-page sales-account">
+ <PageHeader title="瀹℃壒妯℃澘"
+ @back="goBack" />
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input v-model="queryParams.templateName"
+ class="search-text"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ clearable
+ @confirm="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="24"
+ color="#999" />
+ </view>
+ </view>
+ </view>
+
+ <scroll-view class="list-scroll"
+ scroll-y
+ :show-scrollbar="false"
+ @scrolltolower="loadMore">
+ <view v-if="list.length"
+ class="ledger-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff" />
+ </view>
+ <text class="item-id">{{ item.templateName || "-" }}</text>
+ </view>
+ <u-tag :type="enabledTagType(item.enabled)"
+ :text="enabledText(item.enabled)" />
+ </view>
+ <up-divider />
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">瀹℃壒绫诲瀷</text>
+ <text class="detail-value">{{ businessTypeText(item.businessType) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹℃壒鑺傜偣</text>
+ <text class="detail-value">{{ nodeCount(item) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妯℃澘璇存槑</text>
+ <text class="detail-value">{{ item.description || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍒涘缓浜�</text>
+ <text class="detail-value">{{ item.createdUserName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍒涘缓鏃堕棿</text>
+ <text class="detail-value">{{ item.createTime || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ @click.stop="goDetail(item)">
+ 璇︽儏
+ </up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click.stop="goEdit(item)">
+ 缂栬緫
+ </up-button>
+ <up-button v-if="!isSystemTemplate(item)"
+ class="action-btn"
+ size="small"
+ type="error"
+ plain
+ @click.stop="handleDelete(item)">
+ 鍒犻櫎
+ </up-button>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="list"
+ text="鏆傛棤瀹℃壒妯℃澘鏁版嵁" />
+ </view>
+ </scroll-view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import {
+ deleteApprovalTemplate,
+ listApprovalTemplatePage,
+ } from "@/api/oa/approvalTemplate.js";
+ import {
+ buildTypeLabelMap,
+ fetchApprovalTemplateTypes,
+ getTemplateTypeLabel,
+ isSystemApprovalTemplate,
+ } from "../../_utils/approvalTemplateType.js";
+
+ const EDIT_STORAGE_KEY = "oa_approve_template_edit_row";
+ const typeLabelMap = ref({});
+
+ const queryParams = reactive({
+ templateName: "",
+ });
+
+ const list = ref([]);
+ const pageStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ const buildListParams = () => ({
+ page: {
+ current: page.current,
+ size: page.size,
+ },
+ approvalTemplateDto: {
+ templateName: queryParams.templateName?.trim() || undefined,
+ },
+ });
+
+ const enabledText = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "鍚敤";
+ if (val === "0") return "鍋滅敤";
+ return "-";
+ };
+
+ const enabledTagType = enabled => {
+ const val = String(enabled ?? "");
+ if (val === "1") return "success";
+ if (val === "0") return "info";
+ return "info";
+ };
+
+ const businessTypeText = type =>
+ getTemplateTypeLabel(type, typeLabelMap.value);
+
+ const isSystemTemplate = isSystemApprovalTemplate;
+
+ const loadTemplateTypes = () =>
+ fetchApprovalTemplateTypes()
+ .then(opts => {
+ typeLabelMap.value = buildTypeLabelMap(opts);
+ })
+ .catch(() => {});
+
+ const nodeCount = item => {
+ const count = item?.nodes?.length;
+ return count != null ? `${count} 涓猔 : "-";
+ };
+
+ const getList = () => {
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+ pageStatus.value = "loading";
+ listApprovalTemplatePage(buildListParams())
+ .then(res => {
+ const pageData = res?.data || {};
+ const records = pageData.records || [];
+ const total = pageData.total ?? 0;
+
+ if (page.current === 1) {
+ list.value = records;
+ } else {
+ list.value = [...list.value, ...records];
+ }
+
+ page.total = total;
+ if (list.value.length >= total || records.length < page.size) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current += 1;
+ }
+ })
+ .catch(() => {
+ if (page.current === 1) {
+ list.value = [];
+ }
+ pageStatus.value = "loadmore";
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const loadMore = () => {
+ if (pageStatus.value === "loadmore") {
+ getList();
+ }
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const goAdd = () => {
+ uni.removeStorageSync(EDIT_STORAGE_KEY);
+ uni.navigateTo({
+ url: "/pages/oa/ApproveManage/approve-template/edit",
+ });
+ };
+
+ const goDetail = item => {
+ if (!item?.id) return;
+ uni.navigateTo({
+ url: `/pages/oa/ApproveManage/approve-template/detail?id=${item.id}`,
+ });
+ };
+
+ const goEdit = item => {
+ if (!item?.id) return;
+ uni.setStorageSync(EDIT_STORAGE_KEY, item);
+ uni.navigateTo({
+ url: `/pages/oa/ApproveManage/approve-template/edit?id=${item.id}`,
+ });
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ if (isSystemTemplate(item)) {
+ uni.showToast({ title: "绯荤粺鍐呯疆妯℃澘涓嶅彲鍒犻櫎", icon: "none" });
+ return;
+ }
+ const name = item.templateName || "璇ユā鏉�";
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: `纭畾鍒犻櫎銆�${name}銆嶅悧锛熷垹闄ゅ悗鏃犳硶鎭㈠銆俙,
+ confirmText: "鍒犻櫎",
+ confirmColor: "#f56c6c",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "鍒犻櫎涓�...", mask: true });
+ deleteApprovalTemplate([item.id])
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ handleSearch();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ loadTemplateTypes();
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .approve-template-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ }
+
+ .list-scroll {
+ flex: 1;
+ height: 0;
+ padding-bottom: calc(80px + env(safe-area-inset-bottom));
+ }
+
+ .empty-wrap {
+ padding: 48px 20px;
+ }
+
+ .action-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid #f0f0f0;
+ }
+
+ .action-btn {
+ min-width: 72px;
+ }
+</style>
diff --git a/src/pages/oa/AttendManage/leave-apply/index.vue b/src/pages/oa/AttendManage/leave-apply/index.vue
new file mode 100644
index 0000000..feb5e4c
--- /dev/null
+++ b/src/pages/oa/AttendManage/leave-apply/index.vue
@@ -0,0 +1,12 @@
+<!--
+ OA / 鍋囧嫟绠$悊 / 璇峰亣鐢宠
+ 璺敱锛�/pages/oa/AttendManage/leave-apply/index
+-->
+<template>
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.LEAVE" />
+</template>
+
+<script setup>
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+</script>
diff --git a/src/pages/oa/AttendManage/overtime-apply/index.vue b/src/pages/oa/AttendManage/overtime-apply/index.vue
new file mode 100644
index 0000000..439ea26
--- /dev/null
+++ b/src/pages/oa/AttendManage/overtime-apply/index.vue
@@ -0,0 +1,12 @@
+<!--
+ OA / 鍋囧嫟绠$悊 / 鍔犵彮鐢宠
+ 璺敱锛�/pages/oa/AttendManage/overtime-apply/index
+-->
+<template>
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.OVERTIME" />
+</template>
+
+<script setup>
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+</script>
diff --git a/src/pages/oa/ContractManage/purchase-contract/index.vue b/src/pages/oa/ContractManage/purchase-contract/index.vue
new file mode 100644
index 0000000..ac2eb6b
--- /dev/null
+++ b/src/pages/oa/ContractManage/purchase-contract/index.vue
@@ -0,0 +1,26 @@
+<!--
+ OA / 鍚堝悓绠$悊 / 閲囪喘鍚堝悓
+ 璺敱锛�/pages/oa/ContractManage/purchase-contract/index
+ 璇存槑锛氳烦杞嚦閲囪喘鍙拌处 /pages/procurementManagement/procurementLedger/index
+-->
+<template>
+ <view class="redirect-page" />
+</template>
+
+<script setup>
+ /** OA - 鍚堝悓绠$悊 - 閲囪喘鍚堝悓锛堣烦杞噰璐彴璐︼級 */
+ import { onLoad } from "@dcloudio/uni-app";
+
+ onLoad(() => {
+ uni.redirectTo({
+ url: "/pages/procurementManagement/procurementLedger/index",
+ });
+ });
+</script>
+
+<style scoped>
+ .redirect-page {
+ min-height: 100vh;
+ background: #f8f9fa;
+ }
+</style>
diff --git a/src/pages/oa/ContractManage/sale-contract/index.vue b/src/pages/oa/ContractManage/sale-contract/index.vue
new file mode 100644
index 0000000..a05f4e9
--- /dev/null
+++ b/src/pages/oa/ContractManage/sale-contract/index.vue
@@ -0,0 +1,26 @@
+<!--
+ OA / 鍚堝悓绠$悊 / 閿�鍞悎鍚�
+ 璺敱锛�/pages/oa/ContractManage/sale-contract/index
+ 璇存槑锛氳烦杞嚦閿�鍞彴璐� /pages/sales/salesAccount/index
+-->
+<template>
+ <view class="redirect-page" />
+</template>
+
+<script setup>
+ /** OA - 鍚堝悓绠$悊 - 閿�鍞悎鍚岋紙璺宠浆閿�鍞彴璐︼級 */
+ import { onLoad } from "@dcloudio/uni-app";
+
+ onLoad(() => {
+ uni.redirectTo({
+ url: "/pages/sales/salesAccount/index",
+ });
+ });
+</script>
+
+<style scoped>
+ .redirect-page {
+ min-height: 100vh;
+ background: #f8f9fa;
+ }
+</style>
diff --git a/src/pages/oa/EnterpriseNews/news-manage/index.vue b/src/pages/oa/EnterpriseNews/news-manage/index.vue
new file mode 100644
index 0000000..67a2d1a
--- /dev/null
+++ b/src/pages/oa/EnterpriseNews/news-manage/index.vue
@@ -0,0 +1,18 @@
+<!--
+ OA / 浼佷笟鏂伴椈
+ 璺敱锛�/pages/oa/EnterpriseNews/news-manage/index
+-->
+<template>
+ <OaListPage v-if="config"
+ :page-key="pageKey"
+ :page-config="config" />
+</template>
+
+<script setup>
+ /** OA - 浼佷笟鏂伴椈 */
+ import OaListPage from "../../_components/OaListPage.vue";
+ import { useOaPage } from "../../_utils/useOaPage.js";
+
+ const pageKey = "EnterpriseNews/news-manage";
+ const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/post-manage/index.vue b/src/pages/oa/HrManage/post-manage/index.vue
new file mode 100644
index 0000000..75be5aa
--- /dev/null
+++ b/src/pages/oa/HrManage/post-manage/index.vue
@@ -0,0 +1,18 @@
+<!--
+ OA / 浜轰簨绠$悊 / 宀椾綅绠$悊
+ 璺敱锛�/pages/oa/HrManage/post-manage/index
+-->
+<template>
+ <OaListPage v-if="config"
+ :page-key="pageKey"
+ :page-config="config" />
+</template>
+
+<script setup>
+ /** OA - 浜轰簨绠$悊 - 宀椾綅绠$悊 */
+ import OaListPage from "../../_components/OaListPage.vue";
+ import { useOaPage } from "../../_utils/useOaPage.js";
+
+ const pageKey = "HrManage/post-manage";
+ const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/regular-apply/index.vue b/src/pages/oa/HrManage/regular-apply/index.vue
new file mode 100644
index 0000000..e45364a
--- /dev/null
+++ b/src/pages/oa/HrManage/regular-apply/index.vue
@@ -0,0 +1,12 @@
+<!--
+ OA / 浜轰簨绠$悊 / 杞鐢宠
+ 璺敱锛�/pages/oa/HrManage/regular-apply/index
+-->
+<template>
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.REGULAR" />
+</template>
+
+<script setup>
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+</script>
diff --git a/src/pages/oa/HrManage/resign-apply/index.vue b/src/pages/oa/HrManage/resign-apply/index.vue
new file mode 100644
index 0000000..1052832
--- /dev/null
+++ b/src/pages/oa/HrManage/resign-apply/index.vue
@@ -0,0 +1,18 @@
+<!--
+ OA / 浜轰簨绠$悊 / 绂昏亴鐢宠
+ 璺敱锛�/pages/oa/HrManage/resign-apply/index
+-->
+<template>
+ <OaListPage v-if="config"
+ :page-key="pageKey"
+ :page-config="config" />
+</template>
+
+<script setup>
+ /** OA - 浜轰簨绠$悊 - 绂昏亴鐢宠 */
+ import OaListPage from "../../_components/OaListPage.vue";
+ import { useOaPage } from "../../_utils/useOaPage.js";
+
+ const pageKey = "HrManage/resign-apply";
+ const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/staff-archive/index.vue b/src/pages/oa/HrManage/staff-archive/index.vue
new file mode 100644
index 0000000..8d485c0
--- /dev/null
+++ b/src/pages/oa/HrManage/staff-archive/index.vue
@@ -0,0 +1,18 @@
+<!--
+ OA / 浜轰簨绠$悊 / 鍛樺伐妗f
+ 璺敱锛�/pages/oa/HrManage/staff-archive/index
+-->
+<template>
+ <OaListPage v-if="config"
+ :page-key="pageKey"
+ :page-config="config" />
+</template>
+
+<script setup>
+ /** OA - 浜轰簨绠$悊 - 鍛樺伐妗f */
+ import OaListPage from "../../_components/OaListPage.vue";
+ import { useOaPage } from "../../_utils/useOaPage.js";
+
+ const pageKey = "HrManage/staff-archive";
+ const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/staff-contract/index.vue b/src/pages/oa/HrManage/staff-contract/index.vue
new file mode 100644
index 0000000..f12b78d
--- /dev/null
+++ b/src/pages/oa/HrManage/staff-contract/index.vue
@@ -0,0 +1,18 @@
+<!--
+ OA / 浜轰簨绠$悊 / 鍛樺伐鍚堝悓
+ 璺敱锛�/pages/oa/HrManage/staff-contract/index
+-->
+<template>
+ <OaListPage v-if="config"
+ :page-key="pageKey"
+ :page-config="config" />
+</template>
+
+<script setup>
+ /** OA - 浜轰簨绠$悊 - 鍛樺伐鍚堝悓 */
+ import OaListPage from "../../_components/OaListPage.vue";
+ import { useOaPage } from "../../_utils/useOaPage.js";
+
+ const pageKey = "HrManage/staff-contract";
+ const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/HrManage/transfer-apply/index.vue b/src/pages/oa/HrManage/transfer-apply/index.vue
new file mode 100644
index 0000000..99ccacf
--- /dev/null
+++ b/src/pages/oa/HrManage/transfer-apply/index.vue
@@ -0,0 +1,12 @@
+<!--
+ OA / 浜轰簨绠$悊 / 璋冨矖鐢宠
+ 璺敱锛�/pages/oa/HrManage/transfer-apply/index
+-->
+<template>
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.TRANSFER" />
+</template>
+
+<script setup>
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+</script>
diff --git a/src/pages/oa/HrManage/work-handover/index.vue b/src/pages/oa/HrManage/work-handover/index.vue
new file mode 100644
index 0000000..9fa24b6
--- /dev/null
+++ b/src/pages/oa/HrManage/work-handover/index.vue
@@ -0,0 +1,12 @@
+<!--
+ OA / 浜轰簨绠$悊 / 宸ヤ綔浜ゆ帴
+ 璺敱锛�/pages/oa/HrManage/work-handover/index
+-->
+<template>
+ <ApprovalInstanceListPage :module-key="APPROVAL_MODULE_KEYS.WORK_HANDOVER" />
+</template>
+
+<script setup>
+ import ApprovalInstanceListPage from "../../_components/ApprovalInstanceListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+</script>
diff --git a/src/pages/oa/NoticeAnnouncement/notice-manage/index.vue b/src/pages/oa/NoticeAnnouncement/notice-manage/index.vue
new file mode 100644
index 0000000..351ddf0
--- /dev/null
+++ b/src/pages/oa/NoticeAnnouncement/notice-manage/index.vue
@@ -0,0 +1,18 @@
+<!--
+ OA / 鍏憡閫氱煡
+ 璺敱锛�/pages/oa/NoticeAnnouncement/notice-manage/index
+-->
+<template>
+ <OaListPage v-if="config"
+ :page-key="pageKey"
+ :page-config="config" />
+</template>
+
+<script setup>
+ /** OA - 鍏憡閫氱煡 */
+ import OaListPage from "../../_components/OaListPage.vue";
+ import { useOaPage } from "../../_utils/useOaPage.js";
+
+ const pageKey = "NoticeAnnouncement/notice-manage";
+ const { config } = useOaPage(pageKey);
+</script>
diff --git a/src/pages/oa/ReimburseManage/_components/ReimburseApprovalFlowEditor.vue b/src/pages/oa/ReimburseManage/_components/ReimburseApprovalFlowEditor.vue
new file mode 100644
index 0000000..77bd712
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_components/ReimburseApprovalFlowEditor.vue
@@ -0,0 +1,245 @@
+<!--
+ 鎶ラ攢瀹℃壒娴佺▼锛堝彲鎼滅储閫変汉锛岀偣閫夊嵆纭锛�
+-->
+<template>
+ <view class="flow-wrap">
+ <view v-for="(item, index) in innerList"
+ :key="item._uid"
+ class="flow-node-block">
+ <view class="flow-node-card">
+ <view class="node-header">
+ <view class="node-level-badge">{{ index + 1 }}</view>
+ <text class="node-level-text">绗瑊{ levelLabel(index + 1) }}绾у鎵�</text>
+ <view v-if="innerList.length > 1"
+ class="node-delete"
+ @click="remove(index)">
+ <up-icon name="trash"
+ size="16"
+ color="#f56c6c" />
+ </view>
+ </view>
+ <view class="approver-row"
+ @click="openPicker(index)">
+ <view class="approver-avatar"
+ :style="{ backgroundColor: avatarColor(item.approverName) }">
+ {{ (item.approverName || '+').charAt(0) }}
+ </view>
+ <view class="approver-meta">
+ <text class="approver-name">{{ item.approverName || '鐐瑰嚮閫夋嫨瀹℃壒浜�' }}</text>
+ <text class="approver-hint">鏀寔鎼滅储濮撳悕鎴栧伐鍙�</text>
+ </view>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc" />
+ </view>
+ </view>
+ <view v-if="index < innerList.length - 1"
+ class="flow-connector">
+ <view class="flow-connector-line" />
+ </view>
+ </view>
+ <view class="add-node-bar"
+ @click="addNode">
+ <up-icon name="plus-circle"
+ size="18"
+ color="#2979ff" />
+ <text>娣诲姞瀹℃壒绾ф</text>
+ </view>
+
+ <OaUserSearchPicker v-model:show="pickerShow"
+ v-model="pickerUserId"
+ title="閫夋嫨瀹℃壒浜�"
+ :users="userOptions"
+ :show-self-quick="false"
+ @select="onUserSelected" />
+ </view>
+</template>
+
+<script setup>
+ import { ref, watch } from "vue";
+ import OaUserSearchPicker from "../../_components/OaUserSearchPicker.vue";
+ import { userAvatarColor } from "../../_utils/userPickerUtils.js";
+
+ const props = defineProps({
+ modelValue: { type: Array, default: () => [] },
+ userOptions: { type: Array, default: () => [] },
+ });
+ const emit = defineEmits(["update:modelValue"]);
+
+ const innerList = ref([]);
+ const pickerShow = ref(false);
+ const pickerUserId = ref("");
+ const editingIndex = ref(-1);
+
+ function newUid() {
+ return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+ }
+
+ function levelLabel(n) {
+ const t = ["涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�"];
+ return t[n - 1] || String(n);
+ }
+
+ function avatarColor(name) {
+ return userAvatarColor(name);
+ }
+
+ function mapIn(rows) {
+ return (rows || []).map((n, i) => ({
+ _uid: n._uid || newUid(),
+ nodeOrder: n.nodeOrder ?? i + 1,
+ signMode: n.signMode || "countersign",
+ approverId: n.approverId ?? "",
+ approverName: n.approverName || "",
+ id: n.id,
+ templateId: n.templateId,
+ }));
+ }
+
+ function mapOut() {
+ return innerList.value.map((n, i) => ({
+ nodeOrder: i + 1,
+ signMode: n.signMode || "countersign",
+ approverId: n.approverId,
+ approverName: n.approverName,
+ id: n.id,
+ templateId: n.templateId,
+ }));
+ }
+
+ function syncEmit() {
+ emit("update:modelValue", mapOut());
+ }
+
+ watch(
+ () => props.modelValue,
+ v => {
+ innerList.value = mapIn(v);
+ if (!innerList.value.length) {
+ innerList.value = [
+ { _uid: newUid(), nodeOrder: 1, signMode: "countersign", approverId: "", approverName: "" },
+ ];
+ }
+ },
+ { immediate: true, deep: true }
+ );
+
+ function addNode() {
+ innerList.value.push({
+ _uid: newUid(),
+ nodeOrder: innerList.value.length + 1,
+ signMode: "countersign",
+ approverId: "",
+ approverName: "",
+ });
+ syncEmit();
+ }
+
+ function remove(index) {
+ if (innerList.value.length <= 1) {
+ uni.showToast({ title: "鑷冲皯淇濈暀涓�涓鎵硅妭鐐�", icon: "none" });
+ return;
+ }
+ innerList.value.splice(index, 1);
+ syncEmit();
+ }
+
+ function openPicker(index) {
+ editingIndex.value = index;
+ pickerUserId.value = innerList.value[index]?.approverId || "";
+ pickerShow.value = true;
+ }
+
+ function onUserSelected(u) {
+ const node = innerList.value[editingIndex.value];
+ if (!node) return;
+ node.approverId = u.userId ?? u.id;
+ node.approverName = u.nickName || u.userName || "";
+ syncEmit();
+ }
+</script>
+
+<style scoped lang="scss">
+ .flow-node-card {
+ background: #f8f9fb;
+ border-radius: 10px;
+ padding: 12px;
+ border: 1px solid #eef0f3;
+ }
+ .node-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ }
+ .node-level-badge {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: #2979ff;
+ color: #fff;
+ font-size: 12px;
+ text-align: center;
+ line-height: 22px;
+ margin-right: 8px;
+ }
+ .node-level-text {
+ flex: 1;
+ font-size: 14px;
+ color: #303133;
+ font-weight: 500;
+ }
+ .approver-row {
+ display: flex;
+ align-items: center;
+ padding: 10px 12px;
+ background: #fff;
+ border-radius: 8px;
+ }
+ .approver-avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ color: #fff;
+ font-size: 15px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+ .approver-meta {
+ flex: 1;
+ margin-left: 10px;
+ min-width: 0;
+ }
+ .approver-name {
+ display: block;
+ font-size: 15px;
+ color: #303133;
+ }
+ .approver-hint {
+ display: block;
+ font-size: 12px;
+ color: #c0c4cc;
+ margin-top: 2px;
+ }
+ .flow-connector {
+ display: flex;
+ justify-content: center;
+ padding: 6px 0;
+ }
+ .flow-connector-line {
+ width: 2px;
+ height: 14px;
+ background: #dcdfe6;
+ }
+ .add-node-bar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 14px 0 4px;
+ color: #2979ff;
+ font-size: 14px;
+ }
+</style>
diff --git a/src/pages/oa/ReimburseManage/_components/ReimburseExpenseDetailSheet.vue b/src/pages/oa/ReimburseManage/_components/ReimburseExpenseDetailSheet.vue
new file mode 100644
index 0000000..a0b25f0
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_components/ReimburseExpenseDetailSheet.vue
@@ -0,0 +1,315 @@
+<!--
+ 鎶ラ攢鏄庣粏鍗曟潯缂栬緫锛堝簳閮ㄥ脊灞傦級
+-->
+<template>
+ <up-popup :show="show"
+ mode="bottom"
+ round="16"
+ :safe-area-inset-bottom="true"
+ @close="close">
+ <view class="detail-sheet">
+ <view class="sheet-handle" />
+ <view class="sheet-head">
+ <text class="sheet-cancel"
+ @click="close">鍙栨秷</text>
+ <text class="sheet-title">{{ title }}</text>
+ <text class="sheet-confirm"
+ @click="confirm">淇濆瓨</text>
+ </view>
+
+ <scroll-view scroll-y
+ class="sheet-body"
+ :show-scrollbar="false">
+ <view class="sheet-group">
+ <view class="sheet-cell sheet-cell--tap"
+ @click="showDatePicker = true">
+ <text class="sheet-label required">鍙戠エ鏃ユ湡</text>
+ <view class="sheet-value-wrap">
+ <text class="sheet-value"
+ :class="{ placeholder: !draft.invoiceDate }">
+ {{ draft.invoiceDate || '璇烽�夋嫨' }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#c0c4cc" />
+ </view>
+ </view>
+ <view class="sheet-cell sheet-cell--tap"
+ @click="showSubjectSheet = true">
+ <text class="sheet-label required">璐圭敤绉戠洰</text>
+ <view class="sheet-value-wrap">
+ <text class="sheet-value"
+ :class="{ placeholder: !draft.expenseSubject }">
+ {{ subjectText }}
+ </text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc" />
+ </view>
+ </view>
+ <view class="sheet-cell">
+ <text class="sheet-label required">閲戦</text>
+ <view class="sheet-input-wrap">
+ <up-input v-model="draft.amount"
+ type="digit"
+ placeholder="0.00"
+ border="none"
+ input-align="right" />
+ <text class="sheet-unit">鍏�</text>
+ </view>
+ </view>
+ <view class="sheet-cell sheet-cell--col">
+ <text class="sheet-label">鎻忚堪</text>
+ <view class="sheet-textarea-wrap">
+ <up-textarea v-model="draft.description"
+ placeholder="璐圭敤璇存槑锛堥�夊~锛�"
+ maxlength="200"
+ border="none"
+ height="64" />
+ </view>
+ </view>
+ </view>
+
+ <view v-if="showDelete"
+ class="sheet-delete"
+ @click="emit('delete')">
+ 鍒犻櫎鏈潯鏄庣粏
+ </view>
+ </scroll-view>
+ </view>
+
+ <up-action-sheet :show="showSubjectSheet"
+ title="璐圭敤绉戠洰"
+ :actions="subjectActions"
+ @select="onSubjectSelect"
+ @close="showSubjectSheet = false" />
+
+ <up-popup :show="showDatePicker"
+ mode="bottom"
+ round="16"
+ @close="showDatePicker = false">
+ <up-datetime-picker :show="true"
+ v-model="datePickerTs"
+ mode="date"
+ @confirm="onDateConfirm"
+ @cancel="showDatePicker = false" />
+ </up-popup>
+ </up-popup>
+</template>
+
+<script setup>
+ import { computed, reactive, ref, watch } from "vue";
+ import { parseTime } from "@/utils/ruoyi";
+ import { expenseSubjectLabel as costSubjectLabel } from "../_utils/costReimburseUtils.js";
+ import { expenseSubjectLabel as travelSubjectLabel } from "../_utils/travelReimburseUtils.js";
+
+ const props = defineProps({
+ show: { type: Boolean, default: false },
+ modelValue: { type: Object, default: () => ({}) },
+ index: { type: Number, default: 0 },
+ isTravel: { type: Boolean, default: true },
+ subjectOptions: { type: Array, default: () => [] },
+ showDelete: { type: Boolean, default: true },
+ });
+
+ const emit = defineEmits(["update:show", "update:modelValue", "confirm", "delete"]);
+
+ const draft = reactive({
+ invoiceDate: "",
+ expenseSubject: "",
+ amount: "",
+ description: "",
+ });
+
+ const showDatePicker = ref(false);
+ const showSubjectSheet = ref(false);
+ const datePickerTs = ref(Date.now());
+
+ const title = computed(() => `鏄庣粏 ${props.index + 1}`);
+
+ const subjectActions = computed(() =>
+ (props.subjectOptions || []).map(x => ({ name: x.label, value: x.value }))
+ );
+
+ const subjectText = computed(() => resolveSubjectLabel(draft.expenseSubject));
+
+ function resolveSubjectLabel(v) {
+ if (!v) return "璇烽�夋嫨";
+ const labelFn = props.isTravel ? travelSubjectLabel : costSubjectLabel;
+ const t = labelFn(v);
+ if (t && t !== "鈥�") return t;
+ const hit = (props.subjectOptions || []).find(x => x.value === v || x.label === v);
+ return hit?.label || v;
+ }
+
+ watch(
+ () => props.show,
+ v => {
+ if (v && props.modelValue) {
+ Object.assign(draft, {
+ invoiceDate: "",
+ expenseSubject: "",
+ amount: "",
+ description: "",
+ ...JSON.parse(JSON.stringify(props.modelValue)),
+ });
+ }
+ }
+ );
+
+ function close() {
+ emit("update:show", false);
+ }
+
+ function confirm() {
+ if (!draft.invoiceDate) {
+ uni.showToast({ title: "璇烽�夋嫨鍙戠エ鏃ユ湡", icon: "none" });
+ return;
+ }
+ if (!draft.expenseSubject) {
+ uni.showToast({ title: "璇烽�夋嫨璐圭敤绉戠洰", icon: "none" });
+ return;
+ }
+ if (draft.amount === "" || draft.amount == null) {
+ uni.showToast({ title: "璇峰~鍐欓噾棰�", icon: "none" });
+ return;
+ }
+ emit("update:modelValue", { ...draft });
+ emit("confirm", { ...draft });
+ emit("update:show", false);
+ }
+
+ function onSubjectSelect(action) {
+ draft.expenseSubject = action.value;
+ showSubjectSheet.value = false;
+ }
+
+ function onDateConfirm(e) {
+ const ts = e?.value ?? datePickerTs.value;
+ draft.invoiceDate = parseTime(ts, "{y}-{m}-{d}");
+ showDatePicker.value = false;
+ }
+</script>
+
+<style scoped lang="scss">
+ .detail-sheet {
+ background: #fff;
+ border-radius: 16px 16px 0 0;
+ max-height: 85vh;
+ display: flex;
+ flex-direction: column;
+ }
+ .sheet-handle {
+ width: 36px;
+ height: 4px;
+ background: #e4e7ed;
+ border-radius: 2px;
+ margin: 8px auto 4px;
+ }
+ .sheet-head {
+ display: flex;
+ align-items: center;
+ padding: 8px 16px 12px;
+ border-bottom: 1px solid #f0f2f5;
+ }
+ .sheet-cancel {
+ font-size: 15px;
+ color: #909399;
+ min-width: 48px;
+ }
+ .sheet-title {
+ flex: 1;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+ .sheet-confirm {
+ font-size: 15px;
+ color: #2979ff;
+ font-weight: 600;
+ min-width: 48px;
+ text-align: right;
+ }
+ .sheet-body {
+ max-height: 70vh;
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+ .sheet-group {
+ margin: 12px 16px;
+ background: #f8f9fb;
+ border-radius: 12px;
+ overflow: hidden;
+ }
+ .sheet-cell {
+ display: flex;
+ align-items: center;
+ min-height: 52px;
+ padding: 12px 14px;
+ background: #fff;
+ border-bottom: 1px solid #f5f6f8;
+ &--col {
+ flex-direction: column;
+ align-items: stretch;
+ }
+ &--tap:active {
+ background: #fafbfc;
+ }
+ &:last-child {
+ border-bottom: none;
+ }
+ }
+ .sheet-label {
+ width: 80px;
+ font-size: 15px;
+ color: #303133;
+ flex-shrink: 0;
+ &.required::before {
+ content: "*";
+ color: #f56c6c;
+ margin-right: 2px;
+ }
+ }
+ .sheet-value-wrap {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 4px;
+ }
+ .sheet-value {
+ font-size: 15px;
+ color: #303133;
+ &.placeholder {
+ color: #c0c4cc;
+ }
+ }
+ .sheet-input-wrap {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ }
+ .sheet-unit {
+ font-size: 14px;
+ color: #909399;
+ margin-left: 4px;
+ }
+ .sheet-textarea-wrap {
+ width: 100%;
+ margin-top: 8px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ padding: 4px 8px;
+ }
+ .sheet-delete {
+ margin: 16px;
+ text-align: center;
+ font-size: 15px;
+ color: #f56c6c;
+ padding: 14px;
+ background: #fff;
+ border-radius: 12px;
+ border: 1px solid #fde2e2;
+ }
+</style>
diff --git a/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue b/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue
new file mode 100644
index 0000000..0b270f1
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue
@@ -0,0 +1,426 @@
+<!--
+ 宸梾/璐圭敤鎶ラ攢璇︽儏灞曠ず锛堝垪琛ㄨ鎯� / 瀹℃壒璇︽儏鍏辩敤锛�
+-->
+<template>
+ <view class="rd-body">
+ <!-- 姒傝 -->
+ <view class="rd-hero">
+ <view class="rd-hero-top">
+ <text class="rd-bill-no">{{ billNo }}</text>
+ <text :class="['rd-status', statusCssClass]">{{ statusText }}</text>
+ </view>
+ <text class="rd-reason">{{ reasonText }}</text>
+ <view class="rd-amount-row">
+ <text class="rd-amount-label">鐢宠閲戦</text>
+ <text class="rd-amount">{{ amountText }}</text>
+ </view>
+ </view>
+
+ <!-- 鐢宠浜� -->
+ <view class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">鐢宠浜�</text>
+ </view>
+ <view class="rd-group">
+ <view class="rd-cell">
+ <text class="rd-label">濮撳悕</text>
+ <text class="rd-value">{{ r.applicantName || "鈥�" }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鍛樺伐缂栧彿</text>
+ <text class="rd-value">{{ r.applicantCode || r.applicantNo || "鈥�" }}</text>
+ </view>
+ <view v-if="r.applicantDeptName || r.deptName"
+ class="rd-cell">
+ <text class="rd-label">閮ㄩ棬</text>
+ <text class="rd-value">{{ r.applicantDeptName || r.deptName }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍑哄樊 / 璐圭敤 -->
+ <view class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">{{ isTravel ? "鍑哄樊淇℃伅" : "璐圭敤淇℃伅" }}</text>
+ </view>
+ <view class="rd-group">
+ <template v-if="isTravel">
+ <view class="rd-cell">
+ <text class="rd-label">鍑哄樊寮�濮�</text>
+ <text class="rd-value">{{ formatTime(r.travelStartTime) }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鍑哄樊缁撴潫</text>
+ <text class="rd-value">{{ formatTime(r.travelEndTime) }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鍑哄樊澶╂暟</text>
+ <text class="rd-value">{{ travelDaysText }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鍑哄樊鍦�</text>
+ <text class="rd-value">{{ r.departurePlace || "鈥�" }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鐩殑鍦�</text>
+ <text class="rd-value">{{ r.destination || "鈥�" }}</text>
+ </view>
+ </template>
+ <view v-else
+ class="rd-cell">
+ <text class="rd-label">璐圭敤绫诲瀷</text>
+ <text class="rd-value">{{ expenseTypeText }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 宸梾鏍囧噯 -->
+ <view v-if="isTravel && hasTravelStandard"
+ class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">宸梾鏍囧噯</text>
+ </view>
+ <view class="rd-group">
+ <view v-if="r.hotelStandard != null"
+ class="rd-cell">
+ <text class="rd-label">閰掑簵鏍囧噯</text>
+ <text class="rd-value">{{ r.hotelStandard }} 鍏�/鏅�</text>
+ </view>
+ <view v-if="r.hotelDays != null"
+ class="rd-cell">
+ <text class="rd-label">浣忓澶╂暟</text>
+ <text class="rd-value">{{ r.hotelDays }} 澶�</text>
+ </view>
+ <view v-if="r.livingSubsidy != null"
+ class="rd-cell">
+ <text class="rd-label">鐢熸椿琛ヨ创</text>
+ <text class="rd-value">{{ r.livingSubsidy }} 鍏�</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鏍囧噯鏍囪</text>
+ <text class="rd-value">{{ r.standardTag || (r.needSpecialApproval ? "瓒呮敮闇�鐗规壒" : "鍦ㄦ爣鍑嗗唴") }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鏀舵 -->
+ <view class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">鏀舵淇℃伅</text>
+ </view>
+ <view class="rd-group">
+ <view class="rd-cell">
+ <text class="rd-label">鏀舵浜�</text>
+ <text class="rd-value">{{ r.payeeName || r.payee || "鈥�" }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鏀舵璐﹀彿</text>
+ <text class="rd-value">{{ r.payeeAccount || "鈥�" }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">寮�鎴锋敮琛�</text>
+ <text class="rd-value">{{ r.payeeBank || r.bankBranch || "鈥�" }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鎶ラ攢鏄庣粏 -->
+ <view class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">鎶ラ攢鏄庣粏</text>
+ <text class="rd-section-count">鍏� {{ detailRows.length }} 鏉�</text>
+ </view>
+ <view v-if="detailRows.length"
+ class="rd-group">
+ <view v-for="(d, idx) in detailRows"
+ :key="'d-' + idx"
+ class="rd-detail-item">
+ <view class="rd-detail-head">
+ <text class="rd-detail-badge">{{ idx + 1 }}</text>
+ <text class="rd-detail-title">{{ detailSubject(d) }}</text>
+ <text class="rd-detail-amount">{{ detailAmount(d) }}</text>
+ </view>
+ <view class="rd-cell">
+ <text class="rd-label">鍙戠エ鏃ユ湡</text>
+ <text class="rd-value">{{ d.invoiceDate || "鈥�" }}</text>
+ </view>
+ <view v-if="d.description"
+ class="rd-cell">
+ <text class="rd-label">鎻忚堪</text>
+ <text class="rd-value">{{ d.description }}</text>
+ </view>
+ <view v-if="d.invoiceNo"
+ class="rd-cell">
+ <text class="rd-label">鍙戠エ鍙�</text>
+ <text class="rd-value">{{ d.invoiceNo }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="rd-group">
+ <view class="rd-empty">鏆傛棤鎶ラ攢鏄庣粏</view>
+ </view>
+ </view>
+
+ <!-- 闄勪欢 -->
+ <view v-if="attachmentList.length"
+ class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">鍙戠エ闄勪欢</text>
+ </view>
+ <view class="rd-group">
+ <view v-for="(f, i) in attachmentList"
+ :key="i"
+ class="rd-attach"
+ @click="openAttachment(f)">
+ {{ f.name || "闄勪欢" }}
+ </view>
+ </view>
+ </view>
+
+ <!-- 瀹℃壒娴佺▼锛坱asks锛� -->
+ <view class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">瀹℃壒娴佺▼</text>
+ <text class="rd-section-count">{{ flowNodesList.length }} 绾�</text>
+ </view>
+ <view v-if="flowNodesList.length"
+ class="rd-group">
+ <view v-for="(node, nodeIndex) in flowNodesList"
+ :key="nodeIndex"
+ class="rd-flow-node">
+ <view class="rd-flow-line">
+ <view class="rd-flow-dot" />
+ <view v-if="nodeIndex < flowNodesList.length - 1"
+ class="rd-flow-bar" />
+ </view>
+ <view class="rd-flow-body">
+ <text class="rd-flow-level">绗瑊{ node.levelNo }}绾� 路 {{ node.approveType === 'OR' ? '鎴栫' : '浼氱' }}</text>
+ <view v-for="(a, ai) in node.approvers"
+ :key="ai"
+ class="rd-flow-approver">
+ <view class="rd-flow-avatar"
+ :style="{ backgroundColor: avatarColor(a.approverName) }">
+ {{ (a.approverName || "?").charAt(0) }}
+ </view>
+ <view class="rd-flow-approver-meta">
+ <text class="rd-flow-name">{{ a.approverName || "鈥�" }}</text>
+ <text v-if="a.taskStatus"
+ class="rd-flow-status">{{ taskStatusLabel(a.taskStatus) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="rd-group">
+ <view class="rd-empty">鏆傛棤瀹℃壒鑺傜偣</view>
+ </view>
+ </view>
+
+ <!-- 瀹℃壒璁板綍锛坱asks 鐣欑棔锛� -->
+ <view class="rd-section">
+ <view class="rd-section-hd">
+ <text class="rd-section-title">瀹℃壒璁板綍</text>
+ <text class="rd-section-count">{{ approvalRecords.length }} 鏉�</text>
+ </view>
+ <view v-if="approvalRecords.length"
+ class="rd-group">
+ <view v-for="(rec, index) in approvalRecords"
+ :key="rec.id ?? index"
+ class="rd-record-item">
+ <view class="rd-record-head">
+ <text class="rd-record-operator">{{ rec.operatorName }}</text>
+ <text class="rd-record-tag"
+ :class="'rd-record-tag--' + rec.result">{{ recordLabel(rec.result) }}</text>
+ </view>
+ <text v-if="rec.time"
+ class="rd-record-time">{{ rec.time }}</text>
+ <text class="rd-record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</text>
+ </view>
+ </view>
+ <view v-else
+ class="rd-group">
+ <view class="rd-empty">鏆傛棤瀹℃壒璁板綍</view>
+ </view>
+ </view>
+
+ <view class="rd-section">
+ <view class="rd-group">
+ <view class="rd-cell">
+ <text class="rd-label">鍒涘缓鏃堕棿</text>
+ <text class="rd-value">{{ formatTime(r.createTime) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed } from "vue";
+ import { parseTime } from "@/utils/ruoyi";
+ import { isTravelReimbursementType } from "../../_utils/finReimbursementMappers.js";
+ import {
+ billStatusCssClass,
+ billStatusLabel,
+ } from "../../_utils/finReimbursementMappers.js";
+ import { expenseCategoryLabel, EXPENSE_SUBJECT_OPTIONS as COST_SUBJECTS } from "../_utils/costReimburseUtils.js";
+ import { EXPENSE_SUBJECT_OPTIONS as TRAVEL_SUBJECTS } from "../_utils/travelReimburseUtils.js";
+ import {
+ resolveExpenseSubjectLabel,
+ formatDetailAmount,
+ } from "../_utils/expenseDetailDisplay.js";
+ import { userAvatarColor } from "../../_utils/userPickerUtils.js";
+ import {
+ mapTasksToFlowNodes,
+ recordActionLabel,
+ taskStatusText,
+ } from "../../_utils/approveListUtils.js";
+ import config from "@/config.js";
+
+ const props = defineProps({
+ reimburseRow: { type: Object, default: () => ({}) },
+ moduleKey: { type: String, default: "" },
+ });
+
+ const r = computed(() => props.reimburseRow || {});
+
+ const isTravel = computed(() =>
+ isTravelReimbursementType(r.value.reimbursementType ?? props.moduleKey)
+ );
+
+ const billNo = computed(() => r.value.billNo || r.value.reimburseNo || "鈥�");
+ const statusText = computed(() =>
+ billStatusLabel(r.value.billStatus ?? r.value.status)
+ );
+ const statusCssClass = computed(() =>
+ billStatusCssClass(r.value)
+ );
+ const reasonText = computed(
+ () => r.value.reason || r.value.reimburseReason || "鈥�"
+ );
+ const amountText = computed(() =>
+ r.value.applyAmount != null ? String(r.value.applyAmount) : "鈥�"
+ );
+
+ const expenseTypeText = computed(() =>
+ expenseCategoryLabel(r.value.expenseCategory) || r.value.expenseType || "鈥�"
+ );
+
+ const travelDaysText = computed(() => {
+ const d = r.value.travelDays ?? r.value.travel?.travelDays;
+ return d != null ? `${d} 澶ー : "鈥�";
+ });
+
+ const hasTravelStandard = computed(() => {
+ const row = r.value;
+ return (
+ row.hotelStandard != null ||
+ row.hotelDays != null ||
+ row.livingSubsidy != null ||
+ row.standardTag ||
+ row.needSpecialApproval
+ );
+ });
+
+ const subjectOptions = computed(() =>
+ isTravel.value ? TRAVEL_SUBJECTS : COST_SUBJECTS
+ );
+
+ const detailRows = computed(() => {
+ const list = r.value.expenseDetails || r.value.details || [];
+ return Array.isArray(list) ? list : [];
+ });
+
+ const attachmentList = computed(() => {
+ const list =
+ r.value.attachmentList ||
+ r.value.storageBlobVOList ||
+ r.value.invoiceAttachments ||
+ [];
+ return Array.isArray(list) ? list : [];
+ });
+
+ const approvalRecords = computed(() => {
+ const list = r.value.approvalRecords || [];
+ return Array.isArray(list) ? list : [];
+ });
+
+ /** 娴佺▼灞曠ず浼樺厛鐢� enrichment 鍚庣殑 flowNodes锛堟潵鑷� tasks锛� */
+ const flowNodesList = computed(() => {
+ const row = r.value;
+ if (Array.isArray(row.flowNodes) && row.flowNodes.length) {
+ return row.flowNodes;
+ }
+ if (Array.isArray(row.tasks) && row.tasks.length) {
+ return mapTasksToFlowNodes(row.tasks);
+ }
+ return [];
+ });
+
+ function taskStatusLabel(status) {
+ return taskStatusText(status);
+ }
+
+ function recordLabel(result) {
+ return recordActionLabel(result);
+ }
+
+ function formatTime(t) {
+ if (!t) return "鈥�";
+ const s = parseTime(t, "{y}-{m}-{d} {h}:{i}");
+ return s || String(t).replace("T", " ").slice(0, 16);
+ }
+
+ function detailSubject(d) {
+ return (
+ resolveExpenseSubjectLabel(d.expenseSubject || d.expenseCategory, {
+ isTravel: isTravel.value,
+ subjectOptions: subjectOptions.value,
+ }) || "鏈�夌鐩�"
+ );
+ }
+
+ function detailAmount(d) {
+ return formatDetailAmount(d.amount) || "鈥�";
+ }
+
+ function avatarColor(name) {
+ return userAvatarColor(name);
+ }
+
+ function resolveFileUrl(f) {
+ let url = f?.url || f?.downloadURL || f?.previewURL || f?.fileUrl || "";
+ if (!url) return "";
+ if (/^https?:\/\//i.test(url)) return url;
+ const base = (config.baseUrl || "").replace(/\/+$/, "");
+ const path = url.startsWith("/") ? url : `/${url}`;
+ return `${base}${path}`;
+ }
+
+ function openAttachment(f) {
+ const url = resolveFileUrl(f);
+ if (!url) {
+ uni.showToast({ title: "鏃犳硶鎵撳紑闄勪欢", icon: "none" });
+ return;
+ }
+ // #ifdef H5
+ window.open(url, "_blank");
+ // #endif
+ // #ifndef H5
+ uni.downloadFile({
+ url,
+ success: res => {
+ if (res.statusCode === 200) {
+ uni.openDocument({ filePath: res.tempFilePath, showMenu: true });
+ }
+ },
+ fail: () => uni.showToast({ title: "闄勪欢鎵撳紑澶辫触", icon: "none" }),
+ });
+ // #endif
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../reimburse-detail/reimburse-detail.scss";
+</style>
diff --git a/src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js b/src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js
new file mode 100644
index 0000000..118b353
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js
@@ -0,0 +1,120 @@
+import dayjs from "dayjs";
+
+export const EXPENSE_CATEGORY_OPTIONS = [
+ { label: "宸梾", value: "travel" },
+ { label: "鍔炲叕閲囪喘", value: "office_procurement" },
+ { label: "涓氬姟鎷涘緟", value: "business_entertainment" },
+ { label: "浜ら�氳垂", value: "transport" },
+ { label: "閫氳璐�", value: "communication" },
+ { label: "鍏朵粬", value: "other" },
+];
+
+export const EXPENSE_SUBJECT_OPTIONS = [
+ { label: "浜ら�氳垂", value: "transport" },
+ { label: "浣忓璐�", value: "hotel" },
+ { label: "椁愰ギ璐�", value: "meal" },
+ { label: "鍔炲叕鐢ㄥ搧", value: "office_supply" },
+ { label: "鎷涘緟璐�", value: "entertainment" },
+ { label: "閫氳璐�", value: "phone" },
+ { label: "鍏朵粬", value: "other" },
+];
+
+export const CATEGORY_TEMPLATES = {
+ travel: {
+ label: "宸梾璐圭敤",
+ reason: "鍥犲叕鍑哄樊浜х敓鐨勪氦閫氥�佷綇瀹裤�侀楗瓑璐圭敤鎶ラ攢銆�",
+ details: [
+ { expenseSubject: "transport", description: "寰�杩斾氦閫氳垂" },
+ { expenseSubject: "hotel", description: "浣忓璐�" },
+ { expenseSubject: "meal", description: "鍑哄樊椁愰ギ" },
+ ],
+ },
+ office_procurement: {
+ label: "鍔炲叕閲囪喘",
+ reason: "閮ㄩ棬鏃ュ父鍔炲叕鐢ㄥ搧銆佽�楁潗閲囪喘鎶ラ攢銆�",
+ details: [
+ { expenseSubject: "office_supply", description: "鍔炲叕鐢ㄥ搧閲囪喘" },
+ { expenseSubject: "office_supply", description: "鎵撳嵃鑰楁潗" },
+ ],
+ },
+ business_entertainment: {
+ label: "涓氬姟鎷涘緟",
+ reason: "瀹㈡埛鎺ュ緟銆佸晢鍔″璇风瓑璐圭敤鎶ラ攢銆�",
+ details: [
+ { expenseSubject: "entertainment", description: "瀹㈡埛鎺ュ緟椁愯垂" },
+ { expenseSubject: "entertainment", description: "鍟嗗姟绀煎搧" },
+ ],
+ },
+ transport: {
+ label: "浜ら�氳垂",
+ reason: "甯傚唴閫氬嫟銆佹墦杞︺�佸仠杞︾瓑浜ら�氳垂鐢ㄦ姤閿�銆�",
+ details: [{ expenseSubject: "transport", description: "甯傚唴浜ら��" }],
+ },
+ communication: {
+ label: "閫氳璐�",
+ reason: "鍥犲叕閫氳銆佹祦閲忋�佽瘽璐硅ˉ璐存姤閿�銆�",
+ details: [{ expenseSubject: "phone", description: "璇濊垂/娴侀噺" }],
+ },
+ other: {
+ label: "鍏朵粬璐圭敤",
+ reason: "鍏朵粬鍥犲叕鏀嚭璐圭敤鎶ラ攢銆�",
+ details: [{ expenseSubject: "other", description: "鍏朵粬璐圭敤" }],
+ },
+};
+
+export function expenseSubjectLabel(v) {
+ return EXPENSE_SUBJECT_OPTIONS.find(x => x.value === v)?.label || "鈥�";
+}
+
+export function expenseCategoryLabel(v) {
+ return EXPENSE_CATEGORY_OPTIONS.find(x => x.value === v)?.label || v || "鈥�";
+}
+
+export function expenseTypeToCategory(expenseType) {
+ const t = (expenseType || "").trim();
+ const hit = EXPENSE_CATEGORY_OPTIONS.find(x => x.label === t || x.value === t);
+ return hit?.value || "other";
+}
+
+export function createEmptyExpenseDetail() {
+ return {
+ id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
+ invoiceDate: "",
+ expenseSubject: "",
+ amount: "",
+ description: "",
+ };
+}
+
+export function createEmptyCostForm() {
+ return {
+ reimbursementId: undefined,
+ applicantId: "",
+ employeeNo: "",
+ employeeName: "",
+ expenseCategory: "other",
+ reimburseReason: "",
+ applyAmount: "",
+ payee: "",
+ payeeAccount: "",
+ bankBranch: "",
+ expenseDetails: [],
+ attachmentList: [],
+ approvalFlowNodes: [{ approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" }],
+ deptId: "",
+ deptName: "",
+ };
+}
+
+export function applyCategoryTemplate(form, category) {
+ const tpl = CATEGORY_TEMPLATES[category];
+ if (!tpl) return;
+ form.expenseCategory = category;
+ if (!form.reimburseReason?.trim()) form.reimburseReason = tpl.reason;
+ form.expenseDetails = (tpl.details || []).map(d => ({
+ ...createEmptyExpenseDetail(),
+ expenseSubject: d.expenseSubject,
+ description: d.description,
+ invoiceDate: dayjs().format("YYYY-MM-DD"),
+ }));
+}
diff --git a/src/pages/oa/ReimburseManage/_utils/expenseDetailDisplay.js b/src/pages/oa/ReimburseManage/_utils/expenseDetailDisplay.js
new file mode 100644
index 0000000..559bcd1
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_utils/expenseDetailDisplay.js
@@ -0,0 +1,33 @@
+import { expenseSubjectLabel as costSubjectLabel } from "./costReimburseUtils.js";
+import { expenseSubjectLabel as travelSubjectLabel } from "./travelReimburseUtils.js";
+
+/** 璐圭敤绉戠洰灞曠ず锛堝吋瀹� value / 涓枃 label / API expenseCategory锛� */
+export function resolveExpenseSubjectLabel(v, { isTravel = true, subjectOptions = [] } = {}) {
+ if (!v) return "";
+ const labelFn = isTravel ? travelSubjectLabel : costSubjectLabel;
+ const t = labelFn(v);
+ if (t && t !== "鈥�") return t;
+ const hit = subjectOptions.find(x => x.value === v || x.label === v);
+ return hit?.label || String(v);
+}
+
+export function formatDetailAmount(amount) {
+ if (amount === "" || amount == null) return null;
+ const n = Number(amount);
+ if (Number.isNaN(n)) return String(amount);
+ return `${n} 鍏僠;
+}
+
+/** 鍒楄〃琛屾憳瑕� */
+export function buildExpenseDetailSummary(row, opts = {}) {
+ const subject = resolveExpenseSubjectLabel(row?.expenseSubject, opts) || "鏈�夌鐩�";
+ const amount = formatDetailAmount(row?.amount);
+ const date = row?.invoiceDate || "";
+ const desc = (row?.description || "").trim();
+ const parts = [];
+ if (date) parts.push(date);
+ if (desc) parts.push(desc);
+ const sub = parts.length ? parts.join(" 路 ") : "鐐瑰嚮璇︽儏瀹屽杽淇℃伅";
+ const incomplete = !row?.invoiceDate || !row?.expenseSubject || row?.amount === "" || row?.amount == null;
+ return { subject, amount: amount || "閲戦鏈~", sub, incomplete };
+}
diff --git a/src/pages/oa/ReimburseManage/_utils/finReimbursementDetailExtras.js b/src/pages/oa/ReimburseManage/_utils/finReimbursementDetailExtras.js
new file mode 100644
index 0000000..88f056f
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_utils/finReimbursementDetailExtras.js
@@ -0,0 +1,159 @@
+import { parseTime } from "@/utils/ruoyi";
+import {
+ mapApprovalRecords,
+ mapRecordResult,
+ mapTasksToFlowNodes,
+} from "../../_utils/approveListUtils.js";
+
+function formatDisplayTime(val) {
+ if (!val) return "";
+ const s = parseTime(val, "{y}-{m}-{d} {h}:{i}");
+ return s || String(val).replace("T", " ").slice(0, 16);
+}
+
+function taskStatusToNodeStatus(taskStatus) {
+ const s = String(taskStatus ?? "").toUpperCase();
+ if (["APPROVED", "COMPLETED", "FINISHED", "PASSED", "AGREE"].includes(s)) {
+ return "finish";
+ }
+ if (["REJECTED", "REJECT", "REFUSE", "REFUSED"].includes(s)) {
+ return "error";
+ }
+ if (["PENDING", "IN_APPROVAL", "PROCESS", "PROCESSING"].includes(s)) {
+ return "process";
+ }
+ return "wait";
+}
+
+/** storageBlobVOList 鈫� 椤甸潰闄勪欢 */
+export function mapReimbursementAttachments(source = {}) {
+ const list =
+ source.storageBlobVOList ||
+ source.storageBlobDTOs ||
+ source.storageBlobDTOS ||
+ source.attachmentList ||
+ source.invoiceAttachments ||
+ [];
+ if (!Array.isArray(list)) return [];
+ return list.map((b, i) => ({
+ ...b,
+ id: b.id ?? b.blobId ?? `att_${i}`,
+ name:
+ b.fileName ||
+ b.originalFilename ||
+ b.originalFileName ||
+ b.blobName ||
+ b.name ||
+ "闄勪欢",
+ url:
+ b.url ||
+ b.fileUrl ||
+ b.downloadUrl ||
+ b.downloadURL ||
+ b.previewUrl ||
+ b.previewURL ||
+ b.link ||
+ "",
+ }));
+}
+
+/** 瀹℃壒璁板綍鍦� tasks */
+export function mapTasksToApprovalRecords(tasks) {
+ const list = Array.isArray(tasks) ? tasks : [];
+ return list
+ .map((t, index) => ({
+ id: t.id ?? index,
+ operatorName: t.approverName || t.operatorName || t.createUserName || "鈥�",
+ result: mapRecordResult(t.approveAction ?? t.taskStatus ?? t.status),
+ opinion: t.approveComment || t.comment || t.opinion || "",
+ time: formatDisplayTime(
+ t.approveTime || t.finishTime || t.updateTime || t.createTime || ""
+ ),
+ levelNo: t.levelNo ?? t.taskLevel,
+ }))
+ .sort((a, b) => {
+ const la = Number(a.levelNo ?? 0);
+ const lb = Number(b.levelNo ?? 0);
+ if (la !== lb) return la - lb;
+ return String(a.time).localeCompare(String(b.time));
+ });
+}
+
+export function mapTasksToApprovalFlowNodes(tasks) {
+ const grouped = mapTasksToFlowNodes(tasks);
+ return grouped.map((node, i) => {
+ const approvers = node.approvers || [];
+ const statuses = approvers.map(a =>
+ taskStatusToNodeStatus(a.taskStatus ?? a.status)
+ );
+ let nodeStatus = "wait";
+ if (statuses.includes("error")) nodeStatus = "error";
+ else if (statuses.length && statuses.every(s => s === "finish")) {
+ nodeStatus = "finish";
+ } else if (statuses.includes("process")) nodeStatus = "process";
+
+ const names = approvers.map(a => a.approverName).filter(Boolean).join("銆�");
+ const opinions = approvers
+ .map(a => a.approveComment)
+ .filter(Boolean)
+ .join("锛�");
+
+ return {
+ nodeOrder: node.levelNo ?? i + 1,
+ levelNo: node.levelNo ?? i + 1,
+ approveType: node.approveType || "AND",
+ approveTypeLabel: node.approveType === "OR" ? "鎴栫" : "浼氱",
+ approvers,
+ approverName: names || "鈥�",
+ approveOpinion: opinions,
+ nodeStatus,
+ };
+ });
+}
+
+export function computeApprovalFlowCurrentIndex(approvalFlowNodes = []) {
+ const list = approvalFlowNodes || [];
+ const processing = list.findIndex(n => n.nodeStatus === "process");
+ if (processing >= 0) return processing;
+ const errorIdx = list.findIndex(n => n.nodeStatus === "error");
+ if (errorIdx >= 0) return errorIdx;
+ return list.filter(n => n.nodeStatus === "finish").length;
+}
+
+export function applyFinReimbursementDetailEnrichment(mapped, raw = {}) {
+ if (!mapped || typeof mapped !== "object") return mapped;
+ const source = { ...raw, ...mapped };
+ const tasks = Array.isArray(source.tasks) ? source.tasks : [];
+ const attachments = mapReimbursementAttachments(source);
+ const approvalRecords = tasks.length
+ ? mapTasksToApprovalRecords(tasks)
+ : mapApprovalRecords(source.records || source.approvalRecords);
+ const approvalFlowNodes = Array.isArray(mapped.approvalFlowNodes)
+ ? mapped.approvalFlowNodes
+ : [];
+ const approvalFlowProgressNodes = tasks.length
+ ? mapTasksToApprovalFlowNodes(tasks)
+ : approvalFlowNodes;
+ const flowNodes = tasks.length
+ ? mapTasksToFlowNodes(tasks)
+ : mapped.flowNodes || mapped.nodes || [];
+
+ return {
+ ...mapped,
+ tasks,
+ storageBlobVOList: attachments,
+ attachmentList: attachments,
+ invoiceAttachments: attachments,
+ approvalRecords,
+ approvalFlowNodes,
+ approvalFlowProgressNodes,
+ currentNodeIndex: computeApprovalFlowCurrentIndex(
+ approvalFlowProgressNodes.length ? approvalFlowProgressNodes : approvalFlowNodes
+ ),
+ rejectReason:
+ approvalRecords.find(r => r.result === "rejected")?.opinion ||
+ source.rejectReason ||
+ "",
+ flowNodes,
+ };
+}
diff --git a/src/pages/oa/ReimburseManage/_utils/travelReimburseUtils.js b/src/pages/oa/ReimburseManage/_utils/travelReimburseUtils.js
new file mode 100644
index 0000000..b620079
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/_utils/travelReimburseUtils.js
@@ -0,0 +1,82 @@
+import dayjs from "dayjs";
+
+export const EXPENSE_SUBJECT_OPTIONS = [
+ { label: "浜ら�氳垂", value: "transport" },
+ { label: "浣忓璐�", value: "hotel" },
+ { label: "椁愰ギ璐�", value: "meal" },
+ { label: "鍏朵粬", value: "other" },
+];
+
+const TIER1_CITIES = ["鍖椾含", "涓婃捣", "骞垮窞", "娣卞湷"];
+
+export function expenseSubjectLabel(v) {
+ return EXPENSE_SUBJECT_OPTIONS.find(x => x.value === v)?.label || "鈥�";
+}
+
+export function detectTravelTier(destination) {
+ const city = (destination || "").trim();
+ if (!city) return "tier3";
+ if (TIER1_CITIES.some(c => city.includes(c))) return "tier1";
+ const tier2Keywords = ["鏉窞", "鍗椾含", "姝︽眽", "鎴愰兘", "閲嶅簡", "瑗垮畨", "澶╂触", "鑻忓窞", "闀挎矙", "閮戝窞"];
+ if (tier2Keywords.some(c => city.includes(c))) return "tier2";
+ return "tier3";
+}
+
+export function getTravelStandardByTier(tier) {
+ const map = {
+ tier1: { hotelPerNight: 600, transportPerDay: 80, mealPerDay: 100, label: "涓�绾垮煄甯�" },
+ tier2: { hotelPerNight: 450, transportPerDay: 60, mealPerDay: 80, label: "浜岀嚎鍩庡競" },
+ tier3: { hotelPerNight: 350, transportPerDay: 40, mealPerDay: 60, label: "鍏朵粬鍩庡競" },
+ };
+ return map[tier] || map.tier3;
+}
+
+export function computeTravelDays(startStr, endStr) {
+ if (!startStr || !endStr) return null;
+ const t0 = dayjs(startStr);
+ const t1 = dayjs(endStr);
+ if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
+ return Math.max(1, Math.ceil(t1.diff(t0, "day", true)));
+}
+
+export function createEmptyExpenseDetail() {
+ return {
+ id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
+ invoiceDate: "",
+ expenseSubject: "",
+ amount: "",
+ description: "",
+ };
+}
+
+export function createEmptyTravelForm() {
+ return {
+ reimbursementId: undefined,
+ applicantId: "",
+ employeeNo: "",
+ employeeName: "",
+ reimburseReason: "",
+ travelStartTime: "",
+ travelEndTime: "",
+ travelDays: undefined,
+ departurePlace: "",
+ destination: "",
+ hotelStandard: undefined,
+ hotelDays: undefined,
+ livingSubsidy: undefined,
+ transportSubsidy: undefined,
+ lodgingLimit: undefined,
+ applyAmount: "",
+ payee: "",
+ payeeAccount: "",
+ payeeBank: "",
+ expenseDetails: [],
+ attachmentList: [],
+ approvalFlowNodes: [{ approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" }],
+ needSpecialApproval: false,
+ deptId: "",
+ deptName: "",
+ travelTier: "tier3",
+ standardTag: "",
+ };
+}
diff --git a/src/pages/oa/ReimburseManage/cost-reimburse/index.vue b/src/pages/oa/ReimburseManage/cost-reimburse/index.vue
new file mode 100644
index 0000000..dfa4365
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/cost-reimburse/index.vue
@@ -0,0 +1,11 @@
+<!--
+ OA / 鎶ラ攢绠$悊 / 璐圭敤鎶ラ攢锛�/finReimbursement/listPage锛宺eimbursementType=2锛�
+-->
+<template>
+ <FinReimbursementListPage :module-key="APPROVAL_MODULE_KEYS.COST_REIMBURSE" />
+</template>
+
+<script setup>
+ import FinReimbursementListPage from "../../_components/FinReimbursementListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+</script>
diff --git a/src/pages/oa/ReimburseManage/reimburse-detail/index.vue b/src/pages/oa/ReimburseManage/reimburse-detail/index.vue
new file mode 100644
index 0000000..a7aabe9
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/reimburse-detail/index.vue
@@ -0,0 +1,120 @@
+<!--
+ 宸梾/璐圭敤鎶ラ攢璇︽儏椤�
+-->
+<template>
+ <view class="oa-detail-page reimburse-detail-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <view v-if="loading"
+ class="rd-loading-wrap">
+ <up-loading-icon mode="circle" />
+ <text class="rd-loading-text">鍔犺浇涓�...</text>
+ </view>
+
+ <scroll-view v-else-if="reimburseRow"
+ class="oa-detail-scroll reimburse-detail-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <ReimburseInstanceDetailBody :reimburse-row="reimburseRow"
+ :module-key="moduleKey" />
+ </scroll-view>
+
+ <view v-else
+ class="oa-empty">
+ <up-empty mode="data"
+ text="鏈幏鍙栧埌鎶ラ攢鏁版嵁" />
+ </view>
+
+ <view v-if="reimburseRow && canEdit"
+ class="oa-page-footer">
+ <text class="oa-footer-btn btn-default"
+ @click="goBack">杩斿洖</text>
+ <text class="oa-footer-btn btn-primary"
+ @click="goEdit">淇敼</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ReimburseInstanceDetailBody from "../_components/ReimburseInstanceDetailBody.vue";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js";
+ import {
+ canEditReimbursementRow,
+ fetchFinReimbursementListItemDetail,
+ resolveReimbursementDeleteId,
+ } from "../../_utils/finReimbursementMappers.js";
+
+ const moduleKey = ref("");
+ const reimbursementId = ref("");
+ const reimburseRow = ref(null);
+ const loading = ref(false);
+
+ const pageTitle = computed(
+ () => `${getApprovalModuleConfig(moduleKey.value)?.label || "鎶ラ攢"}璇︽儏`
+ );
+
+ const canEdit = computed(() =>
+ reimburseRow.value ? canEditReimbursementRow(reimburseRow.value) : false
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ const rid = resolveReimbursementDeleteId(reimburseRow.value);
+ if (rid == null) {
+ uni.showToast({ title: "鏃犳硶淇敼", icon: "none" });
+ return;
+ }
+ uni.navigateTo({
+ url: `${OA_NAV.reimburseForm}?moduleKey=${moduleKey.value}&mode=edit&reimbursementId=${rid}`,
+ });
+ };
+
+ onLoad(async options => {
+ moduleKey.value = options?.moduleKey || "";
+ reimbursementId.value = options?.reimbursementId || "";
+ if (!moduleKey.value || !reimbursementId.value) {
+ uni.showToast({ title: "鍙傛暟涓嶅畬鏁�", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ loading.value = true;
+ try {
+ reimburseRow.value = await fetchFinReimbursementListItemDetail(
+ { reimbursementId: reimbursementId.value },
+ moduleKey.value
+ );
+ if (reimburseRow.value?.moduleKey) {
+ moduleKey.value = reimburseRow.value.moduleKey;
+ }
+ } catch {
+ uni.showToast({ title: "鍔犺浇璇︽儏澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "../../_styles/oa-approval-list.scss";
+ @import "./reimburse-detail.scss";
+
+ .rd-loading-wrap {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 80px 0;
+ }
+ .rd-loading-text {
+ margin-top: 12px;
+ font-size: 14px;
+ color: #909399;
+ }
+</style>
diff --git a/src/pages/oa/ReimburseManage/reimburse-detail/reimburse-detail.scss b/src/pages/oa/ReimburseManage/reimburse-detail/reimburse-detail.scss
new file mode 100644
index 0000000..660a64b
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/reimburse-detail/reimburse-detail.scss
@@ -0,0 +1,344 @@
+.reimburse-detail-page {
+ min-height: 100vh;
+ background: #f2f4f7;
+}
+
+.reimburse-detail-scroll {
+ padding-bottom: calc(72px + env(safe-area-inset-bottom));
+}
+
+.rd-hero {
+ margin: 12px 16px 0;
+ padding: 16px;
+ background: linear-gradient(135deg, #f0f7ff 0%, #fff 55%);
+ border-radius: 12px;
+ box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06);
+ border: 1px solid #e8f0fe;
+}
+
+.rd-hero-top {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.rd-bill-no {
+ font-size: 13px;
+ color: #8c8c8c;
+ flex: 1;
+ word-break: break-all;
+}
+
+.rd-status {
+ flex-shrink: 0;
+ font-size: 11px;
+ padding: 5px 8px;
+ border-radius: 4px;
+ font-weight: 500;
+
+ &.status-pending {
+ color: #d46b08;
+ background: #fff7e6;
+ }
+ &.status-approved {
+ color: #389e0d;
+ background: #f6ffed;
+ }
+ &.status-rejected {
+ color: #cf1322;
+ background: #fff1f0;
+ }
+ &.status-draft {
+ color: #595959;
+ background: #f5f5f5;
+ }
+ &.status-cancelled {
+ color: #8c8c8c;
+ background: #fafafa;
+ }
+}
+
+.rd-reason {
+ display: block;
+ margin-top: 10px;
+ font-size: 17px;
+ font-weight: 600;
+ color: #1a1a1a;
+ line-height: 1.45;
+}
+
+.rd-amount-row {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-top: 14px;
+ padding-top: 12px;
+ border-top: 1px dashed #e8ecf0;
+}
+
+.rd-amount-label {
+ font-size: 14px;
+ color: #8c8c8c;
+}
+
+.rd-amount {
+ font-size: 22px;
+ font-weight: 700;
+ color: #2979ff;
+}
+
+.rd-section {
+ margin: 12px 16px 0;
+}
+
+.rd-section-hd {
+ padding: 4px 4px 8px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.rd-section-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #909399;
+}
+
+.rd-section-count {
+ font-size: 12px;
+ color: #c0c4cc;
+}
+
+.rd-group {
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 1px 4px rgba(15, 23, 42, 0.04);
+}
+
+.rd-cell {
+ display: flex;
+ align-items: flex-start;
+ padding: 13px 16px;
+ border-bottom: 1px solid #f5f6f8;
+ font-size: 14px;
+ line-height: 1.45;
+
+ &:last-child {
+ border-bottom: none;
+ }
+}
+
+.rd-label {
+ width: 88px;
+ flex-shrink: 0;
+ color: #8c8c8c;
+}
+
+.rd-value {
+ flex: 1;
+ color: #303133;
+ text-align: right;
+ word-break: break-all;
+}
+
+.rd-detail-item {
+ padding: 14px 16px;
+ border-bottom: 1px solid #f5f6f8;
+
+ &:last-child {
+ border-bottom: none;
+ }
+}
+
+.rd-detail-head {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.rd-detail-badge {
+ width: 24px;
+ height: 24px;
+ border-radius: 6px;
+ background: #ecf5ff;
+ color: #2979ff;
+ font-size: 13px;
+ font-weight: 600;
+ text-align: center;
+ line-height: 24px;
+ margin-right: 8px;
+}
+
+.rd-detail-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.rd-detail-amount {
+ margin-left: auto;
+ font-size: 15px;
+ font-weight: 600;
+ color: #2979ff;
+}
+
+.rd-flow-node {
+ display: flex;
+ padding: 12px 16px;
+ border-bottom: 1px solid #f5f6f8;
+
+ &:last-child {
+ border-bottom: none;
+ }
+}
+
+.rd-flow-line {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-right: 12px;
+ width: 20px;
+}
+
+.rd-flow-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: #2979ff;
+ flex-shrink: 0;
+}
+
+.rd-flow-bar {
+ flex: 1;
+ width: 2px;
+ min-height: 20px;
+ background: #e4e7ed;
+ margin-top: 4px;
+}
+
+.rd-flow-body {
+ flex: 1;
+ min-width: 0;
+}
+
+.rd-flow-level {
+ font-size: 14px;
+ font-weight: 500;
+ color: #303133;
+}
+
+.rd-flow-type {
+ font-size: 12px;
+ color: #909399;
+ margin-top: 2px;
+}
+
+.rd-flow-approver {
+ display: flex;
+ align-items: center;
+ margin-top: 8px;
+}
+
+.rd-flow-avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ color: #fff;
+ font-size: 12px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 8px;
+}
+
+.rd-flow-approver-meta {
+ flex: 1;
+ min-width: 0;
+}
+
+.rd-flow-name {
+ display: block;
+ font-size: 14px;
+ color: #303133;
+}
+
+.rd-flow-status {
+ display: block;
+ font-size: 12px;
+ color: #909399;
+ margin-top: 2px;
+}
+
+.rd-record-item {
+ padding: 14px 16px;
+ border-bottom: 1px solid #f5f6f8;
+
+ &:last-child {
+ border-bottom: none;
+ }
+}
+
+.rd-record-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.rd-record-operator {
+ font-size: 15px;
+ font-weight: 500;
+ color: #303133;
+}
+
+.rd-record-tag {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ flex-shrink: 0;
+
+ &--approved {
+ color: #389e0d;
+ background: #f6ffed;
+ }
+ &--rejected {
+ color: #cf1322;
+ background: #fff1f0;
+ }
+ &--pending {
+ color: #d46b08;
+ background: #fff7e6;
+ }
+}
+
+.rd-record-time {
+ display: block;
+ font-size: 12px;
+ color: #c0c4cc;
+ margin-top: 4px;
+}
+
+.rd-record-opinion {
+ display: block;
+ font-size: 13px;
+ color: #606266;
+ margin-top: 6px;
+ line-height: 1.45;
+}
+
+.rd-empty {
+ padding: 20px;
+ text-align: center;
+ font-size: 13px;
+ color: #c0c4cc;
+}
+
+.rd-attach {
+ padding: 12px 16px;
+ font-size: 14px;
+ color: #2979ff;
+ border-bottom: 1px solid #f5f6f8;
+}
diff --git a/src/pages/oa/ReimburseManage/reimburse-form/index.vue b/src/pages/oa/ReimburseManage/reimburse-form/index.vue
new file mode 100644
index 0000000..35eb65d
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/reimburse-form/index.vue
@@ -0,0 +1,564 @@
+<!--
+ 宸梾/璐圭敤鎶ラ攢鏂板/缂栬緫锛堜笌 Web 瀛楁涓�鑷达紝绉诲姩绔紭鍖栭�変汉/甯冨眬锛�
+-->
+<template>
+ <view class="oa-detail-page reimburse-form-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <scroll-view class="oa-detail-scroll reimburse-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view v-if="loading"
+ class="rf-loading">鍔犺浇涓�...</view>
+
+ <view v-else>
+ <!-- 鐢宠浜� -->
+ <view class="rf-section">
+ <view class="rf-section-hd">
+ <text class="rf-section-title">鐢宠浜�</text>
+ </view>
+ <view class="rf-group">
+ <view class="rf-applicant-card"
+ :class="{ 'is-empty': !form.applicantId }"
+ @click="showApplicantPicker = true">
+ <view class="rf-applicant-avatar"
+ :style="{ backgroundColor: applicantAvatarColor }">
+ {{ (form.employeeName || '閫�').charAt(0) }}
+ </view>
+ <view class="rf-applicant-meta">
+ <text class="rf-applicant-name">{{ form.employeeName || '璇烽�夋嫨鍛樺伐' }}</text>
+ <text class="rf-applicant-sub">{{ applicantDisplaySub }}</text>
+ </view>
+ <text class="rf-applicant-action">{{ form.applicantId ? '鏇存崲' : '閫夋嫨' }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍩烘湰淇℃伅 -->
+ <view class="rf-section">
+ <view class="rf-section-hd">
+ <text class="rf-section-title">鍩烘湰淇℃伅</text>
+ </view>
+ <view class="rf-group">
+ <view class="rf-cell rf-cell--col">
+ <text class="rf-label required">鎶ラ攢鍘熷洜</text>
+ <view class="rf-textarea-wrap">
+ <up-textarea v-model="form.reimburseReason"
+ placeholder="璇峰~鍐欏嚭宸強鎶ラ攢鍘熷洜"
+ maxlength="2000"
+ border="none"
+ height="80" />
+ </view>
+ </view>
+
+ <template v-if="isTravel">
+ <view class="rf-cell rf-cell--tap"
+ @click="openDatePicker('travelStartTime')">
+ <text class="rf-label required">鍑哄樊寮�濮�</text>
+ <view class="rf-value-wrap">
+ <text class="rf-value"
+ :class="{ placeholder: !form.travelStartTime }">
+ {{ form.travelStartTime || '璇烽�夋嫨' }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#c0c4cc" />
+ </view>
+ </view>
+ <view class="rf-cell rf-cell--tap"
+ @click="openDatePicker('travelEndTime')">
+ <text class="rf-label required">鍑哄樊缁撴潫</text>
+ <view class="rf-value-wrap">
+ <text class="rf-value"
+ :class="{ placeholder: !form.travelEndTime }">
+ {{ form.travelEndTime || '璇烽�夋嫨' }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#c0c4cc" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">鍑哄樊澶╂暟</text>
+ <view class="rf-value-wrap">
+ <text class="rf-value">{{ travelDaysDisplay || '鈥�' }}</text>
+ <text class="rf-value"
+ style="color:#909399;margin-left:4px">澶�</text>
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label required">鍑哄樊鍦�</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.departurePlace"
+ placeholder="鍑哄彂鍩庡競"
+ border="none"
+ input-align="right"
+ @blur="recalcTravelStandards" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label required">鐩殑鍦�</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.destination"
+ placeholder="鐩殑鍩庡競"
+ border="none"
+ input-align="right"
+ @blur="recalcTravelStandards" />
+ </view>
+ </view>
+ </template>
+
+ <template v-else>
+ <view class="rf-cell rf-cell--tap"
+ @click="showCategorySheet = true">
+ <text class="rf-label required">璐圭敤绫诲瀷</text>
+ <view class="rf-value-wrap">
+ <text class="rf-value"
+ :class="{ placeholder: !form.expenseCategory }">{{ categoryLabel }}</text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc" />
+ </view>
+ </view>
+ <view class="rf-chips">
+ <text v-for="cat in quickCategories"
+ :key="cat.value"
+ class="rf-chip"
+ :class="{ active: form.expenseCategory === cat.value }"
+ @click="applyTemplate(cat.value)">{{ cat.label }}</text>
+ </view>
+ </template>
+ </view>
+ </view>
+
+ <!-- 宸梾鏍囧噯 -->
+ <view v-if="isTravel"
+ class="rf-section">
+ <view class="rf-section-hd">
+ <text class="rf-section-title">宸梾鏍囧噯</text>
+ <text class="rf-section-extra">{{ travelTierLabel }}</text>
+ </view>
+ <view v-if="overBudgetWarnings.length"
+ class="rf-warn-box">
+ <text v-for="(w, i) in overBudgetWarnings"
+ :key="i"
+ class="rf-warn-line">{{ w }}</text>
+ </view>
+ <view class="rf-group">
+ <view class="rf-cell">
+ <text class="rf-label">閰掑簵鏍囧噯</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.hotelStandard"
+ type="digit"
+ placeholder="鍏�/鏅�"
+ border="none"
+ input-align="right"
+ @blur="recalcTravelStandards" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">浣忓澶╂暟</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.hotelDays"
+ type="number"
+ border="none"
+ input-align="right"
+ @blur="recalcTravelStandards" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">鐢熸椿琛ヨ创</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.livingSubsidy"
+ type="digit"
+ border="none"
+ input-align="right" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">浜ら�氳ˉ璐�</text>
+ <view class="rf-value-wrap">
+ <text class="rf-value">寤鸿 {{ suggestedTransportSubsidy }} 鍏�</text>
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">浣忓闄愰</text>
+ <view class="rf-value-wrap">
+ <text class="rf-value">寤鸿 {{ suggestedHotelLimit }} 鍏�</text>
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">鐗规壒鏍囪</text>
+ <text class="rf-tag"
+ :class="form.needSpecialApproval ? 'rf-tag--danger' : 'rf-tag--ok'">
+ {{ form.needSpecialApproval ? '瓒呮敮闇�鐗规壒' : '鍦ㄦ爣鍑嗗唴' }}
+ </text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 閲戦涓庢敹娆� -->
+ <view class="rf-section">
+ <view class="rf-section-hd">
+ <text class="rf-section-title">閲戦涓庢敹娆�</text>
+ <text class="rf-section-extra"
+ @click="syncApplyAmountFromDetails">鎸夋槑缁� {{ detailTotalAmount }} 鍏�</text>
+ </view>
+ <view class="rf-group">
+ <view class="rf-cell">
+ <text class="rf-label required">鐢宠閲戦</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.applyAmount"
+ type="digit"
+ placeholder="鍏�"
+ border="none"
+ input-align="right" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label required">鏀舵浜�</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.payee"
+ placeholder="鏀舵浜�"
+ border="none"
+ input-align="right" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">鏀舵璐﹀彿</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.payeeAccount"
+ placeholder="閫夊~"
+ border="none"
+ input-align="right" />
+ </view>
+ </view>
+ <view class="rf-cell">
+ <text class="rf-label">寮�鎴锋敮琛�</text>
+ <view class="rf-input-body">
+ <up-input v-if="isTravel"
+ v-model="form.payeeBank"
+ placeholder="閫夊~"
+ border="none"
+ input-align="right" />
+ <up-input v-else
+ v-model="form.bankBranch"
+ placeholder="閫夊~"
+ border="none"
+ input-align="right" />
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鎶ラ攢鏄庣粏锛氬垪琛ㄦ憳瑕� + 璇︽儏鎸夐挳 -->
+ <view class="rf-section">
+ <view class="rf-section-hd">
+ <text class="rf-section-title">鎶ラ攢鏄庣粏</text>
+ <text class="rf-section-extra"
+ @click="addAndOpenDetail">+ 鏂板</text>
+ </view>
+ <view class="rf-group"
+ v-if="form.expenseDetails.length">
+ <view v-for="(row, idx) in form.expenseDetails"
+ :key="row.id || idx"
+ class="rf-detail-row"
+ :class="{ 'rf-detail-row--warn': detailSummary(row).incomplete }"
+ @click="openDetailEditor(idx)">
+ <view class="rf-detail-index">{{ idx + 1 }}</view>
+ <view class="rf-detail-body">
+ <view class="rf-detail-line1">
+ <text class="rf-detail-subject">{{ detailSummary(row).subject }}</text>
+ <text class="rf-detail-amount">{{ detailSummary(row).amount }}</text>
+ </view>
+ <text class="rf-detail-line2">{{ detailSummary(row).sub }}</text>
+ </view>
+ <text class="rf-detail-action"
+ @click.stop="openDetailEditor(idx)">璇︽儏</text>
+ </view>
+ </view>
+ <view v-else
+ class="rf-group">
+ <view class="rf-empty"
+ @click="addAndOpenDetail">鐐瑰嚮娣诲姞鎶ラ攢鏄庣粏</view>
+ </view>
+ </view>
+
+ <!-- 闄勪欢 -->
+ <view class="rf-section">
+ <view class="rf-section-hd">
+ <text class="rf-section-title">闄勪欢锛堝彂绁級</text>
+ </view>
+ <view class="rf-group">
+ <view v-for="(f, i) in form.attachmentList"
+ :key="i"
+ class="rf-attach-item">
+ <text>{{ f.name || '闄勪欢' }}</text>
+ <text class="rf-detail-del"
+ @click="removeAttachment(i)">鍒犻櫎</text>
+ </view>
+ <view class="rf-upload-zone"
+ @click="chooseAttachment">
+ <up-icon name="plus-circle"
+ size="22"
+ color="#2979ff" />
+ <text>涓婁紶鍙戠エ/闄勪欢</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 瀹℃壒娴佺▼ -->
+ <view class="rf-section">
+ <view class="rf-section-hd">
+ <text class="rf-section-title">瀹℃壒娴佺▼</text>
+ </view>
+ <view class="rf-group"
+ style="padding:12px">
+ <ReimburseApprovalFlowEditor v-model="form.approvalFlowNodes"
+ :user-options="flowUserOptions" />
+ <text class="rf-hint-row">姣忕骇椤绘寚瀹氬鎵逛汉锛屾敮鎸佹悳绱㈠鍚嶆垨宸ュ彿</text>
+ </view>
+ </view>
+ </view>
+ </scroll-view>
+
+ <view class="oa-page-footer">
+ <text class="oa-footer-btn btn-default"
+ @click="goBack">鍙栨秷</text>
+ <text class="oa-footer-btn btn-primary"
+ :class="{ 'is-disabled': submitting }"
+ @click="onSubmit">鎻愪氦</text>
+ </view>
+
+ <OaUserSearchPicker v-model:show="showApplicantPicker"
+ v-model="form.applicantId"
+ title="閫夋嫨鐢宠浜�"
+ :users="flowUserOptions"
+ @select="onApplicantPicked" />
+
+ <up-action-sheet :show="showCategorySheet"
+ title="璐圭敤绫诲瀷"
+ :actions="categoryActions"
+ @select="onCategorySelect"
+ @close="showCategorySheet = false" />
+
+ <ReimburseExpenseDetailSheet v-model:show="showDetailSheet"
+ v-model="detailDraft"
+ :index="editingDetailIndex"
+ :is-travel="isTravel"
+ :subject-options="expenseSubjectOptions"
+ @confirm="onDetailSheetConfirm"
+ @delete="onDetailSheetDelete" />
+
+ <up-popup :show="showDatePicker"
+ mode="bottom"
+ round="16"
+ @close="showDatePicker = false">
+ <up-datetime-picker :show="true"
+ v-model="datePickerTs"
+ mode="datetime"
+ @confirm="onDateConfirm"
+ @cancel="showDatePicker = false" />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import OaUserSearchPicker from "../../_components/OaUserSearchPicker.vue";
+ import ReimburseExpenseDetailSheet from "../_components/ReimburseExpenseDetailSheet.vue";
+ import config from "@/config.js";
+ import { getToken } from "@/utils/auth";
+ import { parseTime } from "@/utils/ruoyi";
+ import { getApprovalModuleConfig } from "../../_utils/approvalModuleRegistry.js";
+ import { consumeReimburseEditFromApprove } from "../../_utils/reimburseApproveBridge.js";
+ import { EXPENSE_CATEGORY_OPTIONS } from "../_utils/costReimburseUtils.js";
+ import { buildExpenseDetailSummary } from "../_utils/expenseDetailDisplay.js";
+ import ReimburseApprovalFlowEditor from "../_components/ReimburseApprovalFlowEditor.vue";
+ import { useFinReimburseForm } from "./useFinReimburseForm.js";
+
+ const moduleKey = ref("");
+ const mode = ref("add");
+ const reimbursementId = ref("");
+
+ const {
+ form,
+ isTravel,
+ submitting,
+ loading,
+ flowUserOptions,
+ travelDaysDisplay,
+ travelTierLabel,
+ suggestedTransportSubsidy,
+ suggestedHotelLimit,
+ detailTotalAmount,
+ overBudgetWarnings,
+ expenseSubjectOptions,
+ categoryActions,
+ categoryLabel,
+ showApplicantPicker,
+ applicantDisplaySub,
+ applicantAvatarColor,
+ showCategorySheet,
+ loadUserPool,
+ onApplicantPicked,
+ recalcTravelStandards,
+ syncApplyAmountFromDetails,
+ addExpenseDetail,
+ removeExpenseDetail,
+ applyTemplate,
+ initForm,
+ loadEdit,
+ submitForm,
+ } = useFinReimburseForm(moduleKey, mode);
+
+ const showDatePicker = ref(false);
+ const datePickerField = ref("");
+ const datePickerTs = ref(Date.now());
+
+ const showDetailSheet = ref(false);
+ const editingDetailIndex = ref(0);
+ const detailDraft = reactive({
+ invoiceDate: "",
+ expenseSubject: "",
+ amount: "",
+ description: "",
+ });
+
+ const quickCategories = EXPENSE_CATEGORY_OPTIONS.slice(0, 4);
+
+ const pageTitle = computed(() => {
+ const label = getApprovalModuleConfig(moduleKey.value)?.label || "鎶ラ攢";
+ return mode.value === "edit" ? `缂栬緫${label}` : `鏂板${label}`;
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ function detailSummary(row) {
+ return buildExpenseDetailSummary(row, {
+ isTravel: isTravel.value,
+ subjectOptions: expenseSubjectOptions.value,
+ });
+ }
+
+ function openDetailEditor(idx) {
+ editingDetailIndex.value = idx;
+ const row = form.expenseDetails[idx];
+ if (!row) return;
+ Object.assign(detailDraft, JSON.parse(JSON.stringify(row)));
+ showDetailSheet.value = true;
+ }
+
+ function addAndOpenDetail() {
+ addExpenseDetail();
+ openDetailEditor(form.expenseDetails.length - 1);
+ }
+
+ function onDetailSheetConfirm(data) {
+ const idx = editingDetailIndex.value;
+ if (form.expenseDetails[idx]) {
+ Object.assign(form.expenseDetails[idx], data);
+ }
+ recalcTravelStandards();
+ }
+
+ function onDetailSheetDelete() {
+ const idx = editingDetailIndex.value;
+ removeExpenseDetail(idx);
+ showDetailSheet.value = false;
+ }
+
+ function onCategorySelect(action) {
+ form.expenseCategory = action.value;
+ applyTemplate(action.value);
+ showCategorySheet.value = false;
+ }
+
+ function openDatePicker(field) {
+ datePickerField.value = field;
+ detailDateIndex.value = -1;
+ datePickerTs.value = Date.now();
+ showDatePicker.value = true;
+ }
+
+ function onDateConfirm(e) {
+ const ts = e?.value ?? datePickerTs.value;
+ if (datePickerField.value) {
+ form[datePickerField.value] = parseTime(ts, "{y}-{m}-{d} {h}:{i}:{s}");
+ recalcTravelStandards();
+ }
+ showDatePicker.value = false;
+ }
+
+ function chooseAttachment() {
+ uni.chooseImage({
+ count: 9,
+ success: res => {
+ (res.tempFilePaths || []).forEach(path => uploadOne(path));
+ },
+ });
+ }
+
+ function uploadOne(filePath) {
+ uni.uploadFile({
+ url: `${config.baseUrl}/file/upload`,
+ filePath,
+ name: "file",
+ header: { Authorization: "Bearer " + getToken() },
+ success: res => {
+ try {
+ const data = JSON.parse(res.data || "{}");
+ const url = data.url || data.data?.url || "";
+ const name = data.originalFilename || data.fileName || "闄勪欢";
+ if (!form.attachmentList) form.attachmentList = [];
+ form.attachmentList.push({ name, url });
+ } catch {
+ uni.showToast({ title: "涓婁紶瑙f瀽澶辫触", icon: "none" });
+ }
+ },
+ fail: () => uni.showToast({ title: "涓婁紶澶辫触", icon: "none" }),
+ });
+ }
+
+ function removeAttachment(i) {
+ form.attachmentList.splice(i, 1);
+ }
+
+ async function onSubmit() {
+ const ok = await submitForm();
+ if (ok) setTimeout(goBack, 400);
+ }
+
+ onLoad(async options => {
+ moduleKey.value = options?.moduleKey || "";
+ mode.value = options?.mode === "edit" ? "edit" : "add";
+ reimbursementId.value = options?.reimbursementId || "";
+ const fromApprove = consumeReimburseEditFromApprove();
+ if (fromApprove?.moduleKey) {
+ moduleKey.value = fromApprove.moduleKey;
+ mode.value = "edit";
+ reimbursementId.value = String(fromApprove.reimbursementId ?? "");
+ }
+ if (!moduleKey.value) {
+ uni.showToast({ title: "缂哄皯妯″潡绫诲瀷", icon: "none" });
+ setTimeout(goBack, 500);
+ return;
+ }
+ await loadUserPool();
+ await initForm();
+ if (mode.value === "edit" && reimbursementId.value) {
+ try {
+ await loadEdit(reimbursementId.value);
+ } catch {
+ uni.showToast({ title: "鍔犺浇澶辫触", icon: "none" });
+ }
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "../../_styles/oa-approval-list.scss";
+ @import "./reimburse-form.scss";
+</style>
diff --git a/src/pages/oa/ReimburseManage/reimburse-form/reimburse-form.scss b/src/pages/oa/ReimburseManage/reimburse-form/reimburse-form.scss
new file mode 100644
index 0000000..e50634d
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/reimburse-form/reimburse-form.scss
@@ -0,0 +1,354 @@
+.reimburse-form-page {
+ min-height: 100vh;
+ background: #f2f4f7;
+}
+
+.reimburse-scroll {
+ padding-bottom: calc(80px + env(safe-area-inset-bottom));
+}
+
+.rf-section {
+ margin: 12px 16px 0;
+}
+
+.rf-section-hd {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 4px 8px;
+}
+
+.rf-section-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: #909399;
+ letter-spacing: 0.5px;
+}
+
+.rf-section-extra {
+ font-size: 13px;
+ color: #2979ff;
+}
+
+.rf-group {
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+}
+
+.rf-applicant-card {
+ display: flex;
+ align-items: center;
+ padding: 16px;
+ background: linear-gradient(135deg, #f8fbff 0%, #fff 60%);
+ border-bottom: 1px solid #f0f2f5;
+
+ &.is-empty {
+ background: #fff;
+ }
+}
+
+.rf-applicant-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ color: #fff;
+ font-size: 18px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.rf-applicant-meta {
+ flex: 1;
+ margin-left: 12px;
+ min-width: 0;
+}
+
+.rf-applicant-name {
+ font-size: 17px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.rf-applicant-sub {
+ font-size: 13px;
+ color: #909399;
+ margin-top: 4px;
+}
+
+.rf-applicant-action {
+ font-size: 14px;
+ color: #2979ff;
+ padding: 6px 12px;
+ background: #ecf5ff;
+ border-radius: 16px;
+ flex-shrink: 0;
+}
+
+.rf-cell {
+ display: flex;
+ align-items: center;
+ min-height: 52px;
+ padding: 12px 16px;
+ border-bottom: 1px solid #f5f6f8;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &--tap:active {
+ background: #f9fafb;
+ }
+
+ &--col {
+ flex-direction: column;
+ align-items: stretch;
+ min-height: auto;
+ padding-bottom: 14px;
+ }
+}
+
+.rf-label {
+ width: 88px;
+ flex-shrink: 0;
+ font-size: 15px;
+ color: #303133;
+
+ &.required::before {
+ content: "*";
+ color: #f56c6c;
+ margin-right: 2px;
+ }
+}
+
+.rf-value-wrap {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ min-width: 0;
+ gap: 4px;
+}
+
+.rf-value {
+ font-size: 15px;
+ color: #303133;
+ text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &.placeholder {
+ color: #c0c4cc;
+ }
+}
+
+.rf-input-body {
+ flex: 1;
+ min-width: 0;
+}
+
+.rf-textarea-wrap {
+ width: 100%;
+ margin-top: 8px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ padding: 4px 8px;
+}
+
+.rf-inline-input {
+ text-align: right;
+ font-size: 15px;
+}
+
+.rf-hint-row {
+ padding: 8px 16px 12px;
+ font-size: 12px;
+ color: #909399;
+}
+
+.rf-warn-box {
+ margin: 0 16px 8px;
+ padding: 10px 12px;
+ background: #fdf6ec;
+ border-radius: 8px;
+ border-left: 3px solid #e6a23c;
+}
+
+.rf-warn-line {
+ display: block;
+ font-size: 12px;
+ color: #e6a23c;
+ line-height: 1.5;
+}
+
+.rf-tag {
+ font-size: 13px;
+ padding: 4px 10px;
+ border-radius: 4px;
+ &--ok {
+ color: #67c23a;
+ background: #f0f9eb;
+ }
+ &--danger {
+ color: #f56c6c;
+ background: #fef0f0;
+ }
+}
+
+.rf-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding: 0 16px 14px;
+}
+
+.rf-chip {
+ font-size: 13px;
+ padding: 6px 14px;
+ background: #f5f7fa;
+ color: #606266;
+ border-radius: 20px;
+ border: 1px solid #ebeef5;
+
+ &.active {
+ background: #ecf5ff;
+ color: #2979ff;
+ border-color: #b3d8ff;
+ }
+}
+
+.rf-detail-row {
+ display: flex;
+ align-items: center;
+ padding: 14px 16px;
+ border-bottom: 1px solid #f5f6f8;
+ min-height: 64px;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:active {
+ background: #f9fafb;
+ }
+
+ &--warn .rf-detail-subject {
+ color: #e6a23c;
+ }
+}
+
+.rf-detail-index {
+ width: 28px;
+ height: 28px;
+ border-radius: 8px;
+ background: #ecf5ff;
+ color: #2979ff;
+ font-size: 14px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.rf-detail-body {
+ flex: 1;
+ margin: 0 10px;
+ min-width: 0;
+}
+
+.rf-detail-line1 {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.rf-detail-subject {
+ font-size: 15px;
+ font-weight: 500;
+ color: #303133;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.rf-detail-amount {
+ font-size: 15px;
+ font-weight: 600;
+ color: #2979ff;
+ flex-shrink: 0;
+}
+
+.rf-detail-line2 {
+ display: block;
+ font-size: 12px;
+ color: #909399;
+ margin-top: 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.rf-detail-action {
+ flex-shrink: 0;
+ font-size: 14px;
+ color: #2979ff;
+ padding: 6px 12px;
+ background: #ecf5ff;
+ border-radius: 16px;
+ border: 1px solid #d9ecff;
+}
+
+.rf-detail-del {
+ font-size: 13px;
+ color: #f56c6c;
+}
+
+.rf-upload-zone {
+ margin: 0 16px 14px;
+ padding: 20px;
+ border: 1px dashed #c0c4cc;
+ border-radius: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 6px;
+ color: #2979ff;
+ font-size: 14px;
+ background: #fafbfc;
+}
+
+.rf-attach-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 16px;
+ border-bottom: 1px solid #f5f6f8;
+ font-size: 14px;
+}
+
+.rf-link {
+ font-size: 13px;
+ color: #2979ff;
+ padding: 4px 0;
+}
+
+.rf-empty {
+ text-align: center;
+ padding: 20px;
+ color: #c0c4cc;
+ font-size: 13px;
+}
+
+.rf-loading {
+ padding: 60px;
+ text-align: center;
+ color: #909399;
+}
diff --git a/src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js b/src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js
new file mode 100644
index 0000000..1c24bee
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/reimburse-form/useFinReimburseForm.js
@@ -0,0 +1,440 @@
+import { computed, reactive, ref } from "vue";
+import { userListNoPageByTenantId } from "@/api/system/user";
+import useUserStore from "@/store/modules/user";
+import { persistFinReimbursement } from "@/api/oa/finReimbursement.js";
+import {
+ isActiveUser,
+ unwrapUserList,
+ userAvatarColor,
+ userSelectLabel,
+ userSubLabel,
+} from "../../_utils/userPickerUtils.js";
+import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+import {
+ buildCostReimbursementSaveDto,
+ buildTravelReimbursementSaveDto,
+ fetchFinReimbursementFormDetail,
+ getReimbursementTypeByModuleKey,
+ validateReimbursementApprovalNodes,
+ validateReimbursementPersistDto,
+} from "../../_utils/finReimbursementMappers.js";
+import {
+ applyCategoryTemplate,
+ createEmptyCostForm,
+ EXPENSE_CATEGORY_OPTIONS,
+ EXPENSE_SUBJECT_OPTIONS as COST_SUBJECT_OPTIONS,
+ createEmptyExpenseDetail as createCostDetail,
+} from "../_utils/costReimburseUtils.js";
+import {
+ computeTravelDays,
+ createEmptyExpenseDetail,
+ createEmptyTravelForm,
+ detectTravelTier,
+ EXPENSE_SUBJECT_OPTIONS,
+ getTravelStandardByTier,
+} from "../_utils/travelReimburseUtils.js";
+
+const userStore = useUserStore();
+
+function buildOverBudgetWarnings(f, detailTotal, hotelLimit, transportLimit, mealLimit) {
+ const warnings = [];
+ const bySubject = { transport: 0, hotel: 0, meal: 0, other: 0 };
+ (f.expenseDetails || []).forEach(d => {
+ const key = d.expenseSubject || "other";
+ bySubject[key] = (bySubject[key] || 0) + (Number(d.amount) || 0);
+ });
+ if (bySubject.transport > transportLimit && transportLimit > 0) {
+ warnings.push(`浜ら�氳垂 ${bySubject.transport} 鍏冭秴鍑烘爣鍑� ${transportLimit} 鍏僠);
+ }
+ if (bySubject.hotel > hotelLimit && hotelLimit > 0) {
+ warnings.push(`浣忓璐� ${bySubject.hotel} 鍏冭秴鍑洪檺棰� ${hotelLimit} 鍏僠);
+ }
+ if (bySubject.meal > mealLimit && mealLimit > 0) {
+ warnings.push(`椁愰ギ璐� ${bySubject.meal} 鍏冭秴鍑虹敓娲昏ˉ璐村缓璁� ${mealLimit} 鍏僠);
+ }
+ const std = getTravelStandardByTier(f.travelTier);
+ if (Number(f.hotelStandard) > std.hotelPerNight) {
+ warnings.push(`閰掑簵鏍囧噯 ${f.hotelStandard} 鍏�/鏅氶珮浜�${std.label}鏍囧噯 ${std.hotelPerNight} 鍏�/鏅歚);
+ }
+ const apply = Number(f.applyAmount) || detailTotal;
+ const standardTotal = transportLimit + hotelLimit + mealLimit;
+ if (apply > standardTotal && standardTotal > 0) {
+ warnings.push(`鐢宠鎬婚 ${apply} 鍏冮珮浜庡樊鏃呮爣鍑嗗悎璁$害 ${standardTotal} 鍏僠);
+ }
+ return warnings;
+}
+
+export function useFinReimburseForm(moduleKeyRef, modeRef) {
+ const isTravel = computed(
+ () => moduleKeyRef.value === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE
+ );
+
+ const form = reactive(
+ moduleKeyRef.value === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+ ? createEmptyCostForm()
+ : createEmptyTravelForm()
+ );
+
+ const submitting = ref(false);
+ const loading = ref(false);
+ const allUsersCache = ref([]);
+ const showApplicantPicker = ref(false);
+ const applicantDisplaySub = computed(() => {
+ if (!form.applicantId) return "鐐瑰嚮閫夋嫨鐢宠浜�";
+ const u = userById(form.applicantId);
+ if (u) return userSubLabel(u) || form.employeeNo || "";
+ return form.employeeNo ? `宸ュ彿 ${form.employeeNo}` : "";
+ });
+ const applicantAvatarColor = computed(() =>
+ userAvatarColor(form.employeeName || form.employeeNo || "")
+ );
+ const showCategorySheet = ref(false);
+ const showSubjectSheet = ref(false);
+ const editingDetailIndex = ref(-1);
+ const pickApplicantId = ref("");
+ const pickCategoryValue = ref("");
+ const pickSubjectValue = ref("");
+
+ const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
+
+ const travelDaysDisplay = computed(() => {
+ if (!isTravel.value) return "";
+ const d = computeTravelDays(form.travelStartTime, form.travelEndTime);
+ return d == null ? "" : String(d);
+ });
+
+ const travelTierLabel = computed(() => {
+ if (!isTravel.value) return "";
+ const std = getTravelStandardByTier(form.travelTier || detectTravelTier(form.destination));
+ return `鎸�${std.label}鏍囧噯`;
+ });
+
+ const suggestedLivingSubsidy = computed(() => {
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || form.hotelDays || 0;
+ const std = getTravelStandardByTier(form.travelTier);
+ return Math.round(std.mealPerDay * days * 100) / 100;
+ });
+
+ const suggestedTransportSubsidy = computed(() => {
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || 0;
+ const std = getTravelStandardByTier(form.travelTier);
+ return Math.round(std.transportPerDay * days * 100) / 100;
+ });
+
+ const suggestedHotelLimit = computed(() => {
+ const nights = form.hotelDays || 0;
+ const perNight = form.hotelStandard ?? getTravelStandardByTier(form.travelTier).hotelPerNight;
+ return Math.round(perNight * nights * 100) / 100;
+ });
+
+ const detailTotalAmount = computed(() => {
+ const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
+ return Math.round(sum * 100) / 100;
+ });
+
+ const overBudgetWarnings = computed(() => {
+ if (!isTravel.value) return [];
+ return buildOverBudgetWarnings(
+ form,
+ detailTotalAmount.value,
+ suggestedHotelLimit.value,
+ suggestedTransportSubsidy.value,
+ suggestedLivingSubsidy.value
+ );
+ });
+
+ const expenseSubjectOptions = computed(() =>
+ isTravel.value ? EXPENSE_SUBJECT_OPTIONS : COST_SUBJECT_OPTIONS
+ );
+
+ const categoryActions = computed(() =>
+ EXPENSE_CATEGORY_OPTIONS.map(x => ({ name: x.label, value: x.value }))
+ );
+
+ const categoryLabel = computed(() => {
+ const hit = EXPENSE_CATEGORY_OPTIONS.find(x => x.value === form.expenseCategory);
+ return hit?.label || "璇烽�夋嫨璐圭敤绫诲瀷";
+ });
+
+ async function loadUserPool() {
+ try {
+ allUsersCache.value = unwrapUserList(await userListNoPageByTenantId());
+ } catch {
+ allUsersCache.value = [];
+ }
+ }
+
+ function userLabel(u) {
+ return userSelectLabel(u);
+ }
+
+ function userById(id) {
+ return allUsersCache.value.find(u => String(u.userId ?? u.id) === String(id));
+ }
+
+ function employeeNoFromUser(u) {
+ if (!u) return "";
+ return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
+ }
+
+ function fillApplicantFromUser(u) {
+ if (!u) return;
+ form.applicantId = u.userId ?? u.id;
+ form.employeeName = u.nickName || u.userName || "";
+ form.employeeNo = employeeNoFromUser(u);
+ form.payee = form.payee || form.employeeName;
+ form.deptId = String(u.deptId ?? u.sysDeptId ?? "");
+ form.deptName = u.dept?.deptName ?? u.deptName ?? "";
+ }
+
+ function onApplicantPicked(uidOrUser) {
+ const u =
+ typeof uidOrUser === "object" && uidOrUser
+ ? uidOrUser
+ : userById(uidOrUser);
+ fillApplicantFromUser(u);
+ }
+
+ /** 鏂板鏃堕粯璁ゅ甫鍑哄綋鍓嶇櫥褰曚汉锛屽噺灏戦�変汉姝ラ */
+ function tryApplyCurrentUser() {
+ if (modeRef.value === "edit" || form.applicantId) return;
+ const id = userStore.id;
+ if (!id) return;
+ let u = userById(id);
+ if (!u) {
+ u = {
+ userId: id,
+ nickName: userStore.nickName,
+ userName: userStore.name,
+ };
+ }
+ fillApplicantFromUser(u);
+ }
+
+ function recalcTravelStandards() {
+ if (!isTravel.value) return;
+ form.travelTier = detectTravelTier(form.destination);
+ const std = getTravelStandardByTier(form.travelTier);
+ if (form.hotelStandard == null || form.hotelStandard === "" || form.hotelStandard === 0) {
+ form.hotelStandard = std.hotelPerNight;
+ }
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime);
+ if (days != null) {
+ form.travelDays = days;
+ if (form.hotelDays == null || form.hotelDays === "") {
+ form.hotelDays = Math.max(0, days - 1);
+ }
+ if (form.livingSubsidy == null || form.livingSubsidy === "" || form.livingSubsidy === 0) {
+ form.livingSubsidy = suggestedLivingSubsidy.value;
+ }
+ }
+ form.needSpecialApproval = overBudgetWarnings.value.length > 0;
+ }
+
+ function syncApplyAmountFromDetails() {
+ form.applyAmount = detailTotalAmount.value;
+ recalcTravelStandards();
+ }
+
+ function addExpenseDetail() {
+ const row = isTravel.value ? createEmptyExpenseDetail() : createCostDetail();
+ form.expenseDetails.push(row);
+ }
+
+ function removeExpenseDetail(index) {
+ form.expenseDetails.splice(index, 1);
+ recalcTravelStandards();
+ }
+
+ function applyTemplate(category) {
+ applyCategoryTemplate(form, category);
+ syncApplyAmountFromDetails();
+ }
+
+ function resetFormForModule() {
+ const empty = isTravel.value ? createEmptyTravelForm() : createEmptyCostForm();
+ Object.keys(form).forEach(k => delete form[k]);
+ Object.assign(form, empty);
+ if (!form.approvalFlowNodes?.length) {
+ form.approvalFlowNodes = [
+ { approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" },
+ ];
+ }
+ }
+
+ async function loadEdit(reimbursementId) {
+ loading.value = true;
+ try {
+ if (!allUsersCache.value.length) await loadUserPool();
+ const row = await fetchFinReimbursementFormDetail(
+ { reimbursementId },
+ moduleKeyRef.value
+ );
+ if (row?.moduleKey && row.moduleKey !== moduleKeyRef.value) {
+ moduleKeyRef.value = row.moduleKey;
+ }
+ Object.assign(form, JSON.parse(JSON.stringify(row)), {
+ reimbursementId: row.reimbursementId ?? row.id,
+ expenseDetails: JSON.parse(JSON.stringify(row.expenseDetails || [])),
+ approvalFlowNodes: JSON.parse(
+ JSON.stringify(
+ row.approvalFlowNodes?.length
+ ? row.approvalFlowNodes
+ : [{ approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" }]
+ )
+ ),
+ attachmentList: JSON.parse(JSON.stringify(row.attachmentList || [])),
+ });
+ if (!isTravel.value && form.expenseCategory) {
+ /* 宸茬敱 mapCost 杞负 value */
+ }
+ recalcTravelStandards();
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ async function initForm() {
+ resetFormForModule();
+ if (!allUsersCache.value.length) await loadUserPool();
+ if (modeRef.value !== "edit") {
+ form.approvalFlowNodes = [
+ { approverId: "", approverName: "", nodeOrder: 1, signMode: "countersign" },
+ ];
+ tryApplyCurrentUser();
+ }
+ }
+
+ function validateForm() {
+ if (!form.applicantId) {
+ uni.showToast({ title: "璇烽�夋嫨鍛樺伐", icon: "none" });
+ return false;
+ }
+ if (!(form.reimburseReason || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欐姤閿�鍘熷洜", icon: "none" });
+ return false;
+ }
+ if (isTravel.value) {
+ if (!form.travelStartTime) {
+ uni.showToast({ title: "璇烽�夋嫨鍑哄樊寮�濮嬫椂闂�", icon: "none" });
+ return false;
+ }
+ if (!form.travelEndTime) {
+ uni.showToast({ title: "璇烽�夋嫨鍑哄樊缁撴潫鏃堕棿", icon: "none" });
+ return false;
+ }
+ if (computeTravelDays(form.travelStartTime, form.travelEndTime) == null) {
+ uni.showToast({ title: "缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�", icon: "none" });
+ return false;
+ }
+ if (!(form.departurePlace || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欏嚭宸湴", icon: "none" });
+ return false;
+ }
+ if (!(form.destination || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欑洰鐨勫湴", icon: "none" });
+ return false;
+ }
+ } else if (!form.expenseCategory) {
+ uni.showToast({ title: "璇烽�夋嫨璐圭敤绫诲瀷", icon: "none" });
+ return false;
+ }
+ if (form.applyAmount === "" || form.applyAmount == null) {
+ uni.showToast({ title: "璇峰~鍐欑敵璇烽噾棰�", icon: "none" });
+ return false;
+ }
+ if (!(form.payee || "").trim()) {
+ uni.showToast({ title: "璇峰~鍐欐敹娆句汉", icon: "none" });
+ return false;
+ }
+ if (!(form.expenseDetails || []).length) {
+ uni.showToast({ title: "璇疯嚦灏戞坊鍔犱竴鏉℃姤閿�鏄庣粏", icon: "none" });
+ return false;
+ }
+ const nodes = form.approvalFlowNodes || [];
+ if (!nodes.length || nodes.some(n => n.approverId == null || n.approverId === "")) {
+ uni.showToast({ title: "姣忎釜瀹℃壒鑺傜偣椤婚�夋嫨瀹℃壒浜�", icon: "none" });
+ return false;
+ }
+ return true;
+ }
+
+ async function submitForm() {
+ if (!validateForm()) return;
+ recalcTravelStandards();
+ if (isTravel.value && form.needSpecialApproval) {
+ const ok = await new Promise(resolve => {
+ uni.showModal({
+ title: "瓒呮敮鎻愰啋",
+ content: "瀛樺湪瓒呮敮椤癸紝鎻愪氦鍚庡皢鏍囪涓洪渶鐗规壒锛屾槸鍚︾户缁紵",
+ success: r => resolve(!!r.confirm),
+ });
+ });
+ if (!ok) return;
+ }
+ const isEdit = modeRef.value === "edit";
+ const dto = isTravel.value
+ ? buildTravelReimbursementSaveDto(form, { computeTravelDays })
+ : buildCostReimbursementSaveDto(form);
+ const check = validateReimbursementPersistDto(dto, isEdit);
+ if (!check.ok) {
+ uni.showToast({ title: check.message, icon: "none" });
+ return;
+ }
+ const nodeCheck = validateReimbursementApprovalNodes(dto);
+ if (!nodeCheck.ok) {
+ uni.showToast({ title: nodeCheck.message, icon: "none" });
+ return;
+ }
+ submitting.value = true;
+ try {
+ await persistFinReimbursement(dto, isEdit);
+ uni.showToast({ title: isEdit ? "淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛", icon: "success" });
+ return true;
+ } catch {
+ uni.showToast({ title: isEdit ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触", icon: "none" });
+ return false;
+ } finally {
+ submitting.value = false;
+ }
+ }
+
+ return {
+ form,
+ isTravel,
+ submitting,
+ loading,
+ flowUserOptions,
+ travelDaysDisplay,
+ travelTierLabel,
+ suggestedLivingSubsidy,
+ suggestedTransportSubsidy,
+ suggestedHotelLimit,
+ detailTotalAmount,
+ overBudgetWarnings,
+ expenseSubjectOptions,
+ categoryActions,
+ categoryLabel,
+ showApplicantPicker,
+ applicantDisplaySub,
+ applicantAvatarColor,
+ showCategorySheet,
+ showSubjectSheet,
+ editingDetailIndex,
+ pickCategoryValue,
+ pickSubjectValue,
+ loadUserPool,
+ userLabel,
+ onApplicantPicked,
+ tryApplyCurrentUser,
+ recalcTravelStandards,
+ syncApplyAmountFromDetails,
+ addExpenseDetail,
+ removeExpenseDetail,
+ applyTemplate,
+ initForm,
+ loadEdit,
+ submitForm,
+ getReimbursementTypeByModuleKey,
+ };
+}
diff --git a/src/pages/oa/ReimburseManage/travel-reimburse/index.vue b/src/pages/oa/ReimburseManage/travel-reimburse/index.vue
new file mode 100644
index 0000000..a892511
--- /dev/null
+++ b/src/pages/oa/ReimburseManage/travel-reimburse/index.vue
@@ -0,0 +1,11 @@
+<!--
+ OA / 鎶ラ攢绠$悊 / 宸梾鎶ラ攢锛�/finReimbursement/listPage锛宺eimbursementType=1锛�
+-->
+<template>
+ <FinReimbursementListPage :module-key="APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE" />
+</template>
+
+<script setup>
+ import FinReimbursementListPage from "../../_components/FinReimbursementListPage.vue";
+ import { APPROVAL_MODULE_KEYS } from "../../_utils/approvalModuleRegistry.js";
+</script>
diff --git a/src/pages/oa/_components/ApprovalInstanceListPage.vue b/src/pages/oa/_components/ApprovalInstanceListPage.vue
new file mode 100644
index 0000000..02b1f37
--- /dev/null
+++ b/src/pages/oa/_components/ApprovalInstanceListPage.vue
@@ -0,0 +1,353 @@
+<!--
+ 涓氬姟瀹℃壒鐢宠鍒楄〃锛堣浆姝�/璋冨矖/浜ゆ帴/璇峰亣/鍔犵彮锛�
+-->
+<template>
+ <view class="oa-approval-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <view class="oa-toolbar">
+ <view class="oa-filter-chip"
+ :class="{ active: hasActiveFilter }"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="18"
+ :color="hasActiveFilter ? '#2979ff' : '#666'" />
+ <text class="chip-label">绛涢��</text>
+ <text v-if="filterSummary"
+ class="chip-value">{{ filterSummary }}</text>
+ <text v-else
+ class="chip-placeholder">鍏ㄩ儴鏉′欢</text>
+ </view>
+ <view class="oa-icon-btn"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="20"
+ color="#666" />
+ </view>
+ </view>
+
+ <ApprovalModuleSearchPopup v-model:show="showFilter"
+ :module-key="moduleKey"
+ v-model="searchForm"
+ @search="handleSearch"
+ @reset="handleReset" />
+
+ <scroll-view class="oa-list-scroll"
+ scroll-y
+ :show-scrollbar="false"
+ :style="{ height: listScrollHeight + 'px' }"
+ @scrolltolower="loadMore">
+ <view v-if="displayList.length"
+ class="oa-card-list">
+ <view v-for="item in displayList"
+ :key="item.id"
+ class="oa-card"
+ @click="openDetail(item)">
+ <view class="oa-card-head">
+ <view class="oa-card-title-wrap">
+ <text class="oa-card-title">{{ cardTitle(item) }}</text>
+ <text v-if="item.instanceNo"
+ class="oa-card-sub">{{ item.instanceNo }}</text>
+ </view>
+ <text :class="['oa-status', businessStatusClass(item.status)]">
+ {{ businessStatusText(item.status) }}
+ </text>
+ </view>
+
+ <view class="oa-card-body">
+ <view class="oa-info-grid">
+ <view v-for="(row, idx) in visibleDisplayRows(item)"
+ :key="'f-' + idx"
+ class="oa-info-row">
+ <text class="oa-info-label">{{ row.label }}</text>
+ <text class="oa-info-value">{{ row.value || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠浜�</text>
+ <text class="oa-info-value">{{ item.applicantName || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠鏃堕棿</text>
+ <text class="oa-info-value">{{ item.createTime || "-" }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="canEditBusinessInstanceRow(item)"
+ class="oa-card-foot"
+ @click.stop>
+ <text class="oa-foot-btn btn-edit"
+ @click="goEdit(item)">淇敼</text>
+ <text class="oa-foot-btn btn-delete"
+ @click="confirmDelete(item)">鍒犻櫎</text>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+
+ <view v-else-if="!tableLoading"
+ class="oa-empty">
+ <up-empty mode="list"
+ :text="`鏆傛棤${pageTitle}鏁版嵁`" />
+ </view>
+ <view v-if="tableLoading && !list.length"
+ class="oa-loading">
+ <up-loading-icon mode="circle" />
+ </view>
+ </scroll-view>
+
+ <view class="fab-button"
+ @click="handleAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ApprovalModuleSearchPopup from "./ApprovalModuleSearchPopup.vue";
+ import {
+ deleteApprovalInstance,
+ listApprovalInstancePage,
+ } from "@/api/oa/approvalInstance.js";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import { fetchApprovalTemplateTypes } from "../_utils/approvalTemplateType.js";
+ import {
+ getApprovalModuleConfig,
+ getModuleListBusinessType,
+ } from "../_utils/approvalModuleRegistry.js";
+ import {
+ buildModuleListDto,
+ createModuleSearchForm,
+ filterRowsByModuleSearch,
+ filterRowsByModuleBusinessType,
+ formatDateRangeLabel,
+ getModuleSearchMeta,
+ hasActiveModuleSearch,
+ } from "../_utils/approvalModuleListSearch.js";
+ import {
+ buildInstanceListParams,
+ businessStatusClass,
+ businessStatusText,
+ canEditBusinessInstanceRow,
+ EDIT_STORAGE_KEY,
+ mapInstanceListRow,
+ stashInstanceRow,
+ unwrapInstancePage,
+ } from "../_utils/approveListUtils.js";
+
+ const props = defineProps({
+ moduleKey: { type: String, required: true },
+ });
+
+ const moduleConfig = computed(() => getApprovalModuleConfig(props.moduleKey));
+ const pageTitle = computed(() => moduleConfig.value?.label || "鐢宠");
+
+ const showFilter = ref(false);
+ const searchForm = reactive(createModuleSearchForm(props.moduleKey));
+ const list = ref([]);
+ const tableLoading = ref(false);
+ const pageStatus = ref("loadmore");
+ const businessType = ref("");
+ const typeOptions = ref([]);
+
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const listScrollHeight = ref(400);
+
+ function calcListScrollHeight() {
+ const sys = uni.getSystemInfoSync();
+ const statusBar = sys.statusBarHeight || 0;
+ const navBar = 44;
+ const toolbar = 56;
+ const fabGap = 16;
+ listScrollHeight.value = Math.max(
+ 200,
+ sys.windowHeight - statusBar - navBar - toolbar - fabGap
+ );
+ }
+
+ const displayList = computed(() => {
+ const byType = filterRowsByModuleBusinessType(
+ props.moduleKey,
+ list.value,
+ typeOptions.value
+ );
+ return filterRowsByModuleSearch(props.moduleKey, byType, searchForm);
+ });
+
+ const hasActiveFilter = computed(() => Boolean(filterSummary.value));
+
+ const filterSummary = computed(() => {
+ const parts = [];
+ const meta = getModuleSearchMeta(props.moduleKey);
+ for (const field of meta.fields || []) {
+ const val = searchForm[field.key];
+ if (field.type === "input" && (val || "").trim()) {
+ parts.push(`${field.label}:${String(val).trim()}`);
+ } else if (field.type === "daterange" && Array.isArray(val) && val[0]) {
+ parts.push(`${field.label}:${formatDateRangeLabel(val)}`);
+ } else if (field.type === "select" && val) {
+ const opt = (field.options || []).find(o => o.value === val);
+ parts.push(`${field.label}:${opt?.label || val}`);
+ } else if (field.type === "user" && val) {
+ parts.push(`${field.label}:宸查�塦);
+ }
+ }
+ return parts.join("锛�");
+ });
+
+ function cardTitle(item) {
+ return item.summary || item.title || pageTitle.value;
+ }
+
+ function visibleDisplayRows(item) {
+ const rows = item.displayRows || [];
+ return rows.slice(0, 2);
+ }
+
+ const buildListRequestParams = () => {
+ const extraDto = buildModuleListDto(props.moduleKey, searchForm);
+ return buildInstanceListParams({
+ page,
+ businessType: businessType.value,
+ extraDto,
+ searchForm,
+ });
+ };
+
+ const fetchList = async (reset = false) => {
+ if (reset) {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ }
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+ if (!businessType.value && businessType.value !== 0) return;
+
+ pageStatus.value = "loading";
+ tableLoading.value = true;
+
+ try {
+ const res = await listApprovalInstancePage(buildListRequestParams());
+ const { records, total } = unwrapInstancePage(res);
+ const listFields = moduleConfig.value?.listFields || [];
+ let mapped = records.map(row => mapInstanceListRow(row, listFields));
+ if (hasActiveModuleSearch(props.moduleKey, searchForm)) {
+ mapped = filterRowsByModuleSearch(props.moduleKey, mapped, searchForm);
+ }
+
+ if (page.current === 1) {
+ list.value = mapped;
+ } else {
+ list.value = [...list.value, ...mapped];
+ }
+ const dropped = records.length - mapped.length;
+ page.total = hasActiveModuleSearch(props.moduleKey, searchForm)
+ ? list.value.length
+ : dropped > 0
+ ? Math.max(0, Number(total) - dropped)
+ : Number(total);
+
+ if (list.value.length >= total || records.length < page.size) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current += 1;
+ }
+ } catch {
+ if (page.current === 1) list.value = [];
+ pageStatus.value = "loadmore";
+ uni.showToast({ title: `${pageTitle.value}鍔犺浇澶辫触`, icon: "none" });
+ } finally {
+ tableLoading.value = false;
+ }
+ };
+
+ const initBusinessType = async () => {
+ const fixed = getModuleListBusinessType(props.moduleKey);
+ businessType.value = fixed != null && fixed !== "" ? fixed : "";
+ try {
+ typeOptions.value = await fetchApprovalTemplateTypes();
+ } catch {
+ typeOptions.value = [];
+ }
+ };
+
+ const handleSearch = () => fetchList(true);
+ const handleReset = () => {
+ Object.assign(searchForm, createModuleSearchForm(props.moduleKey));
+ fetchList(true);
+ };
+ const loadMore = () => {
+ if (pageStatus.value === "loadmore") fetchList(false);
+ };
+ const goBack = () => uni.navigateBack();
+
+ const openDetail = item => {
+ if (!item?.id) return;
+ stashInstanceRow(item);
+ uni.navigateTo({
+ url: `${OA_NAV.approveListDetail}?id=${item.id}&from=business`,
+ });
+ };
+
+ const goEdit = item => {
+ if (!canEditBusinessInstanceRow(item)) {
+ uni.showToast({ title: "杩涜涓垨宸插畬鎴愮殑瀹℃壒涓嶅彲淇敼", icon: "none" });
+ return;
+ }
+ if (!item?.id) return;
+ uni.setStorageSync(EDIT_STORAGE_KEY, item);
+ stashInstanceRow(item);
+ uni.navigateTo({
+ url: `${OA_NAV.approveListApply}?id=${item.id}&moduleKey=${props.moduleKey}`,
+ });
+ };
+
+ const confirmDelete = item => {
+ if (!item?.id) return;
+ const title = item.title || item.templateName || item.instanceNo || "璇ュ鎵�";
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: `纭畾瑕佸垹闄ゃ��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ confirmText: "纭畾鍒犻櫎",
+ confirmColor: "#f56c6c",
+ success: async res => {
+ if (!res.confirm) return;
+ try {
+ await deleteApprovalInstance([item.id]);
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ fetchList(true);
+ } catch {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ }
+ },
+ });
+ };
+
+ const handleAdd = () => {
+ uni.navigateTo({
+ url: `${OA_NAV.approveListTemplateSelect}?moduleKey=${props.moduleKey}`,
+ });
+ };
+
+ onMounted(() => {
+ calcListScrollHeight();
+ });
+
+ onShow(async () => {
+ calcListScrollHeight();
+ await initBusinessType();
+ fetchList(true);
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+ @import "../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/_components/ApprovalModuleSearchPopup.vue b/src/pages/oa/_components/ApprovalModuleSearchPopup.vue
new file mode 100644
index 0000000..522bd02
--- /dev/null
+++ b/src/pages/oa/_components/ApprovalModuleSearchPopup.vue
@@ -0,0 +1,268 @@
+<!--
+ 涓氬姟瀹℃壒鍒楄〃绛涢�夊脊绐�
+-->
+<template>
+ <up-popup :show="modelShow"
+ mode="bottom"
+ round
+ @close="closePopup">
+ <view class="oa-filter-popup">
+ <view class="oa-filter-head">绛涢�夋潯浠�</view>
+ <scroll-view scroll-y
+ class="oa-filter-scroll">
+ <view v-for="field in fields"
+ :key="field.key"
+ class="oa-filter-field">
+ <text class="oa-field-label">{{ field.label }}</text>
+
+ <up-input v-if="field.type === 'input'"
+ v-model="localForm[field.key]"
+ :placeholder="field.placeholder || `璇疯緭鍏�${field.label}`"
+ clearable
+ border="surround" />
+
+ <view v-else-if="field.type === 'daterange'"
+ class="oa-picker-row"
+ @click="openDateRange(field.key)">
+ <text :class="{ placeholder: !dateLabel(field.key) }">
+ {{ dateLabel(field.key) || `璇烽�夋嫨${field.label}` }}
+ </text>
+ <up-icon name="calendar"
+ size="18"
+ color="#999" />
+ </view>
+
+ <view v-else-if="field.type === 'user'"
+ class="oa-picker-row"
+ @click="openUserPicker">
+ <text :class="{ placeholder: !userLabel }">
+ {{ userLabel || field.placeholder || '璇烽�夋嫨鐢宠浜�' }}
+ </text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999" />
+ </view>
+
+ <view v-else-if="field.type === 'select'"
+ class="oa-picker-row"
+ @click="openSelect(field)">
+ <text :class="{ placeholder: !selectLabel(field) }">
+ {{ selectLabel(field) || `璇烽�夋嫨${field.label}` }}
+ </text>
+ <up-icon name="arrow-down"
+ size="14"
+ color="#999" />
+ </view>
+ </view>
+ </scroll-view>
+
+ <view class="oa-filter-actions">
+ <up-button text="閲嶇疆"
+ shape="circle"
+ @click="handleReset" />
+ <up-button type="primary"
+ text="纭畾"
+ shape="circle"
+ @click="handleConfirm" />
+ </view>
+ </view>
+
+ <up-calendar :show="calendarShow"
+ mode="range"
+ :maxDate="maxDate"
+ minDate="2020-01-01"
+ :monthNum="24"
+ @confirm="onDateConfirm"
+ @close="calendarShow = false" />
+
+ <up-picker :show="pickerShow"
+ :columns="pickerColumns"
+ keyName="label"
+ @confirm="onPickerConfirm"
+ @cancel="pickerShow = false"
+ @close="pickerShow = false" />
+
+ <up-popup :show="userPickerShow"
+ mode="bottom"
+ round
+ @close="userPickerShow = false">
+ <view class="oa-user-popup">
+ <view class="oa-user-popup-title">閫夋嫨鐢宠浜�</view>
+ <up-input v-model="userKeyword"
+ placeholder="鎼滅储濮撳悕鎴栫紪鍙�"
+ clearable
+ border="surround" />
+ <scroll-view scroll-y
+ class="oa-user-list">
+ <view v-for="u in filteredUsers"
+ :key="u.userId ?? u.id"
+ class="oa-user-item"
+ @click="pickUser(u)">
+ <text>{{ userSelectLabel(u) }}</text>
+ </view>
+ <view v-if="!filteredUsers.length"
+ class="oa-user-empty">鏆傛棤鍖归厤鐢ㄦ埛</view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ </up-popup>
+</template>
+
+<script setup>
+ import { computed, reactive, ref, watch } from "vue";
+ import dayjs from "dayjs";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import {
+ formatDateRangeLabel,
+ getModuleSearchMeta,
+ resetModuleSearchForm,
+ userSelectLabel,
+ } from "../_utils/approvalModuleListSearch.js";
+
+ const props = defineProps({
+ show: { type: Boolean, default: false },
+ moduleKey: { type: String, required: true },
+ modelValue: { type: Object, default: () => ({}) },
+ });
+
+ const emit = defineEmits(["update:show", "update:modelValue", "search", "reset"]);
+
+ const modelShow = computed({
+ get: () => props.show,
+ set: v => emit("update:show", v),
+ });
+
+ function closePopup() {
+ emit("update:show", false);
+ }
+
+ const localForm = reactive({});
+ const calendarShow = ref(false);
+ const activeDateKey = ref("applyDateRange");
+ const maxDate = dayjs().format("YYYY-MM-DD");
+ const pickerShow = ref(false);
+ const pickerColumns = ref([[]]);
+ const activeSelectField = ref(null);
+ const userPickerShow = ref(false);
+ const userKeyword = ref("");
+ const allUsers = ref([]);
+
+ const fields = computed(() => getModuleSearchMeta(props.moduleKey).fields || []);
+
+ const userLabel = computed(() => {
+ const id = localForm.applicantId;
+ if (!id) return "";
+ const u = allUsers.value.find(x => String(x.userId ?? x.id) === String(id));
+ return u ? userSelectLabel(u) : String(id);
+ });
+
+ const filteredUsers = computed(() => {
+ const q = userKeyword.value.trim().toLowerCase();
+ const list = allUsers.value.filter(u => {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status != null && String(u.status) !== "0") return false;
+ return true;
+ });
+ if (!q) return list.slice(0, 50);
+ return list
+ .filter(u => {
+ const nick = (u.nickName || "").toLowerCase();
+ const name = (u.userName || "").toLowerCase();
+ const id = String(u.userId ?? u.id ?? "");
+ return nick.includes(q) || name.includes(q) || id.includes(q);
+ })
+ .slice(0, 50);
+ });
+
+ function unwrapUsers(res) {
+ if (Array.isArray(res)) return res;
+ if (Array.isArray(res?.data)) return res.data;
+ if (Array.isArray(res?.rows)) return res.rows;
+ return [];
+ }
+
+ async function loadUsers() {
+ if (allUsers.value.length) return;
+ try {
+ const res = await userListNoPageByTenantId();
+ allUsers.value = unwrapUsers(res);
+ } catch {
+ allUsers.value = [];
+ }
+ }
+
+ watch(
+ () => props.show,
+ v => {
+ if (!v) return;
+ Object.assign(localForm, props.modelValue || {});
+ }
+ );
+
+ function dateLabel(key) {
+ return formatDateRangeLabel(localForm[key]);
+ }
+
+ function openDateRange(key) {
+ activeDateKey.value = key;
+ calendarShow.value = true;
+ }
+
+ function onDateConfirm(e) {
+ if (!e?.length) {
+ calendarShow.value = false;
+ return;
+ }
+ localForm[activeDateKey.value] = [e[0], e[e.length - 1]];
+ calendarShow.value = false;
+ }
+
+ function selectLabel(field) {
+ const val = localForm[field.key];
+ if (!val) return "";
+ const opt = (field.options || []).find(o => o.value === val);
+ return opt?.label || String(val);
+ }
+
+ function openSelect(field) {
+ activeSelectField.value = field;
+ pickerColumns.value = [field.options || []];
+ pickerShow.value = true;
+ }
+
+ function onPickerConfirm(e) {
+ const item = e?.value?.[0];
+ if (activeSelectField.value?.key && item) {
+ localForm[activeSelectField.value.key] = item.value;
+ }
+ pickerShow.value = false;
+ }
+
+ async function openUserPicker() {
+ await loadUsers();
+ userKeyword.value = "";
+ userPickerShow.value = true;
+ }
+
+ function pickUser(u) {
+ localForm.applicantId = u.userId ?? u.id ?? "";
+ userPickerShow.value = false;
+ }
+
+ function handleReset() {
+ resetModuleSearchForm(props.moduleKey, localForm);
+ emit("update:modelValue", { ...localForm });
+ emit("reset", { ...localForm });
+ emit("update:show", false);
+ }
+
+ function handleConfirm() {
+ emit("update:modelValue", { ...localForm });
+ emit("search", { ...localForm });
+ emit("update:show", false);
+ }
+</script>
+
+<style scoped lang="scss">
+ @import "../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/_components/FinReimbursementListPage.vue b/src/pages/oa/_components/FinReimbursementListPage.vue
new file mode 100644
index 0000000..ab6ad06
--- /dev/null
+++ b/src/pages/oa/_components/FinReimbursementListPage.vue
@@ -0,0 +1,362 @@
+<!--
+ 宸梾/璐圭敤鎶ラ攢鍒楄〃锛�/finReimbursement/listPage锛�
+-->
+<template>
+ <view class="oa-approval-page">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <view class="oa-toolbar">
+ <view class="oa-filter-chip"
+ :class="{ active: hasActiveFilter }"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="18"
+ :color="hasActiveFilter ? '#2979ff' : '#666'" />
+ <text class="chip-label">绛涢��</text>
+ <text v-if="filterSummary"
+ class="chip-value">{{ filterSummary }}</text>
+ <text v-else
+ class="chip-placeholder">鍏ㄩ儴鏉′欢</text>
+ </view>
+ <view class="oa-icon-btn"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="20"
+ color="#666" />
+ </view>
+ </view>
+
+ <ApprovalModuleSearchPopup v-model:show="showFilter"
+ :module-key="moduleKey"
+ v-model="searchForm"
+ @search="handleSearch"
+ @reset="handleReset" />
+
+ <scroll-view class="oa-list-scroll"
+ scroll-y
+ :show-scrollbar="false"
+ :style="{ height: listScrollHeight + 'px' }"
+ @scrolltolower="loadMore">
+ <view v-if="displayList.length"
+ class="oa-card-list">
+ <view v-for="item in displayList"
+ :key="item.reimbursementId || item.id"
+ class="oa-card">
+ <view class="oa-card-head">
+ <view class="oa-card-title-wrap">
+ <text class="oa-card-title">{{ cardTitle(item) }}</text>
+ <text v-if="item.billNo"
+ class="oa-card-sub">{{ item.billNo }}</text>
+ </view>
+ <text :class="['oa-status', billStatusCssClass(item)]">
+ {{ billStatusLabel(item.billStatus ?? item.status) }}
+ </text>
+ </view>
+
+ <view class="oa-card-body">
+ <view class="oa-info-grid">
+ <view v-for="(row, idx) in visibleDisplayRows(item)"
+ :key="'f-' + idx"
+ class="oa-info-row">
+ <text class="oa-info-label">{{ row.label }}</text>
+ <text class="oa-info-value">{{ row.value || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠浜�</text>
+ <text class="oa-info-value">{{ item.applicantName || "-" }}</text>
+ </view>
+ <view class="oa-info-row">
+ <text class="oa-info-label">鐢宠鏃堕棿</text>
+ <text class="oa-info-value">{{ formatListTime(item.createTime) }}</text>
+ </view>
+ </view>
+ </view>
+
+ <view class="oa-card-foot"
+ @click.stop>
+ <text class="oa-foot-btn btn-detail"
+ @click="openDetail(item)">璇︽儏</text>
+ <text v-if="canEditReimbursementRow(item)"
+ class="oa-foot-btn btn-edit"
+ @click="goEdit(item)">淇敼</text>
+ <text v-if="canDeleteReimbursementRow(item)"
+ class="oa-foot-btn btn-delete"
+ @click="confirmDelete(item)">鍒犻櫎</text>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+
+ <view v-else-if="!tableLoading"
+ class="oa-empty">
+ <up-empty mode="list"
+ :text="`鏆傛棤${pageTitle}鏁版嵁`" />
+ </view>
+ <view v-if="tableLoading && !list.length"
+ class="oa-loading">
+ <up-loading-icon mode="circle" />
+ </view>
+ </scroll-view>
+
+ <view class="fab-button"
+ @click="handleAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import ApprovalModuleSearchPopup from "./ApprovalModuleSearchPopup.vue";
+ import { listFinReimbursementPage } from "@/api/oa/finReimbursement.js";
+ import { OA_NAV } from "@/config/oaPaths.js";
+ import { getApprovalModuleConfig } from "../_utils/approvalModuleRegistry.js";
+ import {
+ createModuleSearchForm,
+ formatDateRangeLabel,
+ getModuleSearchMeta,
+ } from "../_utils/approvalModuleListSearch.js";
+ import { parseTime } from "@/utils/ruoyi";
+ import {
+ billStatusCssClass,
+ billStatusLabel,
+ buildFinReimbursementListParams,
+ canDeleteReimbursementRow,
+ canEditReimbursementRow,
+ deleteFinReimbursement,
+ getReimbursementTypeByModuleKey,
+ enrichReimbursementListRowsWithApprovalFlow,
+ filterReimbursementRowsBySearch,
+ filterRowsByReimbursementType,
+ hasActiveReimbursementSearch,
+ mapFinReimbursementFromApi,
+ resolveReimbursementDeleteId,
+ unwrapFinReimbursementPage,
+ } from "../_utils/finReimbursementMappers.js";
+
+ const props = defineProps({
+ moduleKey: { type: String, required: true },
+ });
+
+ const moduleConfig = computed(() => getApprovalModuleConfig(props.moduleKey));
+ const pageTitle = computed(() => moduleConfig.value?.label || "鎶ラ攢");
+ const reimbursementType = computed(() =>
+ getReimbursementTypeByModuleKey(props.moduleKey)
+ );
+
+ const showFilter = ref(false);
+ const searchForm = reactive(createModuleSearchForm(props.moduleKey));
+ const list = ref([]);
+ const tableLoading = ref(false);
+ const pageStatus = ref("loadmore");
+
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const listScrollHeight = ref(400);
+
+ function calcListScrollHeight() {
+ const sys = uni.getSystemInfoSync();
+ const statusBar = sys.statusBarHeight || 0;
+ const navBar = 44;
+ const toolbar = 56;
+ const fabGap = 16;
+ listScrollHeight.value = Math.max(
+ 200,
+ sys.windowHeight - statusBar - navBar - toolbar - fabGap
+ );
+ }
+
+ const displayList = computed(() =>
+ filterReimbursementRowsBySearch(list.value, searchForm)
+ );
+
+ const hasActiveFilter = computed(() => Boolean(filterSummary.value));
+
+ const filterSummary = computed(() => {
+ const parts = [];
+ const meta = getModuleSearchMeta(props.moduleKey);
+ for (const field of meta.fields || []) {
+ const val = searchForm[field.key];
+ if (field.type === "input" && (val || "").trim()) {
+ parts.push(`${field.label}:${String(val).trim()}`);
+ } else if (field.type === "daterange" && Array.isArray(val) && val[0]) {
+ parts.push(`${field.label}:${formatDateRangeLabel(val)}`);
+ } else if (field.type === "select" && val) {
+ const opt = (field.options || []).find(o => o.value === val);
+ parts.push(`${field.label}:${opt?.label || val}`);
+ }
+ }
+ return parts.join("锛�");
+ });
+
+ function cardTitle(item) {
+ return item.summary || item.title || item.reason || pageTitle.value;
+ }
+
+ function visibleDisplayRows(item) {
+ return (item.displayRows || []).slice(0, 3);
+ }
+
+ function formatListTime(t) {
+ if (!t) return "-";
+ const formatted = parseTime(t, "{y}-{m}-{d} {h}:{i}");
+ return formatted || String(t).replace("T", " ").slice(0, 16);
+ }
+
+ const fetchList = async (reset = false) => {
+ if (!reimbursementType.value) return;
+
+ if (reset) {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ }
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+ pageStatus.value = "loading";
+ tableLoading.value = true;
+
+ try {
+ const res = await listFinReimbursementPage(
+ buildFinReimbursementListParams({
+ page,
+ searchForm,
+ reimbursementType: reimbursementType.value,
+ })
+ );
+ const { records, total } = unwrapFinReimbursementPage(res);
+ const filtered = filterRowsByReimbursementType(
+ records,
+ reimbursementType.value
+ );
+ let mapped = filtered.map(row =>
+ mapFinReimbursementFromApi(row, {
+ reimbursementType: reimbursementType.value,
+ moduleKey: props.moduleKey,
+ })
+ );
+ mapped = await enrichReimbursementListRowsWithApprovalFlow(
+ mapped,
+ reimbursementType.value
+ );
+ if (hasActiveReimbursementSearch(searchForm)) {
+ mapped = filterReimbursementRowsBySearch(mapped, searchForm);
+ }
+
+ if (page.current === 1) {
+ list.value = mapped;
+ } else {
+ list.value = [...list.value, ...mapped];
+ }
+ const dropped = records.length - filtered.length;
+ let nextTotal =
+ dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total);
+ if (hasActiveReimbursementSearch(searchForm)) {
+ nextTotal = list.value.length;
+ }
+ page.total = nextTotal;
+
+ if (list.value.length >= total || records.length < page.size) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current += 1;
+ }
+ } catch {
+ if (page.current === 1) list.value = [];
+ pageStatus.value = "loadmore";
+ uni.showToast({ title: `${pageTitle.value}鍔犺浇澶辫触`, icon: "none" });
+ } finally {
+ tableLoading.value = false;
+ }
+ };
+
+ const handleSearch = () => fetchList(true);
+ const handleReset = () => {
+ Object.assign(searchForm, createModuleSearchForm(props.moduleKey));
+ fetchList(true);
+ };
+ const loadMore = () => {
+ if (pageStatus.value === "loadmore") fetchList(false);
+ };
+ const goBack = () => uni.navigateBack();
+
+ const openDetail = item => {
+ const rid = resolveReimbursementDeleteId(item);
+ if (rid == null) {
+ uni.showToast({ title: "鏃犳硶鏌ョ湅璇︽儏锛氱己灏戞姤閿�鍗� ID", icon: "none" });
+ return;
+ }
+ uni.navigateTo({
+ url: `${OA_NAV.reimburseDetail}?moduleKey=${props.moduleKey}&reimbursementId=${rid}`,
+ });
+ };
+
+ const handleAdd = () => {
+ uni.navigateTo({
+ url: `${OA_NAV.reimburseForm}?moduleKey=${props.moduleKey}&mode=add`,
+ });
+ };
+
+ const goEdit = item => {
+ if (!canEditReimbursementRow(item)) {
+ uni.showToast({ title: "瀹℃壒涓垨宸插畬鎴愮殑鎶ラ攢涓嶅彲淇敼", icon: "none" });
+ return;
+ }
+ const rid = resolveReimbursementDeleteId(item);
+ if (rid == null) {
+ uni.showToast({ title: "鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID", icon: "none" });
+ return;
+ }
+ uni.navigateTo({
+ url: `${OA_NAV.reimburseForm}?moduleKey=${props.moduleKey}&mode=edit&reimbursementId=${rid}`,
+ });
+ };
+
+ const confirmDelete = item => {
+ if (!canDeleteReimbursementRow(item)) {
+ uni.showToast({ title: "璇ョ姸鎬佷笉鍙垹闄�", icon: "none" });
+ return;
+ }
+ const id = resolveReimbursementDeleteId(item);
+ if (id == null) {
+ uni.showToast({ title: "鏃犳硶鍒犻櫎锛氱己灏戞姤閿�鍗� ID", icon: "none" });
+ return;
+ }
+ const title = item.billNo || item.summary || item.title || "璇ユ姤閿�鍗�";
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: `纭畾瑕佸垹闄ゃ��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ confirmText: "纭畾鍒犻櫎",
+ confirmColor: "#f56c6c",
+ success: async res => {
+ if (!res.confirm) return;
+ try {
+ await deleteFinReimbursement([id]);
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ fetchList(true);
+ } catch {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ }
+ },
+ });
+ };
+
+ onMounted(() => {
+ calcListScrollHeight();
+ });
+
+ onShow(() => {
+ calcListScrollHeight();
+ fetchList(true);
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+ @import "../_styles/oa-approval-list.scss";
+</style>
diff --git a/src/pages/oa/_components/OaListPage.vue b/src/pages/oa/_components/OaListPage.vue
new file mode 100644
index 0000000..47614fc
--- /dev/null
+++ b/src/pages/oa/_components/OaListPage.vue
@@ -0,0 +1,182 @@
+<template>
+ <view class="oa-page sales-account">
+ <PageHeader :title="pageConfig.title"
+ @back="goBack" />
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input v-model="keyword"
+ class="search-text"
+ :placeholder="`鎼滅储${pageConfig.title}`"
+ clearable
+ @change="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="24"
+ color="#999" />
+ </view>
+ </view>
+ </view>
+
+ <scroll-view class="list-scroll"
+ scroll-y
+ :show-scrollbar="false">
+ <view v-if="displayList.length"
+ class="ledger-list">
+ <view v-for="item in displayList"
+ :key="item.id"
+ class="ledger-item"
+ @click="openDetail(item)">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff" />
+ </view>
+ <text class="item-id">{{ item.summary || item.applicantName || pageConfig.title }}</text>
+ </view>
+ <u-tag :type="getStatusMeta(item.status).type"
+ :text="getStatusMeta(item.status).text" />
+ </view>
+ <up-divider />
+ <view class="item-details">
+ <view v-for="field in pageConfig.fields"
+ :key="field.prop"
+ class="detail-row">
+ <text class="detail-label">{{ field.label }}</text>
+ <text class="detail-value">{{ item[field.prop] || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐢宠浜�</text>
+ <text class="detail-value">{{ item.applicantName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐢宠鏃堕棿</text>
+ <text class="detail-value">{{ item.createTime || "-" }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="empty-wrap">
+ <up-empty mode="list"
+ :text="`鏆傛棤${pageConfig.title}鏁版嵁`" />
+ </view>
+ </scroll-view>
+
+ <view class="footer-add">
+ <up-button type="primary"
+ text="鏂板"
+ @click="handleAdd" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { ensureList, saveList } from "../_utils/oaStorage.js";
+ import { getStatusMeta } from "../_utils/oaPageRegistry.js";
+ import { showToast } from "../_utils/oaUi.js";
+
+ const props = defineProps({
+ pageKey: {
+ type: String,
+ required: true,
+ },
+ pageConfig: {
+ type: Object,
+ required: true,
+ },
+ });
+
+ const keyword = ref("");
+ const list = ref([]);
+
+ const displayList = computed(() => {
+ const kw = keyword.value.trim();
+ if (!kw) return list.value;
+ return list.value.filter(item => {
+ const text = [
+ item.summary,
+ item.applicantName,
+ item.deptName,
+ ...props.pageConfig.fields.map(f => item[f.prop]),
+ ]
+ .filter(Boolean)
+ .join(" ");
+ return text.includes(kw);
+ });
+ });
+
+ const loadData = () => {
+ list.value = ensureList(
+ props.pageConfig.storageKey,
+ props.pageConfig.mockRows || []
+ );
+ };
+
+ const handleSearch = () => {
+ /* 鍏抽敭瀛楃敱 computed 杩囨护 */
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const openDetail = item => {
+ showToast(`鏌ョ湅锛�${item.summary || props.pageConfig.title}`);
+ };
+
+ const handleAdd = () => {
+ const row = {
+ ...(props.pageConfig.mockRows?.[0] || {}),
+ id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
+ applicantName: "褰撳墠鐢ㄦ埛",
+ status: "pending",
+ createTime: new Date().toISOString().slice(0, 19).replace("T", " "),
+ summary: `鏂板缓${props.pageConfig.title}`,
+ };
+ list.value = [row, ...list.value];
+ saveList(props.pageConfig.storageKey, list.value);
+ showToast("宸叉柊澧烇紙鏈湴绀轰緥锛�", "success");
+ };
+
+ onShow(() => {
+ loadData();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .oa-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ }
+
+ .list-scroll {
+ flex: 1;
+ height: 0;
+ padding-bottom: 80px;
+ }
+
+ .empty-wrap {
+ padding: 48px 20px;
+ }
+
+ .footer-add {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding: 12px 20px calc(12px + env(safe-area-inset-bottom));
+ background: #fff;
+ box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.06);
+ }
+</style>
diff --git a/src/pages/oa/_components/OaUserSearchPicker.vue b/src/pages/oa/_components/OaUserSearchPicker.vue
new file mode 100644
index 0000000..90a704b
--- /dev/null
+++ b/src/pages/oa/_components/OaUserSearchPicker.vue
@@ -0,0 +1,261 @@
+<!--
+ OA 閫氱敤锛氬彲鎼滅储鐨勭敤鎴峰崟閫夊脊灞傦紙鐐归�夊嵆纭锛�
+-->
+<template>
+ <up-popup :show="show"
+ mode="bottom"
+ round="16"
+ :safe-area-inset-bottom="true"
+ @close="emit('update:show', false)">
+ <view class="oa-user-sheet">
+ <view class="sheet-handle" />
+ <view class="sheet-head">
+ <text class="sheet-cancel"
+ @click="emit('update:show', false)">鍙栨秷</text>
+ <text class="sheet-title">{{ title }}</text>
+ <text class="sheet-spacer" />
+ </view>
+
+ <view class="sheet-search">
+ <up-search v-model="keyword"
+ placeholder="鎼滅储濮撳悕鎴栧伐鍙�"
+ :show-action="false"
+ shape="round"
+ bg-color="#f5f7fa" />
+ </view>
+
+ <view v-if="selfUser && showSelfQuick"
+ class="self-quick"
+ @click="pickUser(selfUser)">
+ <view class="user-avatar"
+ :style="{ backgroundColor: avatarColor(selfUser.nickName || selfUser.userName) }">
+ {{ (selfUser.nickName || selfUser.userName || "鎴�").charAt(0) }}
+ </view>
+ <view class="user-meta">
+ <text class="user-name">閫夋湰浜� 路 {{ userSelectLabel(selfUser) }}</text>
+ <text class="user-sub">{{ userSubLabel(selfUser) }}</text>
+ </view>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc" />
+ </view>
+
+ <scroll-view scroll-y
+ class="user-scroll"
+ :show-scrollbar="false">
+ <view v-for="u in filteredList"
+ :key="String(u.userId ?? u.id)"
+ class="user-item"
+ :class="{ selected: isSelected(u) }"
+ @click="pickUser(u)">
+ <view class="user-avatar"
+ :style="{ backgroundColor: avatarColor(u.nickName || u.userName) }">
+ {{ (u.nickName || u.userName || "?").charAt(0) }}
+ </view>
+ <view class="user-meta">
+ <text class="user-name">{{ userSelectLabel(u) }}</text>
+ <text class="user-sub">{{ userSubLabel(u) }}</text>
+ </view>
+ <view class="user-check"
+ :class="{ checked: isSelected(u) }">
+ <up-icon v-if="isSelected(u)"
+ name="checkmark"
+ size="14"
+ color="#fff" />
+ </view>
+ </view>
+ <view v-if="!filteredList.length"
+ class="user-empty">
+ <up-empty mode="search"
+ text="鏆傛棤鍖归厤鐢ㄦ埛" />
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from "vue";
+ import useUserStore from "@/store/modules/user";
+ import {
+ filterActiveUsers,
+ userAvatarColor,
+ userSelectLabel,
+ userSubLabel,
+ } from "../_utils/userPickerUtils.js";
+
+ const props = defineProps({
+ show: { type: Boolean, default: false },
+ title: { type: String, default: "閫夋嫨鍛樺伐" },
+ users: { type: Array, default: () => [] },
+ modelValue: { type: [String, Number], default: "" },
+ showSelfQuick: { type: Boolean, default: true },
+ });
+
+ const emit = defineEmits(["update:show", "update:modelValue", "select"]);
+
+ const keyword = ref("");
+ const userStore = useUserStore();
+
+ const filteredList = computed(() =>
+ filterActiveUsers(props.users, keyword.value, 100)
+ );
+
+ const selfUser = computed(() => {
+ const id = userStore.id;
+ if (!id) return null;
+ const hit = props.users.find(u => String(u.userId ?? u.id) === String(id));
+ if (hit) return hit;
+ return {
+ userId: id,
+ nickName: userStore.nickName,
+ userName: userStore.name,
+ };
+ });
+
+ watch(
+ () => props.show,
+ v => {
+ if (v) keyword.value = "";
+ }
+ );
+
+ function avatarColor(name) {
+ return userAvatarColor(name);
+ }
+
+ function isSelected(u) {
+ const id = u.userId ?? u.id;
+ return id != null && String(id) === String(props.modelValue ?? "");
+ }
+
+ function pickUser(u) {
+ const id = u.userId ?? u.id;
+ emit("update:modelValue", id);
+ emit("select", u);
+ emit("update:show", false);
+ }
+</script>
+
+<style scoped lang="scss">
+ .oa-user-sheet {
+ background: #fff;
+ border-radius: 16px 16px 0 0;
+ max-height: 78vh;
+ display: flex;
+ flex-direction: column;
+ }
+ .sheet-handle {
+ width: 36px;
+ height: 4px;
+ background: #e4e7ed;
+ border-radius: 2px;
+ margin: 8px auto 4px;
+ }
+ .sheet-head {
+ display: flex;
+ align-items: center;
+ padding: 8px 16px 12px;
+ }
+ .sheet-cancel {
+ font-size: 15px;
+ color: #909399;
+ min-width: 48px;
+ }
+ .sheet-title {
+ flex: 1;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+ .sheet-spacer {
+ min-width: 48px;
+ }
+ .sheet-search {
+ padding: 0 16px 10px;
+ }
+ .self-quick {
+ display: flex;
+ align-items: center;
+ margin: 0 16px 8px;
+ padding: 12px;
+ background: linear-gradient(135deg, #ecf5ff 0%, #f0f9ff 100%);
+ border-radius: 12px;
+ border: 1px solid #d9ecff;
+ }
+ .user-scroll {
+ flex: 1;
+ max-height: 52vh;
+ padding: 0 8px 16px;
+ box-sizing: border-box;
+ }
+ .user-item,
+ .self-quick {
+ &:active {
+ opacity: 0.85;
+ }
+ }
+ .user-item {
+ display: flex;
+ align-items: center;
+ padding: 12px 10px;
+ border-radius: 10px;
+ margin-bottom: 4px;
+ &.selected {
+ background: #f0f7ff;
+ }
+ }
+ .user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ color: #fff;
+ font-size: 16px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+ .user-meta {
+ flex: 1;
+ margin-left: 12px;
+ min-width: 0;
+ }
+ .user-name {
+ display: block;
+ font-size: 15px;
+ color: #303133;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .user-sub {
+ display: block;
+ font-size: 12px;
+ color: #909399;
+ margin-top: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .user-check {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: 2px solid #dcdfe6;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ &.checked {
+ background: #2979ff;
+ border-color: #2979ff;
+ }
+ }
+ .user-empty {
+ padding: 24px 0;
+ }
+</style>
diff --git a/src/pages/oa/_styles/oa-approval-list.scss b/src/pages/oa/_styles/oa-approval-list.scss
new file mode 100644
index 0000000..81a9ad1
--- /dev/null
+++ b/src/pages/oa/_styles/oa-approval-list.scss
@@ -0,0 +1,414 @@
+// OA 瀹℃壒鍒楄〃锛堜笟鍔$敵璇� + 瀹℃壒鍒楄〃锛夌粺涓�鏍峰紡
+
+.oa-approval-page {
+ min-height: 100vh;
+ background: #f2f4f7;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+.oa-list-scroll {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.oa-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px;
+ background: #fff;
+ border-bottom: 1px solid #eef0f3;
+}
+
+.oa-filter-chip {
+ flex: 1;
+ min-height: 40px;
+ padding: 0 14px;
+ background: #f5f7fa;
+ border-radius: 20px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ border: 1px solid transparent;
+
+ &.active {
+ background: #ecf3ff;
+ border-color: #b3d4ff;
+ }
+
+ .chip-label {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ flex-shrink: 0;
+ }
+
+ .chip-value {
+ flex: 1;
+ font-size: 13px;
+ color: #666;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .chip-placeholder {
+ flex: 1;
+ font-size: 13px;
+ color: #aaa;
+ }
+}
+
+.oa-icon-btn {
+ width: 40px;
+ height: 40px;
+ border-radius: 20px;
+ background: #f5f7fa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ &:active {
+ background: #e8ebf0;
+ }
+}
+
+.oa-card-list {
+ padding: 12px 16px 4px;
+}
+
+.oa-card {
+ background: #fff;
+ border-radius: 12px;
+ margin-bottom: 12px;
+ overflow: hidden;
+ box-shadow: 0 1px 4px rgba(15, 23, 42, 0.06);
+
+ &:active {
+ opacity: 0.92;
+ }
+}
+
+.oa-card-head {
+ padding: 14px 14px 10px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.oa-card-title-wrap {
+ flex: 1;
+ min-width: 0;
+}
+
+.oa-card-title {
+ display: block;
+ font-size: 15px;
+ font-weight: 600;
+ color: #1a1a1a;
+ line-height: 1.4;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.oa-card-sub {
+ display: block;
+ margin-top: 4px;
+ font-size: 12px;
+ color: #8c8c8c;
+}
+
+.oa-status {
+ flex-shrink: 0;
+ font-size: 11px;
+ line-height: 1;
+ padding: 5px 8px;
+ border-radius: 4px;
+ font-weight: 500;
+
+ &.status-pending {
+ color: #d46b08;
+ background: #fff7e6;
+ }
+
+ &.status-approved {
+ color: #389e0d;
+ background: #f6ffed;
+ }
+
+ &.status-rejected {
+ color: #cf1322;
+ background: #fff1f0;
+ }
+
+ &.status-draft {
+ color: #595959;
+ background: #f5f5f5;
+ }
+
+ &.status-cancelled {
+ color: #8c8c8c;
+ background: #fafafa;
+ }
+}
+
+.oa-card-body {
+ padding: 0 14px 12px;
+}
+
+.oa-info-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.oa-info-row {
+ display: flex;
+ align-items: baseline;
+ font-size: 13px;
+ line-height: 1.45;
+}
+
+.oa-info-label {
+ width: 72px;
+ flex-shrink: 0;
+ color: #8c8c8c;
+}
+
+.oa-info-value {
+ flex: 1;
+ color: #333;
+ word-break: break-all;
+}
+
+.oa-card-foot {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 10px;
+ padding: 10px 14px;
+ background: #fafbfc;
+ border-top: 1px solid #f0f2f5;
+}
+
+.oa-foot-btn {
+ min-width: 64px;
+ height: 32px;
+ line-height: 32px;
+ padding: 0 14px;
+ font-size: 13px;
+ border-radius: 16px;
+ text-align: center;
+
+ &.btn-detail {
+ color: #fff;
+ background: #2979ff;
+ }
+
+ &.btn-edit {
+ color: #2979ff;
+ background: #ecf3ff;
+ }
+
+ &.btn-delete {
+ color: #ff4d4f;
+ background: #fff1f0;
+ }
+
+ &.btn-approve {
+ color: #fff;
+ background: #2979ff;
+ }
+}
+
+.oa-empty,
+.oa-loading {
+ padding: 48px 20px;
+}
+
+.oa-loading {
+ display: flex;
+ justify-content: center;
+}
+
+// 绛涢�夊脊绐�
+.oa-filter-popup {
+ padding: 16px 16px calc(16px + env(safe-area-inset-bottom));
+ max-height: 72vh;
+ display: flex;
+ flex-direction: column;
+ background: #fff;
+}
+
+.oa-filter-head {
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #1a1a1a;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f2f5;
+ margin-bottom: 12px;
+}
+
+.oa-filter-scroll {
+ max-height: 50vh;
+}
+
+.oa-filter-field {
+ margin-bottom: 16px;
+}
+
+.oa-field-label {
+ display: block;
+ font-size: 13px;
+ color: #666;
+ margin-bottom: 8px;
+}
+
+.oa-picker-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 44px;
+ padding: 0 14px;
+ background: #f5f7fa;
+ border-radius: 8px;
+ font-size: 14px;
+ color: #333;
+
+ .placeholder {
+ color: #bbb;
+ }
+}
+
+.oa-filter-actions {
+ display: flex;
+ gap: 12px;
+ margin-top: 16px;
+ padding-top: 12px;
+}
+
+.oa-filter-actions :deep(.u-button) {
+ flex: 1;
+ border-radius: 22px !important;
+}
+
+.oa-user-popup {
+ padding: 16px;
+ max-height: 60vh;
+ background: #fff;
+}
+
+.oa-user-popup-title {
+ text-align: center;
+ font-weight: 600;
+ font-size: 16px;
+ margin-bottom: 12px;
+}
+
+.oa-user-list {
+ max-height: 40vh;
+ margin-top: 10px;
+}
+
+.oa-user-item {
+ padding: 14px 4px;
+ font-size: 14px;
+ color: #333;
+ border-bottom: 1px solid #f0f2f5;
+}
+
+.oa-user-empty {
+ text-align: center;
+ color: #999;
+ padding: 24px;
+ font-size: 13px;
+}
+
+// 璇︽儏 / 瀹℃壒澶勭悊绛夐〉搴曢儴鎿嶄綔鏍�
+.oa-page-footer {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 100;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
+ background: #fff;
+ border-top: 1px solid #eef0f3;
+ box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.08);
+}
+
+.oa-footer-btn {
+ flex: 1;
+ min-width: 0;
+ height: 44px;
+ line-height: 44px;
+ text-align: center;
+ font-size: 15px;
+ font-weight: 500;
+ border-radius: 22px;
+ border: none;
+
+ &:active {
+ opacity: 0.85;
+ }
+
+ &.btn-default {
+ color: #666;
+ background: #f2f4f7;
+ }
+
+ &.btn-primary {
+ color: #fff;
+ background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
+ box-shadow: 0 4px 10px rgba(0, 108, 251, 0.25);
+ }
+
+ &.btn-warn {
+ color: #fff;
+ background: linear-gradient(140deg, #ffb347 0%, #ff9800 100%);
+ box-shadow: 0 4px 10px rgba(255, 152, 0, 0.25);
+ }
+
+ &.btn-success {
+ color: #fff;
+ background: linear-gradient(140deg, #52c41a 0%, #389e0d 100%);
+ box-shadow: 0 4px 10px rgba(56, 158, 13, 0.25);
+ }
+
+ &.btn-danger {
+ color: #fff;
+ background: linear-gradient(140deg, #ff7875 0%, #f5222d 100%);
+ box-shadow: 0 4px 10px rgba(245, 34, 45, 0.2);
+ }
+
+ &.is-disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+}
+
+.oa-detail-page {
+ min-height: 100vh;
+ background: #f2f4f7;
+ display: flex;
+ flex-direction: column;
+}
+
+.oa-detail-scroll {
+ flex: 1;
+ height: 0;
+ padding: 10px 12px;
+ padding-bottom: calc(76px + env(safe-area-inset-bottom));
+ box-sizing: border-box;
+}
diff --git a/src/pages/oa/_utils/approvalFormField.js b/src/pages/oa/_utils/approvalFormField.js
new file mode 100644
index 0000000..813d05e
--- /dev/null
+++ b/src/pages/oa/_utils/approvalFormField.js
@@ -0,0 +1,372 @@
+import { parseTime } from "@/utils/ruoyi";
+
+/** 濉姤瀛楁绫诲瀷锛氫笅鎷� */
+export const SELECT_FIELD_TYPES = new Set(["select", "dropdown", "picker"]);
+
+/** 鏃ユ湡鏃堕棿绫� type */
+const DATE_KIND_BY_TYPE = {
+ date: "date",
+ time: "time",
+ datetime: "datetime",
+ datetimerange: "datetime",
+};
+
+const DEFAULT_FORMAT = {
+ date: "YYYY-MM-DD",
+ time: "HH:mm",
+ datetime: "YYYY-MM-DD HH:mm:ss",
+};
+
+/** 瑙f瀽 formConfig JSON锛堝惈宓屽 formPayload锛屼笌 Web parseInstanceFormConfig 涓�鑷达級 */
+export function parseApprovalFormConfig(raw) {
+ if (!raw) return { prompt: "", fields: [], formPayload: {} };
+ try {
+ const obj = typeof raw === "string" ? JSON.parse(raw) : raw;
+ const payload = obj?.formPayload;
+ return {
+ prompt: obj?.prompt || obj?.summaryPlaceholder || "",
+ summaryPlaceholder: obj?.summaryPlaceholder || "",
+ approvalType: obj?.approvalType || "",
+ fields: Array.isArray(obj?.fields)
+ ? obj.fields
+ : Array.isArray(obj?.formFields)
+ ? obj.formFields
+ : [],
+ formPayload: payload && typeof payload === "object" ? payload : {},
+ };
+ } catch {
+ return { prompt: "", fields: [], formPayload: {} };
+ }
+}
+
+/**
+ * 淇敼瀹℃壒锛氭ā鏉垮瓧娈靛畾涔� + 瀹炰緥宸插~鍊煎悎骞�
+ */
+export function mergeFormConfigForEdit(templateRaw, instanceRaw) {
+ const template = parseApprovalFormConfig(templateRaw);
+ const instance = parseApprovalFormConfig(instanceRaw);
+ const valueMap = {};
+ instance.fields.forEach(field => {
+ if (!field?.key) return;
+ const val = field.value ?? field.defaultValue;
+ if (val !== undefined && val !== null && val !== "") {
+ valueMap[field.key] = val;
+ }
+ });
+ Object.assign(valueMap, instance.formPayload || {});
+ const baseFields = template.fields.length ? template.fields : instance.fields;
+ return {
+ prompt: instance.prompt || template.prompt,
+ fields: baseFields.map(field => ({
+ ...field,
+ value: valueMap[field.key] ?? field.value ?? field.defaultValue ?? "",
+ })),
+ };
+}
+
+/** 鏄惁涓轰笅鎷夌被瀛楁 */
+export function isSelectField(field) {
+ const type = String(field?.type ?? "").toLowerCase();
+ return SELECT_FIELD_TYPES.has(type);
+}
+
+/** 鏄惁涓哄琛屾枃鏈� */
+export function isTextareaField(field) {
+ return String(field?.type ?? "").toLowerCase() === "textarea";
+}
+
+/** 璇诲彇瀛楁閰嶇疆鐨勬棩鏈熸牸寮忥紙鍏煎 format / dateFormat / timeFormat锛� */
+export function getFieldFormatStr(field) {
+ return (
+ field?.format ?? field?.dateFormat ?? field?.timeFormat ?? ""
+ ).trim();
+}
+
+/** 鏃ユ湡鏃堕棿瀛楁绉嶇被锛歞ate | time | datetime | null */
+export function getDateFieldKind(field) {
+ const type = String(field?.type ?? "").toLowerCase();
+ if (DATE_KIND_BY_TYPE[type]) return DATE_KIND_BY_TYPE[type];
+
+ const fmt = getFieldFormatStr(field);
+ if (!fmt) return null;
+ const hasDate = /Y{2,4}|D{1,2}/i.test(fmt);
+ const hasTime = /H{1,2}|h{1,2}|m{1,2}|s{1,2}/i.test(fmt);
+ if (hasDate && hasTime) return "datetime";
+ if (hasTime && !hasDate) return "time";
+ if (hasDate) return "date";
+ return null;
+}
+
+/** 鏄惁涓烘棩鏈�/鏃堕棿绫诲瓧娈碉紙涓嶅惈鏃ユ湡鏃堕棿鑼冨洿锛� */
+export function isDateLikeField(field) {
+ if (isDatetimerangeField(field)) return false;
+ return !!getDateFieldKind(field);
+}
+
+/** @deprecated 浣跨敤 isDateLikeField */
+export function isDateField(field) {
+ return isDateLikeField(field);
+}
+
+/** uView datetime-picker 鐨� mode */
+export function getDatePickerMode(field) {
+ const kind = getDateFieldKind(field);
+ if (kind === "time") return "time";
+ if (kind === "datetime") return "datetime";
+ return "date";
+}
+
+/** moment 椋庢牸鏍煎紡 鈫� parseTime 妯℃澘 */
+export function momentFormatToParsePattern(fmt) {
+ if (!fmt) return null;
+ return fmt
+ .replace(/YYYY/g, "{y}")
+ .replace(/YY/g, "{y}")
+ .replace(/DD/g, "{d}")
+ .replace(/dd/g, "{d}")
+ .replace(/MM/g, "{m}")
+ .replace(/HH/g, "{h}")
+ .replace(/hh/g, "{h}")
+ .replace(/mm/g, "{i}")
+ .replace(/ss/g, "{s}");
+}
+
+/** 灏嗘椂闂存埑/Date 鏍煎紡鍖栦负瀛楁閰嶇疆鏍煎紡 */
+export function formatFieldDateValue(field, dateSource) {
+ const kind = getDateFieldKind(field);
+ if (!kind) return "";
+ const fmt = getFieldFormatStr(field) || DEFAULT_FORMAT[kind];
+ const pattern = momentFormatToParsePattern(fmt);
+ let date;
+ if (typeof dateSource === "number") date = new Date(dateSource);
+ else if (dateSource instanceof Date) date = dateSource;
+ else return String(dateSource ?? "");
+ return parseTime(date, pattern) || "";
+}
+
+/** 灞曠ず鐢細灏嗗凡瀛樺�兼寜閰嶇疆鏍煎紡鍥炴樉 */
+export function formatFieldDisplayValue(field, storedValue) {
+ if (storedValue === undefined || storedValue === null || storedValue === "") {
+ return "";
+ }
+ if (!getDateFieldKind(field)) return String(storedValue);
+ const ts = parseFieldDateToTs(storedValue);
+ if (ts) return formatFieldDateValue(field, ts);
+ return String(storedValue);
+}
+
+/** 灏嗗凡瀛樻棩鏈熷瓧绗︿覆杞负鏃堕棿鎴筹紙渚涢�夋嫨鍣ㄥ垵濮嬪�硷級 */
+export function parseFieldDateToTs(value) {
+ if (value === undefined || value === null || value === "") return null;
+ if (typeof value === "number") return value;
+ const str = String(value).trim();
+ const normalized = str.replace(/-/g, "/").replace("T", " ");
+ const t = new Date(normalized).getTime();
+ return Number.isNaN(t) ? null : t;
+}
+
+/** 鏄惁涓烘暟瀛� */
+export function isNumberField(field) {
+ return String(field?.type ?? "").toLowerCase() === "number";
+}
+
+/**
+ * 灏嗗瓧娈甸厤缃腑鐨勯�夐」瑙勮寖涓� { label, value }[]
+ * 鏀寔锛歰ptions / optionList锛涢」涓哄瓧绗︿覆鎴� { label|name|text, value|key|code }
+ */
+export function normalizeFieldOptions(field) {
+ const raw =
+ field?.options ?? field?.optionList ?? field?.dictOptions ?? field?.items;
+ if (!Array.isArray(raw) || !raw.length) return [];
+
+ return raw
+ .map((item, index) => {
+ if (item == null) return null;
+ if (typeof item === "string" || typeof item === "number") {
+ const text = String(item);
+ return { label: text, value: text };
+ }
+ if (typeof item !== "object") return null;
+
+ const label =
+ item.label ??
+ item.name ??
+ item.text ??
+ item.dictLabel ??
+ item.title;
+ const rawValue =
+ item.value ?? item.key ?? item.code ?? item.dictValue ?? item.id;
+
+ if (label == null && rawValue == null) return null;
+
+ const value =
+ rawValue !== undefined && rawValue !== null ? rawValue : label ?? index;
+ return {
+ label: String(label ?? value),
+ value,
+ };
+ })
+ .filter(Boolean);
+}
+
+/** 鎸夊瓨鍌ㄥ�煎尮閰嶉�夐」灞曠ず鏂囨 */
+export function getFieldOptionLabel(field, storedValue) {
+ if (storedValue === undefined || storedValue === null || storedValue === "") {
+ return "";
+ }
+ const options = normalizeFieldOptions(field);
+ const strVal = String(storedValue);
+ const matched = options.find(
+ opt =>
+ String(opt.value) === strVal ||
+ String(opt.label) === strVal
+ );
+ return matched?.label ?? "";
+}
+
+/** 鍒濆鍖栧~鎶ュ�硷細浼樺厛宸插~ value锛屽叾娆� defaultValue */
+export function getFieldInitialValue(field) {
+ if (field?.value !== undefined && field?.value !== null && field?.value !== "") {
+ return field.value;
+ }
+ if (
+ field?.defaultValue !== undefined &&
+ field?.defaultValue !== null &&
+ field?.defaultValue !== ""
+ ) {
+ return field.defaultValue;
+ }
+ return "";
+}
+
+/** 妯℃澘缂栬緫锛氭帶浠剁被鍨嬮�夐」锛堜笌 Web 绔竴鑷达級 */
+export const FIELD_EDITOR_TYPE_OPTIONS = [
+ { name: "鍗曡鏂囨湰", value: "text" },
+ { name: "澶氳鏂囨湰", value: "textarea" },
+ { name: "鏁板瓧", value: "number" },
+ { name: "鏃ユ湡", value: "date" },
+ { name: "鏃ユ湡鏃堕棿鑼冨洿", value: "datetimerange" },
+ { name: "涓嬫媺閫夋嫨", value: "select" },
+];
+
+/** 涓嬫媺閫夐」鏉ユ簮 */
+export const FIELD_OPTION_SOURCE_OPTIONS = [
+ { name: "鎵嬪姩閰嶇疆", value: "manual" },
+ { name: "浜哄憳鍒楄〃", value: "user" },
+ { name: "閮ㄩ棬鍒楄〃", value: "dept" },
+];
+
+const OPTION_SOURCE_ALIASES = {
+ manual: "manual",
+ user: "user",
+ personnel: "user",
+ userlist: "user",
+ dept: "dept",
+ department: "dept",
+ deptlist: "dept",
+};
+
+export function getFieldEditorTypeLabel(type) {
+ const found = FIELD_EDITOR_TYPE_OPTIONS.find(
+ item => String(item.value) === String(type)
+ );
+ return found?.name || type || "-";
+}
+
+export function getFieldOptionSourceLabel(source) {
+ const key = getFieldOptionSource(source);
+ const found = FIELD_OPTION_SOURCE_OPTIONS.find(item => item.value === key);
+ return found?.name || "鎵嬪姩閰嶇疆";
+}
+
+/** 瑙f瀽閫夐」鏉ユ簮锛歮anual | user | dept */
+export function getFieldOptionSource(fieldOrSource) {
+ const raw =
+ typeof fieldOrSource === "object"
+ ? fieldOrSource?.optionSource
+ : fieldOrSource;
+ const key = String(raw ?? "manual")
+ .trim()
+ .toLowerCase();
+ return OPTION_SOURCE_ALIASES[key] || "manual";
+}
+
+export function isDatetimerangeField(field) {
+ return String(field?.type ?? "").toLowerCase() === "datetimerange";
+}
+
+/** 瑙f瀽鏃ユ湡鏃堕棿鑼冨洿榛樿鍊硷細start,end */
+export function parseDatetimerangeValue(stored) {
+ if (stored === undefined || stored === null || stored === "") {
+ return { start: "", end: "" };
+ }
+ const parts = String(stored)
+ .split(",")
+ .map(s => s.trim());
+ return { start: parts[0] || "", end: parts[1] || "" };
+}
+
+export function joinDatetimerangeValue(start, end) {
+ const s = String(start ?? "").trim();
+ const e = String(end ?? "").trim();
+ if (!s && !e) return "";
+ return `${s},${e}`;
+}
+
+export function formatDatetimerangeDisplay(stored) {
+ const { start, end } = parseDatetimerangeValue(stored);
+ if (!start && !end) return "";
+ if (start && end) return `${start} 鑷� ${end}`;
+ return start || end;
+}
+
+/**
+ * 瑙f瀽涓嬫媺閫夐」锛堝惈浜哄憳/閮ㄩ棬鍔ㄦ�佹潵婧愶級
+ * @param {object} field
+ * @param {{ users?: array, depts?: array }} context
+ */
+export function resolveFieldOptions(field, context = {}) {
+ const source = getFieldOptionSource(field);
+ if (source === "user") {
+ return (context.users || []).map(user => ({
+ label: user.nickName || user.userName || String(user.userId ?? ""),
+ value: user.userId,
+ }));
+ }
+ if (source === "dept") {
+ return (context.depts || []).map(dept => ({
+ label: dept.deptName || dept.name || String(dept.deptId ?? dept.id ?? ""),
+ value: dept.deptId ?? dept.id,
+ }));
+ }
+ return normalizeFieldOptions(field);
+}
+
+export function createEmptyFieldOption() {
+ return { label: "", value: "" };
+}
+
+/** 灏嗙紪杈戣崏绋胯鑼冧负鍙彁浜ょ殑瀛楁瀵硅薄 */
+export function buildFieldConfigPayload(draft, existingKey) {
+ const payload = {
+ key: (draft.key || existingKey || "").trim(),
+ label: (draft.label || "").trim(),
+ type: draft.type || "text",
+ required: !!draft.required,
+ defaultValue: String(draft.defaultValue ?? "").trim(),
+ };
+ if (isSelectField(payload)) {
+ payload.optionSource = getFieldOptionSource(draft.optionSource);
+ if (payload.optionSource === "manual") {
+ payload.options = (draft.options || [])
+ .map(opt => ({
+ label: String(opt?.label ?? "").trim(),
+ value: String(opt?.value ?? "").trim(),
+ }))
+ .filter(opt => opt.label && opt.value);
+ } else {
+ delete payload.options;
+ }
+ }
+ return payload;
+}
diff --git a/src/pages/oa/_utils/approvalModuleApplyExtras.js b/src/pages/oa/_utils/approvalModuleApplyExtras.js
new file mode 100644
index 0000000..820d584
--- /dev/null
+++ b/src/pages/oa/_utils/approvalModuleApplyExtras.js
@@ -0,0 +1,293 @@
+import dayjs from "dayjs";
+import {
+ APPROVAL_MODULE_KEYS,
+ APPROVAL_MODULE_REGISTRY,
+ getModuleMatchingBusinessTypes,
+} from "./approvalModuleRegistry.js";
+import { matchBusinessTypeValue } from "./approvalTemplateType.js";
+import { parseDatetimerangeValue } from "./approvalFormField.js";
+
+/** 浜哄憳涓嬫媺瀛楁璇嗗埆锛堜笌 Web SELECT_OPTION_SOURCE.USER 绛変环锛� */
+export function isUserSelectField(field) {
+ const src = String(field?.optionSource ?? "").toLowerCase();
+ return (
+ src === "user" ||
+ src === "personnel" ||
+ src === "userlist" ||
+ (field?.type === "select" && String(field?.label || "").includes("鐢宠浜�"))
+ );
+}
+
+export function findApplicantTemplateField(fields = []) {
+ return (
+ fields.find(f => String(f?.label || "").includes("鐢宠浜�")) ||
+ fields.find(f => isUserSelectField(f)) ||
+ null
+ );
+}
+
+/* ---------- 璇峰亣 ---------- */
+
+export function isLeaveBalanceField(field) {
+ const label = String(field?.label || "");
+ return label.includes("鍋囨湡浣欓") || field?.key === "leaveBalanceDays";
+}
+
+export function isLeaveDurationField(field) {
+ const label = String(field?.label || "");
+ return label.includes("璇峰亣鏃堕暱") || field?.key === "leaveDurationDays";
+}
+
+export function displayLeaveTemplateFields(fields = []) {
+ return (fields || []).filter(
+ f => !isLeaveBalanceField(f) && !isLeaveDurationField(f)
+ );
+}
+
+export function findLeaveTimeTemplateField(fields = []) {
+ return (
+ fields.find(
+ f => f?.type === "datetimerange" && String(f?.label || "").includes("璇峰亣鏃堕棿")
+ ) ||
+ fields.find(f => f?.type === "datetimerange" && f?.key === "dateRange") ||
+ fields.find(f => f?.type === "datetimerange") ||
+ null
+ );
+}
+
+export function resolveTimeRangeFromPayload(payload, timeField) {
+ if (!timeField?.key) return { start: "", end: "" };
+ const val = payload?.[timeField.key];
+ if (Array.isArray(val) && val.length >= 2) {
+ return { start: val[0] || "", end: val[1] || "" };
+ }
+ return parseDatetimerangeValue(val);
+}
+
+export function computeLeaveDays(startStr, endStr) {
+ if (!startStr || !endStr) return null;
+ const t0 = dayjs(startStr);
+ const t1 = dayjs(endStr);
+ if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
+ const days = t1.diff(t0, "millisecond") / (24 * 60 * 60 * 1000);
+ return Math.round(days * 100) / 100;
+}
+
+export function computeLeaveDurationDisplay(fields, formPayload) {
+ const timeField = findLeaveTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, timeField);
+ const d = computeLeaveDays(start, end);
+ return d == null ? "" : String(d);
+}
+
+export function validateLeaveBeforeSubmit(fields, formPayload) {
+ const timeField = findLeaveTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, timeField);
+ if (computeLeaveDays(start, end) == null) {
+ return "璇锋鏌ユā鏉夸腑鐨勮鍋囨椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�";
+ }
+ return "";
+}
+
+/* ---------- 鍔犵彮 ---------- */
+
+export function isOvertimeDurationField(field) {
+ const label = String(field?.label || "");
+ return label.includes("鍔犵彮鏃堕暱") || field?.key === "overtimeHours";
+}
+
+export function displayOvertimeTemplateFields(fields = []) {
+ return (fields || []).filter(f => !isOvertimeDurationField(f));
+}
+
+export function findOvertimeTimeTemplateField(fields = []) {
+ return (
+ fields.find(
+ f => f?.type === "datetimerange" && String(f?.label || "").includes("鍔犵彮鏃堕棿")
+ ) ||
+ fields.find(f => f?.type === "datetimerange") ||
+ null
+ );
+}
+
+export function computeOvertimeHours(startStr, endStr) {
+ if (!startStr || !endStr) return null;
+ const t0 = dayjs(startStr);
+ const t1 = dayjs(endStr);
+ if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
+ return Math.round((t1.diff(t0, "millisecond") / 3600000) * 100) / 100;
+}
+
+export function computeOvertimeHoursDisplay(fields, formPayload) {
+ const field = findOvertimeTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, field);
+ const h = computeOvertimeHours(start, end);
+ return h == null ? "" : String(h);
+}
+
+export function validateOvertimeBeforeSubmit(fields, formPayload) {
+ const field = findOvertimeTimeTemplateField(fields);
+ const { start, end } = resolveTimeRangeFromPayload(formPayload, field);
+ if (computeOvertimeHours(start, end) == null) {
+ return "璇锋鏌ユā鏉夸腑鐨勫姞鐝椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�";
+ }
+ return "";
+}
+
+/* ---------- 璋冨矖 ---------- */
+
+export function isOriginalPostField(field) {
+ const label = String(field?.label || "");
+ return (
+ label.includes("鍘熷矖浣�") ||
+ field?.key === "originalPost" ||
+ field?.key === "originalPostName" ||
+ field?.key === "originalPostId"
+ );
+}
+
+export function displayTransferTemplateFields(fields = []) {
+ return (fields || []).filter(f => !isOriginalPostField(f));
+}
+
+export function unwrapUserArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+export function isActiveUser(u) {
+ if (u?.delFlag === "2" || u?.delFlag === 2) return false;
+ if (u?.status == null) return true;
+ return String(u.status) === "0";
+}
+
+export function firstPostId(user) {
+ if (!user) return undefined;
+ if (Array.isArray(user.postIds) && user.postIds.length) return user.postIds[0];
+ if (user.postId != null && user.postId !== "") return user.postId;
+ return undefined;
+}
+
+export function buildPostIdToNameMap(postRows = []) {
+ const m = {};
+ for (const p of postRows) {
+ const id = p.postId ?? p.value ?? p.id;
+ if (id != null && id !== "") {
+ m[String(id)] = p.postName ?? p.label ?? p.name ?? "";
+ }
+ }
+ return m;
+}
+
+export function resolveOriginalPostName(user, postIdToName = {}) {
+ if (!user) return "";
+ const nameStr = (user.postName ?? user.postname ?? "").toString().trim();
+ if (nameStr) return nameStr;
+ if (Array.isArray(user.posts) && user.posts.length) {
+ return (user.posts[0].postName ?? "").toString() || "鏈懡鍚嶅矖浣�";
+ }
+ const pid = firstPostId(user);
+ if (pid != null && pid !== "") {
+ const n = postIdToName[String(pid)] || "";
+ return n || "褰撳墠宀椾綅锛堟湭鍦ㄥ矖浣嶅瓧鍏镐腑锛�";
+ }
+ return "鏈垎閰嶅矖浣�";
+}
+
+export function userById(users, id) {
+ if (id == null || id === "") return undefined;
+ return (users || []).find(u => String(u.userId ?? u.id) === String(id));
+}
+
+/** 鎸� moduleKey 杩囨护妯℃澘濉姤椤� */
+export function displayTemplateFieldsByModule(moduleKey, fields = []) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ return displayLeaveTemplateFields(fields);
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ return displayOvertimeTemplateFields(fields);
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) {
+ return displayTransferTemplateFields(fields);
+ }
+ return fields || [];
+}
+
+/** 淇濆瓨鍓嶅皢涓氬姟鎵╁睍瀛楁鍐欏叆 formValues */
+export function syncModuleExtrasToFormValues(moduleKey, formValues, extras, fields) {
+ if (!moduleKey || !formValues) return;
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ if (extras.leaveBalanceDays != null && extras.leaveBalanceDays !== "") {
+ formValues.leaveBalanceDays = extras.leaveBalanceDays;
+ }
+ const days = computeLeaveDurationDisplay(fields, formValues);
+ if (days) formValues.leaveDurationDays = days;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ const hours = computeOvertimeHoursDisplay(fields, formValues);
+ if (hours) formValues.overtimeHours = hours;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRANSFER && extras.originalPostName) {
+ formValues.originalPostName = extras.originalPostName;
+ formValues.originalPost = extras.originalPostName;
+ }
+}
+
+/** 涓氬姟鎵╁睍鏍¢獙 */
+export function validateModuleExtras(moduleKey, fields, formPayload, extras) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ if (
+ extras.leaveBalanceDays == null ||
+ extras.leaveBalanceDays === "" ||
+ Number.isNaN(Number(extras.leaveBalanceDays))
+ ) {
+ return "璇峰~鍐欏亣鏈熶綑棰�";
+ }
+ const msg = validateLeaveBeforeSubmit(fields, formPayload);
+ if (msg) return msg;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.OVERTIME) {
+ const msg = validateOvertimeBeforeSubmit(fields, formPayload);
+ if (msg) return msg;
+ }
+ return "";
+}
+
+/** 浠庡疄渚� businessType 鎺ㄦ柇 moduleKey锛堢紪杈戝叆鍙f湭甯� moduleKey 鏃讹級 */
+export function inferModuleKeyFromRow(row, typeOptions = []) {
+ const bt = row?.businessType;
+ if (bt == null || bt === "") return "";
+ for (const key of Object.values(APPROVAL_MODULE_KEYS)) {
+ const types = getModuleMatchingBusinessTypes(key, typeOptions);
+ if (types.some(t => matchBusinessTypeValue(t, bt))) return key;
+ const cfg = APPROVAL_MODULE_REGISTRY[key];
+ if (cfg && matchBusinessTypeValue(cfg.approvalType, bt)) return key;
+ }
+ return "";
+}
+
+/** 缂栬緫鍥炴樉锛氫粠瀹炰緥琛屾仮澶嶆墿灞曞瓧娈� */
+export function loadModuleExtrasFromRow(moduleKey, row, formPayload) {
+ const extras = {
+ leaveBalanceDays: undefined,
+ originalPostName: "",
+ };
+ if (!moduleKey || !row) return extras;
+
+ const payload = formPayload || {};
+ if (moduleKey === APPROVAL_MODULE_KEYS.LEAVE) {
+ const v = payload.leaveBalanceDays ?? row.leaveBalanceDays;
+ extras.leaveBalanceDays =
+ v != null && v !== "" ? Number(v) : undefined;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRANSFER) {
+ extras.originalPostName =
+ payload.originalPostName ||
+ payload.originalPost ||
+ row.originalPostName ||
+ "";
+ }
+ return extras;
+}
diff --git a/src/pages/oa/_utils/approvalModuleListSearch.js b/src/pages/oa/_utils/approvalModuleListSearch.js
new file mode 100644
index 0000000..a98409a
--- /dev/null
+++ b/src/pages/oa/_utils/approvalModuleListSearch.js
@@ -0,0 +1,456 @@
+import {
+ APPROVAL_MODULE_KEYS,
+ APPROVAL_MODULE_REGISTRY,
+ getApprovalModuleConfig,
+ getModuleMatchingBusinessTypes,
+} from "./approvalModuleRegistry.js";
+import {
+ parseApprovalFormConfig,
+ parseDatetimerangeValue,
+} from "./approvalFormField.js";
+import { matchBusinessTypeValue } from "./approvalTemplateType.js";
+
+/** 涓� Web leave-apply LEAVE_TYPE_OPTIONS 涓�鑷� */
+export const LEAVE_TYPE_OPTIONS = [
+ { label: "骞村亣", value: "annual" },
+ { label: "鐥呭亣", value: "sick" },
+ { label: "浜嬪亣", value: "personal" },
+ { label: "濠氬亣", value: "marriage" },
+ { label: "浜у亣", value: "maternity" },
+ { label: "鍝轰钩鍋�", value: "nursing" },
+ { label: "鎱板攣鍋�", value: "condolence" },
+ { label: "璋冧紤", value: "compensatory" },
+];
+
+/** 涓� Web overtime-apply OVERTIME_TYPE_OPTIONS 涓�鑷� */
+export const OVERTIME_TYPE_OPTIONS = [
+ { label: "宸ヤ綔鏃ュ姞鐝�", value: "weekday" },
+ { label: "浼戞伅鏃ュ姞鐝�", value: "weekend" },
+ { label: "娉曞畾鑺傚亣鏃ュ姞鐝�", value: "holiday" },
+];
+
+export const HANDOVER_STATUS_OPTIONS = [
+ { label: "杩涜涓�", value: "in_progress" },
+ { label: "宸插畬鎴�", value: "completed" },
+ { label: "宸查��鍥�", value: "returned" },
+];
+
+export const HANDOVER_TYPE_OPTIONS = [
+ { label: "绂昏亴浜ゆ帴", value: "resignation" },
+ { label: "璋冨矖浜ゆ帴", value: "transfer" },
+];
+
+function buildFormPayloadFromFields(fields = []) {
+ const payload = {};
+ for (const f of fields) {
+ if (!f?.key) continue;
+ const val = f.value ?? f.defaultValue;
+ if (val !== undefined && val !== null && val !== "") {
+ payload[f.key] = val;
+ }
+ }
+ return payload;
+}
+
+function guessFieldTypeFromValue(val) {
+ if (Array.isArray(val)) return "datetimerange";
+ if (typeof val === "number") return "number";
+ return "text";
+}
+
+/** 瑙f瀽瀹炰緥 formConfig / formPayload锛堜笌 Web resolveInstanceFormFields 瀵归綈锛� */
+export function resolveInstanceFormPayload(row) {
+ const cfg = parseApprovalFormConfig(row?.formConfig);
+ let fields = (row?.formFieldDefs?.length ? row.formFieldDefs : cfg.fields) || [];
+ const formPayload = {
+ ...(fields.length ? buildFormPayloadFromFields(fields) : {}),
+ ...cfg.formPayload,
+ ...(row?.formPayload || {}),
+ };
+ if (!fields.length && Object.keys(formPayload).length) {
+ fields = Object.keys(formPayload)
+ .filter(k => k && k !== "summary")
+ .map(k => ({
+ key: k,
+ label: k,
+ type: guessFieldTypeFromValue(formPayload[k]),
+ required: false,
+ options: [],
+ }));
+ }
+ fields = fields.map(field => ({
+ ...field,
+ value:
+ formPayload[field.key] ?? field.value ?? field.defaultValue ?? "",
+ }));
+ return { fields, formPayload };
+}
+
+/** 宸茬煡涓嬫媺瀛楁 value 鈫� 灞曠ず鏂囨锛堟ā鏉挎湭甯� options 鏃跺厹搴曪級 */
+export function formatKnownSelectLabel(prop, val) {
+ if (val === undefined || val === null || val === "") return "";
+ const maps = {
+ leaveType: LEAVE_TYPE_OPTIONS,
+ overtimeType: OVERTIME_TYPE_OPTIONS,
+ handoverStatus: HANDOVER_STATUS_OPTIONS,
+ handoverType: HANDOVER_TYPE_OPTIONS,
+ };
+ const options = maps[prop];
+ if (!options) return "";
+ const hit = options.find(o => String(o.value) === String(val));
+ return hit?.label || "";
+}
+
+export function getRowPayloadValue(row, keys) {
+ const keyList = Array.isArray(keys) ? keys : [keys];
+ const { formPayload } = resolveInstanceFormPayload(row);
+ for (const k of keyList) {
+ if (row?.[k] != null && row[k] !== "") return row[k];
+ if (formPayload[k] != null && formPayload[k] !== "") return formPayload[k];
+ }
+ return "";
+}
+
+const DATETIME_RANGE_KEYS = [
+ "dateRange",
+ "leaveTime",
+ "overtimeTime",
+ "timeRange",
+];
+
+function pickDatetimerangeRaw(formPayload, fields = []) {
+ for (const key of DATETIME_RANGE_KEYS) {
+ const v = formPayload?.[key];
+ if (v != null && v !== "") return v;
+ const field = (fields || []).find(f => f?.key === key);
+ const fv = field?.value ?? field?.defaultValue;
+ if (fv != null && fv !== "") return fv;
+ }
+ const rangeField = (fields || []).find(
+ f => String(f?.type || "").toLowerCase() === "datetimerange"
+ );
+ if (rangeField?.key) {
+ const v =
+ formPayload?.[rangeField.key] ?? rangeField.value ?? rangeField.defaultValue;
+ if (v != null && v !== "") return v;
+ }
+ return null;
+}
+
+function splitRangeValue(val) {
+ if (val === undefined || val === null || val === "") {
+ return { start: "", end: "" };
+ }
+ if (Array.isArray(val)) {
+ return { start: val[0] || "", end: val[1] || "" };
+ }
+ return parseDatetimerangeValue(val);
+}
+
+/**
+ * 鍒楄〃鍒� prop 涓� formPayload 瀵归綈锛堣鍋� startTime/endTime 鏉ヨ嚜 dateRange锛�
+ */
+export function resolveListFieldRawValue(prop, row, fields = [], formPayload = {}) {
+ const payload = formPayload || {};
+ const direct = payload[prop] ?? row?.[prop];
+
+ if (prop === "startTime" || prop === "endTime") {
+ if (direct != null && direct !== "") return direct;
+ const altStart =
+ payload.start ?? payload.startDate ?? payload.beginTime ?? row?.startTime;
+ const altEnd =
+ payload.end ?? payload.endDate ?? payload.finishTime ?? row?.endTime;
+ if (prop === "startTime" && altStart) return altStart;
+ if (prop === "endTime" && altEnd) return altEnd;
+ const { start, end } = splitRangeValue(pickDatetimerangeRaw(payload, fields));
+ return prop === "startTime" ? start : end;
+ }
+
+ if (prop === "overtimeDate") {
+ const d = payload.overtimeDate ?? payload.date ?? direct;
+ if (d != null && d !== "") return Array.isArray(d) ? d[0] || "" : d;
+ const { start } = splitRangeValue(pickDatetimerangeRaw(payload, fields));
+ return start || "";
+ }
+
+ if (direct != null && direct !== "") return direct;
+ const hit = (fields || []).find(f => f?.key === prop);
+ return hit?.value ?? hit?.defaultValue ?? "";
+}
+
+/** 鎵佸钩鍖栦负 Spring GET 鍙粦瀹氱殑 query锛坅pprovalInstanceDto.xxx锛屽嬁鐢ㄦ柟鎷彿锛� */
+export function appendDotNotationQuery(target, prefix, fields) {
+ if (!fields || typeof fields !== "object") return;
+ for (const [key, value] of Object.entries(fields)) {
+ if (value == null || value === "") continue;
+ target[`${prefix}.${key}`] = value;
+ }
+}
+
+function pickApplicantFromSearchForm(searchForm = {}) {
+ const out = {};
+ const sf = searchForm || {};
+ const name = (sf.applicantName || "").trim();
+ const kw = (sf.applicantKeyword || "").trim();
+ const id = sf.applicantId;
+
+ if (name) out.applicantName = name;
+ if (kw) {
+ out.applicantName = kw;
+ if (/^\d+$/.test(kw)) out.applicantId = Number(kw);
+ }
+ if (id != null && id !== "") {
+ out.applicantId = typeof id === "number" ? id : Number(id) || id;
+ }
+ return out;
+}
+
+function pickInstanceNoFromSearchForm(searchForm = {}) {
+ const no = (searchForm?.instanceNo || "").trim();
+ return no ? { instanceNo: no } : {};
+}
+
+/** 鏀寔瀹℃壒鍗曞彿鏌ヨ鐨勫鎵圭敵璇锋ā鍧� */
+export const INSTANCE_NO_SEARCH_MODULE_KEYS = new Set([
+ APPROVAL_MODULE_KEYS.REGULAR,
+ APPROVAL_MODULE_KEYS.TRANSFER,
+ APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+ APPROVAL_MODULE_KEYS.LEAVE,
+ APPROVAL_MODULE_KEYS.OVERTIME,
+]);
+
+const INSTANCE_NO_SEARCH_FIELD = {
+ key: "instanceNo",
+ type: "input",
+ label: "瀹℃壒鍗曞彿",
+ placeholder: "璇疯緭鍏ュ鎵瑰崟鍙�",
+};
+
+/** 缁勮 approvalInstanceDto 鏌ヨ鐗囨锛堢敵璇蜂汉 + 瀹℃壒鍗曞彿锛� */
+export function buildApprovalInstanceSearchDto(searchForm = {}, extraParams = {}) {
+ const dto = {
+ ...(extraParams && typeof extraParams === "object" ? extraParams : {}),
+ };
+ Object.assign(dto, pickApplicantFromSearchForm(searchForm));
+ Object.assign(dto, pickInstanceNoFromSearchForm(searchForm));
+ delete dto.createTime;
+ delete dto.createTimeStart;
+ delete dto.createTimeEnd;
+ return dto;
+}
+
+function pickDateRange(searchForm) {
+ return buildApprovalInstanceSearchDto(searchForm);
+}
+
+/** 鍚勬ā鍧楅粯璁ゆ煡璇㈣〃鍗曪紙涓� Web searchForm 瀛楁涓�鑷达級 */
+const APPLICANT_ONLY_MODULE_KEYS = new Set([
+ APPROVAL_MODULE_KEYS.REGULAR,
+ APPROVAL_MODULE_KEYS.TRANSFER,
+ APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+ APPROVAL_MODULE_KEYS.LEAVE,
+ APPROVAL_MODULE_KEYS.OVERTIME,
+ APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE,
+ APPROVAL_MODULE_KEYS.COST_REIMBURSE,
+]);
+
+function withInstanceNoSearch(moduleKey, base) {
+ if (INSTANCE_NO_SEARCH_MODULE_KEYS.has(moduleKey)) {
+ return { instanceNo: "", ...base };
+ }
+ return base;
+}
+
+function applicantOnlySearchForm(moduleKey) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.REGULAR) {
+ return withInstanceNoSearch(moduleKey, { applicantName: "" });
+ }
+ if (
+ moduleKey === APPROVAL_MODULE_KEYS.TRANSFER ||
+ moduleKey === APPROVAL_MODULE_KEYS.WORK_HANDOVER
+ ) {
+ return withInstanceNoSearch(moduleKey, { applicantId: "" });
+ }
+ if (APPLICANT_ONLY_MODULE_KEYS.has(moduleKey)) {
+ return withInstanceNoSearch(moduleKey, { applicantKeyword: "" });
+ }
+ return {};
+}
+
+export function createModuleSearchForm(moduleKey) {
+ if (APPLICANT_ONLY_MODULE_KEYS.has(moduleKey)) {
+ return applicantOnlySearchForm(moduleKey);
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS) {
+ return { applicantKeyword: "" };
+ }
+ return {};
+}
+
+/** 鏈嶅姟绔� listPage DTO 鐗囨锛堜笌 Web buildApprovalInstanceListParams 涓�鑷达級 */
+export function buildModuleListDto(moduleKey, searchForm = {}) {
+ return buildApprovalInstanceSearchDto(searchForm);
+}
+
+function matchInstanceNo(row, instanceNo) {
+ const kw = (instanceNo || "").trim().toLowerCase();
+ if (!kw) return true;
+ const parts = [row?.instanceNo, row?.bizId]
+ .filter(v => v != null && v !== "")
+ .map(v => String(v).toLowerCase());
+ return parts.some(p => p.includes(kw));
+}
+
+export function hasActiveModuleSearch(moduleKey, searchForm = {}) {
+ const sf = searchForm || {};
+ if ((sf.instanceNo || "").trim()) return true;
+ if ((sf.applicantKeyword || "").trim()) return true;
+ if ((sf.applicantName || "").trim()) return true;
+ return sf.applicantId != null && sf.applicantId !== "";
+}
+
+function matchApplicantKeyword(row, keyword) {
+ const kw = (keyword || "").trim().toLowerCase();
+ if (!kw) return true;
+ const parts = [
+ row?.applicantName,
+ row?.applicantNo,
+ row?.applicantId,
+ getRowPayloadValue(row, ["applicant", "applicantName", "applicantId"]),
+ ]
+ .filter(v => v != null && v !== "")
+ .map(v => String(v).toLowerCase());
+ return parts.some(p => p.includes(kw));
+}
+
+function matchSelectValue(row, keys, expected) {
+ if (!expected) return true;
+ const raw = getRowPayloadValue(row, keys);
+ return String(raw) === String(expected);
+}
+
+function matchApplicantId(row, applicantId) {
+ if (!applicantId) return true;
+ const id = String(applicantId);
+ if (row?.applicantId != null && String(row.applicantId) === id) return true;
+ const payloadApplicant = getRowPayloadValue(row, [
+ "applicant",
+ "applicantId",
+ "applicantUserId",
+ ]);
+ return String(payloadApplicant) === id;
+}
+
+/** 鎸夋ā鍧� businessType / 鏍囬褰掑睘杩囨护锛堟湇鍔$鏈敓鏁堟椂鐨勫厹搴曪級 */
+export function filterRowsByModuleBusinessType(moduleKey, rows, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return rows;
+
+ const types = getModuleMatchingBusinessTypes(moduleKey, typeOptions);
+ const myLabels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+
+ return (rows || []).filter(row => {
+ if (types.length && row?.businessType != null && row.businessType !== "") {
+ if (types.some(t => matchBusinessTypeValue(row.businessType, t))) {
+ return true;
+ }
+ }
+
+ const title = String(row?.title || row?.templateName || "").trim();
+ if (title) {
+ if (myLabels.some(l => title === l || title.includes(l))) return true;
+ for (const [key, other] of Object.entries(APPROVAL_MODULE_REGISTRY)) {
+ if (key === moduleKey) continue;
+ const otherLabels = [other.label, ...(other.typeLabels || [])].filter(Boolean);
+ if (otherLabels.some(l => title === l || (l.length > 2 && title.includes(l)))) {
+ return false;
+ }
+ }
+ }
+
+ return types.length === 0;
+ });
+}
+
+/** 鎸夌敵璇蜂汉銆佸鎵瑰崟鍙峰仛鍓嶇鍏滃簳绛涢�� */
+export function filterRowsByModuleSearch(moduleKey, rows, searchForm = {}) {
+ const sf = searchForm || {};
+ const list = Array.isArray(rows) ? rows : [];
+ if (!hasActiveModuleSearch(moduleKey, sf)) return list;
+
+ return list.filter(
+ row =>
+ matchInstanceNo(row, sf.instanceNo) &&
+ matchApplicantId(row, sf.applicantId) &&
+ matchApplicantKeyword(row, sf.applicantKeyword || sf.applicantName)
+ );
+}
+
+function prependInstanceNoField(fields, moduleKey) {
+ if (!INSTANCE_NO_SEARCH_MODULE_KEYS.has(moduleKey)) return fields;
+ return [INSTANCE_NO_SEARCH_FIELD, ...fields];
+}
+
+/** 妯″潡绛涢�� UI 閰嶇疆 */
+export function getModuleSearchMeta(moduleKey) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.REGULAR) {
+ return {
+ fields: prependInstanceNoField(
+ [
+ { key: "applicantName", type: "input", label: "鐢宠浜�", placeholder: "璇疯緭鍏ョ敵璇蜂汉" },
+ ],
+ moduleKey
+ ),
+ };
+ }
+ if (
+ moduleKey === APPROVAL_MODULE_KEYS.TRANSFER ||
+ moduleKey === APPROVAL_MODULE_KEYS.WORK_HANDOVER
+ ) {
+ return {
+ fields: prependInstanceNoField(
+ [
+ { key: "applicantId", type: "user", label: "鐢宠浜�", placeholder: "璇烽�夋嫨鐢宠浜�" },
+ ],
+ moduleKey
+ ),
+ };
+ }
+ if (APPLICANT_ONLY_MODULE_KEYS.has(moduleKey)) {
+ return {
+ fields: prependInstanceNoField(
+ [
+ {
+ key: "applicantKeyword",
+ type: "input",
+ label: "鐢宠浜�",
+ placeholder: "濮撳悕鎴栫紪鍙�",
+ },
+ ],
+ moduleKey
+ ),
+ };
+ }
+ return { fields: [] };
+}
+
+export function resetModuleSearchForm(moduleKey, target) {
+ const defaults = createModuleSearchForm(moduleKey);
+ Object.keys(target).forEach(k => {
+ if (!(k in defaults)) delete target[k];
+ });
+ Object.assign(target, defaults);
+}
+
+export function formatDateRangeLabel(range) {
+ if (!Array.isArray(range) || !range[0]) return "";
+ if (range[1]) return `${range[0]} 鑷� ${range[1]}`;
+ return range[0];
+}
+
+export function userSelectLabel(u) {
+ const nick = u?.nickName || "";
+ const name = u?.userName || "";
+ if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
+ return nick || name || `鐢ㄦ埛${u?.userId ?? u?.id ?? ""}`;
+}
diff --git a/src/pages/oa/_utils/approvalModuleRegistry.js b/src/pages/oa/_utils/approvalModuleRegistry.js
new file mode 100644
index 0000000..1019d35
--- /dev/null
+++ b/src/pages/oa/_utils/approvalModuleRegistry.js
@@ -0,0 +1,189 @@
+
+/** 涓� Web approvalModuleRegistry 涓�鑷� */
+export const APPROVAL_MODULE_KEYS = {
+ REGULAR: "regular",
+ TRANSFER: "transfer",
+ WORK_HANDOVER: "work_handover",
+ LEAVE: "leave",
+ OVERTIME: "overtime",
+ TRAVEL_REIMBURSE: "travel_reimburse",
+ COST_REIMBURSE: "cost_reimburse",
+ ENTERPRISE_NEWS: "enterprise_news",
+};
+
+/** 瀹℃壒瀹炰緥 listPage businessType锛堜笌鍚庣绾﹀畾涓�鑷达級 */
+export const APPROVAL_BUSINESS_TYPE = {
+ [APPROVAL_MODULE_KEYS.REGULAR]: 10,
+ [APPROVAL_MODULE_KEYS.TRANSFER]: 11,
+ [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: 13,
+ [APPROVAL_MODULE_KEYS.LEAVE]: 14,
+ [APPROVAL_MODULE_KEYS.OVERTIME]: 15,
+ [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: 16,
+ [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: 17,
+ [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: 18,
+};
+
+export const APPROVAL_MODULE_REGISTRY = {
+ [APPROVAL_MODULE_KEYS.REGULAR]: {
+ label: "杞鐢宠",
+ approvalType: "regular",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.REGULAR],
+ typeLabels: ["杞", "杞鐢宠"],
+ listFields: [
+ { label: "瀹℃壒鍗曞彿", prop: "instanceNo" },
+ { label: "鍏ヨ亴鏃ユ湡", prop: "entryDate" },
+ { label: "杞鏃ユ湡", prop: "regularDate" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.TRANSFER]: {
+ label: "璋冨矖鐢宠",
+ approvalType: "transfer",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.TRANSFER],
+ typeLabels: ["璋冨矖", "璋冨姩", "璋冨矖鐢宠", "璋冨姩鐢宠"],
+ listFields: [
+ { label: "瀹℃壒鍗曞彿", prop: "instanceNo" },
+ { label: "鍘熷矖浣�", prop: "fromPost" },
+ { label: "鐩爣宀椾綅", prop: "toPost" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: {
+ label: "宸ヤ綔浜ゆ帴",
+ approvalType: "work_handover",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.WORK_HANDOVER],
+ typeLabels: ["宸ヤ綔浜ゆ帴", "浜ゆ帴", "宸ヤ綔浜ゆ帴瀹℃壒"],
+ listFields: [
+ { label: "瀹℃壒鍗曞彿", prop: "instanceNo" },
+ { label: "浜ゆ帴浜�", prop: "handoverTo" },
+ { label: "浜ゆ帴浜嬮」", prop: "handoverItems" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.LEAVE]: {
+ label: "璇峰亣鐢宠",
+ approvalType: "leave",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.LEAVE],
+ typeLabels: ["璇峰亣", "璇峰亣鐢宠", "璇峰亣瀹℃壒"],
+ listFields: [
+ { label: "瀹℃壒鍗曞彿", prop: "instanceNo" },
+ { label: "璇峰亣绫诲瀷", prop: "leaveType" },
+ { label: "寮�濮嬫椂闂�", prop: "startTime" },
+ { label: "缁撴潫鏃堕棿", prop: "endTime" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.OVERTIME]: {
+ label: "鍔犵彮鐢宠",
+ approvalType: "overtime",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.OVERTIME],
+ typeLabels: ["鍔犵彮", "鍔犵彮鐢宠", "鍔犵彮瀹℃壒"],
+ listFields: [
+ { label: "瀹℃壒鍗曞彿", prop: "instanceNo" },
+ { label: "鍔犵彮鏃ユ湡", prop: "overtimeDate" },
+ { label: "鏃堕暱(灏忔椂)", prop: "hours" },
+ ],
+ },
+ [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: {
+ label: "宸梾鎶ラ攢",
+ approvalType: "travel_reimburse",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE],
+ typeLabels: ["宸梾", "宸梾鎶ラ攢", "鍑哄樊鎶ラ攢"],
+ listFields: [],
+ },
+ [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: {
+ label: "璐圭敤鎶ラ攢",
+ approvalType: "cost_reimburse",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.COST_REIMBURSE],
+ typeLabels: ["璐圭敤", "璐圭敤鎶ラ攢"],
+ listFields: [],
+ },
+ [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: {
+ label: "浼佷笟鏂伴椈",
+ approvalType: "enterprise_news",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS],
+ typeLabels: ["浼佷笟鏂伴椈", "鏂伴椈", "鏂伴椈鍙戝竷"],
+ listFields: [],
+ },
+};
+
+export function getApprovalModuleConfig(moduleKey) {
+ if (!moduleKey) return null;
+ return APPROVAL_MODULE_REGISTRY[moduleKey] || null;
+}
+
+export function getModuleListBusinessType(moduleKey) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return "";
+ if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType;
+ return APPROVAL_BUSINESS_TYPE[moduleKey] ?? "";
+}
+
+function matchBiz(a, b) {
+ if (a == null || a === "" || b == null || b === "") return false;
+ return a === b || a === Number(b) || Number(a) === b || String(a) === String(b);
+}
+
+export function resolveModuleBusinessType(moduleKey, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return null;
+
+ const fixed = getModuleListBusinessType(moduleKey);
+ if (fixed != null && fixed !== "") return fixed;
+
+ const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+ const hitByLabel = (typeOptions || []).find(opt => {
+ const optLabel = String(opt?.name || opt?.label || "").trim();
+ if (!optLabel) return false;
+ return labels.some(
+ l => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
+ );
+ });
+ if (hitByLabel?.value != null && hitByLabel.value !== "") return hitByLabel.value;
+
+ if (cfg.approvalType) {
+ const hitByValue = (typeOptions || []).find(
+ opt =>
+ matchBiz(opt?.value, cfg.approvalType) || matchBiz(opt?.code, cfg.approvalType)
+ );
+ if (hitByValue?.value != null && hitByValue.value !== "") return hitByValue.value;
+ }
+
+ return null;
+}
+
+export function getModuleMatchingBusinessTypes(moduleKey, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return [];
+
+ const fixed = getModuleListBusinessType(moduleKey);
+ if (fixed != null && fixed !== "") return [fixed];
+
+ const values = new Set();
+ const primary = resolveModuleBusinessType(moduleKey, typeOptions);
+ if (primary != null && primary !== "") values.add(primary);
+
+ const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+ for (const opt of typeOptions || []) {
+ const optLabel = String(opt?.name || opt?.label || "").trim();
+ if (!optLabel) continue;
+ const matched = labels.some(
+ l => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
+ );
+ if (matched && opt.value != null && opt.value !== "") {
+ values.add(opt.value);
+ }
+ }
+ return [...values];
+}
+
+export const PAGE_KEY_TO_MODULE = {
+ "HrManage/regular-apply": APPROVAL_MODULE_KEYS.REGULAR,
+ "HrManage/transfer-apply": APPROVAL_MODULE_KEYS.TRANSFER,
+ "HrManage/work-handover": APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+ "AttendManage/leave-apply": APPROVAL_MODULE_KEYS.LEAVE,
+ "AttendManage/overtime-apply": APPROVAL_MODULE_KEYS.OVERTIME,
+ "ReimburseManage/travel-reimburse": APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE,
+ "ReimburseManage/cost-reimburse": APPROVAL_MODULE_KEYS.COST_REIMBURSE,
+ "EnterpriseNews/news-manage": APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS,
+};
+
+export function getModuleKeyFromPageKey(pageKey) {
+ return PAGE_KEY_TO_MODULE[pageKey] || "";
+}
diff --git a/src/pages/oa/_utils/approvalTemplateType.js b/src/pages/oa/_utils/approvalTemplateType.js
new file mode 100644
index 0000000..e554068
--- /dev/null
+++ b/src/pages/oa/_utils/approvalTemplateType.js
@@ -0,0 +1,178 @@
+import { getTypeEnums } from "@/api/basic/enum.js";
+import { listApprovalTemplateByType } from "@/api/oa/approvalTemplate.js";
+
+/**
+ * GET /approvalTemplate/list/{type} 璺緞鍙傛暟涓� templateType
+ * 1 = 鑷畾涔変笖宸插惎鐢紙涓� businessType 鏃犲叧锛�
+ */
+export const CUSTOM_TEMPLATE_LIST_TYPE = 1;
+
+/** 绯荤粺鍐呯疆妯℃澘锛堜笉鍙垹闄わ紝濉姤椤圭瓑鍙楅檺锛� */
+export const SYSTEM_TEMPLATE_TYPE = 0;
+
+export function isSystemApprovalTemplate(item) {
+ return Number(item?.templateType) === SYSTEM_TEMPLATE_TYPE;
+}
+
+/** 涓氬姟绫诲瀷鏋氫妇鍏滃簳锛坅pproveType锛�1鍏嚭 2璇峰亣 鈥︼級 */
+export const FALLBACK_BUSINESS_TYPE_OPTIONS = [
+ { name: "鍏嚭绠$悊", value: 1 },
+ { name: "璇峰亣绠$悊", value: 2 },
+];
+
+/** 瑙f瀽 TypeEnums 鎺ュ彛杩斿洖锛堝吋瀹� { TypeEnums: [] } 宓屽锛� */
+export function unwrapEnumList(data) {
+ if (Array.isArray(data)) return data;
+ if (!data || typeof data !== "object") return [];
+ if (Array.isArray(data.TypeEnums)) return data.TypeEnums;
+ if (Array.isArray(data.typeEnums)) return data.typeEnums;
+ const nested = Object.values(data).find(v => Array.isArray(v));
+ return nested || [];
+}
+
+/** 瑙f瀽妯℃澘鍒楄〃鎺ュ彛杩斿洖 */
+export function unwrapTemplateList(payload) {
+ const data = payload?.data ?? payload;
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.records)) return data.records;
+ if (Array.isArray(data?.list)) return data.list;
+ if (Array.isArray(data?.rows)) return data.rows;
+ return [];
+}
+
+/** enabled锛�1 / true 涓哄惎鐢紙涓� Web mapEnabledFromApi 涓�鑷达級 */
+export function mapEnabledFromApi(enabled) {
+ if (enabled === undefined || enabled === null) return true;
+ if (enabled === true || enabled === 1) return true;
+ const s = String(enabled).toLowerCase();
+ return s === "1" || s === "true" || s === "yes";
+}
+
+export function filterEnabledTemplates(list) {
+ return (list || []).filter(row => mapEnabledFromApi(row?.enabled));
+}
+
+/** 灏� /basic/enum/TypeEnums 鍝嶅簲瑙勮寖涓� { name, value }[] */
+export function normalizeEnumOptions(data) {
+ return unwrapEnumList(data)
+ .map(item => {
+ const name =
+ item?.name ??
+ item?.label ??
+ item?.text ??
+ item?.dictLabel ??
+ item?.description;
+ const rawValue =
+ item?.value ?? item?.code ?? item?.dictValue ?? item?.key ?? item?.id;
+ if (name == null || rawValue === undefined || rawValue === null) {
+ return null;
+ }
+ const num = Number(rawValue);
+ return {
+ name: String(name),
+ value: Number.isNaN(num) ? rawValue : num,
+ };
+ })
+ .filter(Boolean);
+}
+
+/** 鎷夊彇涓氬姟绫诲瀷鏋氫妇锛圱ypeEnums 鈫� businessType锛� */
+export async function fetchApprovalTemplateTypes() {
+ try {
+ const res = await getTypeEnums();
+ const options = normalizeEnumOptions(res?.data ?? res);
+ return options.length ? options : [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+ } catch {
+ return [...FALLBACK_BUSINESS_TYPE_OPTIONS];
+ }
+}
+
+/** 閫夊彇绗竴涓�屾湁妯℃澘銆嶇殑 Tab 涓嬫爣 */
+export function pickTabIndexWithTemplates(typeOptions, templates) {
+ if (!typeOptions?.length) return 0;
+ for (let i = 0; i < typeOptions.length; i++) {
+ const bt = typeOptions[i]?.value;
+ if (filterTemplatesByBusinessType(templates, bt).length > 0) return i;
+ }
+ return 0;
+}
+
+/** 鎷夊彇宸插惎鐢ㄦā鏉匡紙鑷畾涔� + 绯荤粺鍐呯疆锛屼笌 Web 瀵煎叆閫昏緫涓�鑷达級 */
+export async function fetchEnabledApprovalTemplates() {
+ const [customRes, builtinRes] = await Promise.all([
+ listApprovalTemplateByType(CUSTOM_TEMPLATE_LIST_TYPE),
+ listApprovalTemplateByType(SYSTEM_TEMPLATE_TYPE),
+ ]);
+ const merged = [
+ ...unwrapTemplateList(customRes),
+ ...unwrapTemplateList(builtinRes),
+ ];
+ const byId = new Map();
+ merged.forEach(item => {
+ if (item?.id != null) byId.set(String(item.id), item);
+ });
+ return filterEnabledTemplates([...byId.values()]);
+}
+
+/** businessType 瀹芥澗鍖归厤锛堜笌 Web matchBusinessTypeValue 涓�鑷达級 */
+export function matchBusinessTypeValue(a, b) {
+ if (a == null || a === "" || b == null || b === "") return false;
+ return a === b || a === Number(b) || Number(a) === b || String(a) === String(b);
+}
+
+/** 鎸� businessType 绛涢�夋ā鏉� */
+export function filterTemplatesByBusinessType(templates, businessType) {
+ if (businessType == null || businessType === "") return [];
+ return (templates || []).filter(item =>
+ matchBusinessTypeValue(item.businessType, businessType)
+ );
+}
+
+/** 鎸夊涓� businessType 绛涢�夛紙涓氬姟妯″潡锛� */
+export function filterTemplatesByBusinessTypes(templates, businessTypes = []) {
+ const types = (businessTypes || []).filter(t => t != null && t !== "");
+ if (!types.length) return [];
+ return (templates || []).filter(item =>
+ types.some(t => matchBusinessTypeValue(item.businessType, t))
+ );
+}
+
+/** 榛樿 Tab 涓嬫爣锛堝彲鎸変笟鍔$被鍨� value 鎸囧畾锛岄粯璁ょ涓�椤癸級 */
+export function getDefaultTypeTabIndex(options, defaultBusinessType) {
+ if (!options?.length) return 0;
+ if (defaultBusinessType == null) return 0;
+ const idx = options.findIndex(
+ opt => String(opt.value) === String(defaultBusinessType)
+ );
+ return idx >= 0 ? idx : 0;
+}
+
+/** TypeEnums 涓虹┖鏃讹紝浠庢ā鏉垮垪琛ㄥ弽鎺� Tab */
+export function buildTypeOptionsFromTemplates(templates) {
+ const map = new Map();
+ (templates || []).forEach(item => {
+ const v = item?.businessType;
+ if (v == null || v === "") return;
+ const key = String(v);
+ if (!map.has(key)) {
+ map.set(key, { name: `瀹℃壒绫诲瀷 ${key}`, value: v });
+ }
+ });
+ return [...map.values()];
+}
+
+export function buildTypeLabelMap(options) {
+ const map = {};
+ (options || []).forEach(opt => {
+ map[String(opt.value)] = opt.name;
+ });
+ return map;
+}
+
+/** 鏍规嵁 businessType 鏄剧ず涓氬姟绫诲瀷鍚嶇О */
+export function getTemplateTypeLabel(type, labelMap) {
+ if (type == null || type === "") return "-";
+ return labelMap?.[String(type)] ?? String(type);
+}
+
+export const getBusinessTypeLabel = getTemplateTypeLabel;
diff --git a/src/pages/oa/_utils/approveListUtils.js b/src/pages/oa/_utils/approveListUtils.js
new file mode 100644
index 0000000..538fec9
--- /dev/null
+++ b/src/pages/oa/_utils/approveListUtils.js
@@ -0,0 +1,358 @@
+import { parseTime } from "@/utils/ruoyi";
+import {
+ formatFieldDisplayValue,
+ getFieldOptionLabel,
+ isSelectField,
+ mergeFormConfigForEdit,
+} from "./approvalFormField.js";
+import {
+ appendDotNotationQuery,
+ buildApprovalInstanceSearchDto,
+ formatKnownSelectLabel,
+ resolveInstanceFormPayload,
+ resolveListFieldRawValue,
+} from "./approvalModuleListSearch.js";
+
+export const DETAIL_STORAGE_KEY = "oa_approve_instance_detail_row";
+
+export const INSTANCE_STATUS_TEXT = {
+ PENDING: "杩涜涓�",
+ APPROVED: "宸查�氳繃",
+ REJECTED: "宸查┏鍥�",
+ DRAFT: "鑽夌",
+};
+
+export const INSTANCE_STATUS_TAG = {
+ PENDING: "warning",
+ APPROVED: "success",
+ REJECTED: "error",
+ DRAFT: "info",
+};
+
+export const TASK_STATUS_TEXT = {
+ PENDING: "寰呭鐞�",
+ APPROVED: "宸查�氳繃",
+ REJECTED: "宸查┏鍥�",
+};
+
+export const TASK_STATUS_TAG = {
+ PENDING: "warning",
+ APPROVED: "success",
+ REJECTED: "error",
+};
+
+export function instanceStatusText(status) {
+ return INSTANCE_STATUS_TEXT[status] || status || "-";
+}
+
+export function instanceStatusTagType(status) {
+ return INSTANCE_STATUS_TAG[status] || "info";
+}
+
+export function taskStatusText(status) {
+ const key = String(status || "").toUpperCase();
+ return TASK_STATUS_TEXT[key] || status || "寰呭鐞�";
+}
+
+export function taskStatusTagType(status) {
+ const key = String(status || "").toUpperCase();
+ return TASK_STATUS_TAG[key] || "info";
+}
+
+export function formatDateTime(val) {
+ if (!val) return "-";
+ return parseTime(val, "{y}-{m}-{d} {h}:{i}:{s}") || String(val);
+}
+
+/** 瑙f瀽瀹炰緥涓哄彧璇诲睍绀哄瓧娈碉紙鍚堝苟 formPayload锛屾敮鎸佷紶鏁磋鎴栦粎 formConfig锛� */
+export function resolveInstanceDisplayFields(formConfigOrRow) {
+ const row =
+ formConfigOrRow &&
+ typeof formConfigOrRow === "object" &&
+ (formConfigOrRow.formConfig != null ||
+ formConfigOrRow.formPayload != null ||
+ formConfigOrRow.formFieldDefs != null)
+ ? formConfigOrRow
+ : { formConfig: formConfigOrRow };
+ const { fields } = resolveInstanceFormPayload(row);
+ if (fields.length) return fields.filter(f => f?.key);
+ const merged = mergeFormConfigForEdit("", row.formConfig);
+ return (merged.fields || []).filter(f => f?.key);
+}
+
+export function displayFieldValue(field) {
+ const val = field.value ?? field.defaultValue;
+ if (val === undefined || val === null || val === "") return "-";
+ if (isSelectField(field)) {
+ const fromOptions = getFieldOptionLabel(field, val);
+ if (fromOptions && fromOptions !== "-") return fromOptions;
+ const known = formatKnownSelectLabel(field.key, val);
+ if (known) return known;
+ return String(val);
+ }
+ const known = formatKnownSelectLabel(field?.key, val);
+ if (known) return known;
+ const shown = formatFieldDisplayValue(field, val);
+ return shown || String(val);
+}
+
+const DATETIME_LIST_PROPS = new Set([
+ "startTime",
+ "endTime",
+ "overtimeDate",
+ "applyTime",
+]);
+
+function formatListFieldDisplay(prop, val, field) {
+ if (val === undefined || val === null || val === "") return "-";
+ if (DATETIME_LIST_PROPS.has(prop)) {
+ const shown = formatDateTime(val);
+ if (shown && shown !== "-") return shown;
+ }
+ if (field?.type === "datetimerange") {
+ const shown = formatFieldDisplayValue(field, val);
+ if (shown) return shown;
+ }
+ if (field) return displayFieldValue({ ...field, value: val });
+ const known = formatKnownSelectLabel(prop, val);
+ if (known) return known;
+ return String(val);
+}
+
+/** 瀹℃壒璁板綍 result锛歛pproved | rejected | pending */
+export function mapRecordResult(action) {
+ const s = String(action || "").toUpperCase();
+ if (s === "APPROVED" || s === "APPROVE" || s === "PASS") return "approved";
+ if (s === "REJECTED" || s === "REJECT" || s === "REFUSE") return "rejected";
+ return "pending";
+}
+
+export function recordActionLabel(result) {
+ if (result === "approved") return "閫氳繃";
+ if (result === "rejected") return "椹冲洖";
+ return "寰呭鐞�";
+}
+
+export function mapApprovalRecords(records) {
+ const list = Array.isArray(records) ? records : [];
+ return list.map((r, index) => ({
+ id: r.id ?? index,
+ operatorName: r.approverName || r.operatorName || r.createUserName || "鈥�",
+ result: mapRecordResult(r.approveAction ?? r.action ?? r.status),
+ opinion: r.approveComment || r.comment || r.opinion || "",
+ time: formatDateTime(r.approveTime || r.createTime || r.time),
+ }));
+}
+
+export function getRejectReasonFromRecords(records) {
+ const mapped = mapApprovalRecords(records);
+ const hit = mapped.find(r => r.result === "rejected");
+ return hit?.opinion || "";
+}
+
+/** 鍒楄〃 tasks 鈫� 娴佺▼鑺傜偣锛堜笌 apply 椤佃妭鐐圭粨鏋勬帴杩戯級 */
+export function mapTasksToFlowNodes(tasks) {
+ const list = Array.isArray(tasks) ? tasks : [];
+ if (!list.length) return [];
+
+ const byLevel = new Map();
+ list.forEach(t => {
+ const level = Number(t.levelNo ?? t.taskLevel ?? t.nodeOrder ?? 1);
+ if (!byLevel.has(level)) {
+ byLevel.set(level, {
+ levelNo: level,
+ approveType: t.approveType || "AND",
+ approvers: [],
+ });
+ }
+ const node = byLevel.get(level);
+ node.approvers.push({
+ approverName: t.approverName || "鈥�",
+ taskStatus: t.taskStatus ?? t.status,
+ approveComment: t.approveComment,
+ approveTime: t.approveTime,
+ });
+ if (t.approveType) node.approveType = t.approveType;
+ });
+
+ return [...byLevel.entries()]
+ .sort(([a], [b]) => a - b)
+ .map(([, node]) => node);
+}
+
+/** 缁勮瀹℃壒鎻愪氦 DTO锛堜笌 Web buildApproveInstanceDto 涓�鑷达級 */
+export function buildApproveInstanceDto(id, uiResult, comment) {
+ const opinion = (comment || "").trim();
+ return {
+ id,
+ approveAction: uiResult === "rejected" ? "REJECTED" : "APPROVED",
+ approveComment: opinion || (uiResult === "approved" ? "鍚屾剰" : ""),
+ };
+}
+
+/** 鏄惁鏈汉鍙戣捣鐨勫鎵� */
+export function isOwnApplication(item, userStore) {
+ const uid = userStore?.id;
+ if (item?.applicantId != null && uid != null && uid !== "") {
+ return String(item.applicantId) === String(uid);
+ }
+ const loginName = userStore?.nickName || userStore?.name;
+ if (loginName && item?.applicantName) {
+ return String(item.applicantName).trim() === String(loginName).trim();
+ }
+ return false;
+}
+
+/** 浠呰繘琛屼腑涓旀湰浜哄彂璧锋椂鍙紪杈� */
+export function canModifyInstance(item, userStore) {
+ return item?.status === "PENDING" && isOwnApplication(item, userStore);
+}
+
+/** 寰呭綋鍓嶇敤鎴峰鎵� */
+export function canApproveInstance(item) {
+ return Boolean(item?.isApprove) && item?.status === "PENDING";
+}
+
+export function stashInstanceRow(item) {
+ if (item) {
+ uni.setStorageSync(DETAIL_STORAGE_KEY, item);
+ }
+}
+
+export function loadInstanceRow(id) {
+ const row = uni.getStorageSync(DETAIL_STORAGE_KEY);
+ if (!row || String(row.id) !== String(id)) return null;
+ return row;
+}
+
+export const EDIT_STORAGE_KEY = "oa_approve_instance_edit_row";
+
+/** 涓氬姟鐢宠椤电姸鎬侊細杩涜涓�/宸插畬鎴愪笉鍙慨鏀癸紙涓� Web canEditBusinessInstanceRow 涓�鑷达級 */
+export function normalizeApprovalStatusKey(v) {
+ if (v == null || v === "") return "pending";
+ const upper = String(v).trim().toUpperCase();
+ if (upper === "DRAFT") return "draft";
+ if (upper === "APPROVED" || upper === "PASS") return "approved";
+ if (upper === "REJECTED" || upper === "REJECT" || upper === "REFUSE") {
+ return "rejected";
+ }
+ if (upper === "CANCELLED" || upper === "CANCEL") return "cancelled";
+ if (upper === "PENDING" || upper === "IN_PROGRESS") return "pending";
+ const lower = String(v).trim().toLowerCase();
+ if (["draft", "pending", "approved", "rejected", "cancelled"].includes(lower)) {
+ return lower;
+ }
+ return "pending";
+}
+
+export function canEditBusinessInstanceRow(row) {
+ const key = normalizeApprovalStatusKey(row?.status ?? row?.approvalStatus);
+ return key !== "pending" && key !== "approved";
+}
+
+export function businessStatusText(status) {
+ const key = normalizeApprovalStatusKey(status);
+ if (key === "draft") return "鑽夌";
+ if (key === "pending") return "杩涜涓�";
+ if (key === "approved") return "宸插畬鎴�";
+ if (key === "rejected") return "宸查┏鍥�";
+ if (key === "cancelled") return "宸叉挙閿�";
+ return instanceStatusText(status);
+}
+
+export function businessStatusTagType(status) {
+ const key = normalizeApprovalStatusKey(status);
+ if (key === "approved") return "success";
+ if (key === "rejected") return "error";
+ if (key === "draft" || key === "cancelled") return "info";
+ return "warning";
+}
+
+/** OA 鍒楄〃鑷畾涔夌姸鎬佽鏍� class */
+export function businessStatusClass(status) {
+ return `status-${normalizeApprovalStatusKey(status)}`;
+}
+
+/**
+ * 涓� Web buildApprovalInstanceListParams 涓�鑷�
+ */
+export function buildInstanceListParams({
+ page,
+ businessType,
+ extraDto = {},
+ searchForm,
+}) {
+ const dto = buildApprovalInstanceSearchDto(searchForm, extraDto);
+ const bizType = businessType ?? searchForm?.businessType;
+ if (bizType != null && bizType !== "") {
+ dto.businessType = bizType;
+ }
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ "page.current": page.current,
+ "page.size": page.size,
+ ...dto,
+ };
+ appendDotNotationQuery(params, "approvalInstanceDto", dto);
+ return params;
+}
+
+export function unwrapInstancePage(res) {
+ const data = res?.data ?? res;
+ return {
+ records: Array.isArray(data?.records) ? data.records : [],
+ total: Number(data?.total ?? 0),
+ };
+}
+
+/** 浠庡疄渚嬭鎻愬彇鍒楄〃灞曠ず瀛楁锛坙abel + value锛屽惈 formPayload锛� */
+export function buildFormDisplayRows(row, listFields = []) {
+ const { fields, formPayload } = resolveInstanceFormPayload(row);
+ const fieldByKey = new Map((fields || []).map(f => [f.key, f]));
+ const rows = [];
+ const defs = listFields || [];
+
+ if (defs.length) {
+ defs.forEach(def => {
+ if (!def?.prop) return;
+ const prop = def.prop;
+ const hit = fieldByKey.get(prop);
+ const raw = resolveListFieldRawValue(prop, row, fields, formPayload);
+ rows.push({
+ label: def.label || hit?.label || prop,
+ value: formatListFieldDisplay(prop, raw, hit),
+ });
+ });
+ } else {
+ fields.slice(0, 3).forEach(f => {
+ rows.push({ label: f.label, value: displayFieldValue(f) });
+ });
+ }
+ return rows;
+}
+
+/** 鍒楄〃琛屽寮猴紙淇濈暀鍘熷瀛楁渚涜鎯�/缂栬緫锛� */
+export function mapInstanceListRow(row, listFields = []) {
+ if (!row) return {};
+ const displayRows = buildFormDisplayRows(row, listFields);
+ const extra = {};
+ const { fields, formPayload } = resolveInstanceFormPayload(row);
+ (listFields || []).forEach(def => {
+ if (!def?.prop) return;
+ const hit = fields.find(f => f.key === def.prop);
+ const raw = resolveListFieldRawValue(def.prop, row, fields, formPayload);
+ extra[def.prop] = formatListFieldDisplay(def.prop, raw, hit);
+ });
+ return {
+ ...row,
+ approvalStatus: normalizeApprovalStatusKey(row.status),
+ summary: row.title || row.templateName || "",
+ createTime: formatDateTime(row.applyTime || row.createTime),
+ displayRows,
+ formPayload,
+ formFieldDefs: fields,
+ ...extra,
+ };
+}
diff --git a/src/pages/oa/_utils/finReimbursementMappers.js b/src/pages/oa/_utils/finReimbursementMappers.js
new file mode 100644
index 0000000..9c9a511
--- /dev/null
+++ b/src/pages/oa/_utils/finReimbursementMappers.js
@@ -0,0 +1,1058 @@
+import dayjs from "dayjs";
+import {
+ deleteFinReimbursement,
+ getFinReimbursementDetail,
+ persistFinReimbursement,
+} from "@/api/oa/finReimbursement.js";
+import { APPROVAL_MODULE_KEYS } from "./approvalModuleRegistry.js";
+import { businessStatusClass, normalizeApprovalStatusKey } from "./approveListUtils.js";
+import {
+ EXPENSE_CATEGORY_OPTIONS,
+ expenseTypeToCategory,
+} from "../ReimburseManage/_utils/costReimburseUtils.js";
+import { EXPENSE_SUBJECT_OPTIONS as TRAVEL_EXPENSE_SUBJECTS } from "../ReimburseManage/_utils/travelReimburseUtils.js";
+import { EXPENSE_SUBJECT_OPTIONS as COST_EXPENSE_SUBJECTS } from "../ReimburseManage/_utils/costReimburseUtils.js";
+import { resolveExpenseSubjectLabel } from "../ReimburseManage/_utils/expenseDetailDisplay.js";
+import { mapTasksToFlowNodes } from "./approveListUtils.js";
+import { applyFinReimbursementDetailEnrichment } from "../ReimburseManage/_utils/finReimbursementDetailExtras.js";
+
+export const FIN_REIMBURSEMENT_TYPE = {
+ TRAVEL: "1",
+ COST: "2",
+};
+
+const REIMBURSEMENT_TYPE_LABEL = {
+ [FIN_REIMBURSEMENT_TYPE.TRAVEL]: "宸梾鎶ラ攢",
+ [FIN_REIMBURSEMENT_TYPE.COST]: "璐圭敤鎶ラ攢",
+};
+
+/** 褰掍竴鍖栨姤閿�绫诲瀷锛�1-宸梾锛�2-璐圭敤 */
+export function normalizeReimbursementType(val) {
+ const s = String(val ?? "").trim();
+ if (s === "1" || s === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+ return FIN_REIMBURSEMENT_TYPE.TRAVEL;
+ }
+ if (s === "2" || s === FIN_REIMBURSEMENT_TYPE.COST) {
+ return FIN_REIMBURSEMENT_TYPE.COST;
+ }
+ return "";
+}
+
+export function reimbursementTypeLabel(type) {
+ return REIMBURSEMENT_TYPE_LABEL[normalizeReimbursementType(type)] || "鈥�";
+}
+
+export function getModuleKeyByReimbursementType(type) {
+ const t = normalizeReimbursementType(type);
+ if (t === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+ return APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE;
+ }
+ if (t === FIN_REIMBURSEMENT_TYPE.COST) {
+ return APPROVAL_MODULE_KEYS.COST_REIMBURSE;
+ }
+ return "";
+}
+
+/** 浼樺厛鎺ュ彛 reimbursementType锛屽叾娆¢〉闈� moduleKey / 鍏ュ弬 */
+export function resolveReimbursementType(raw, fallback) {
+ const fromApi = normalizeReimbursementType(raw?.reimbursementType);
+ if (fromApi) return fromApi;
+ return (
+ normalizeReimbursementType(fallback) ||
+ getReimbursementTypeByModuleKey(fallback) ||
+ ""
+ );
+}
+
+export function isTravelReimbursementType(type) {
+ return resolveReimbursementType({ reimbursementType: type }, type) === FIN_REIMBURSEMENT_TYPE.TRAVEL;
+}
+
+export function filterRowsByReimbursementType(rows, expectedType) {
+ const expected = normalizeReimbursementType(expectedType);
+ if (!expected) return rows || [];
+ return (rows || []).filter(row => {
+ const t = resolveReimbursementType(row, expected);
+ return t === expected;
+ });
+}
+
+const BILL_STATUS_LABEL = {
+ DRAFT: "鑽夌",
+ IN_APPROVAL: "瀹℃壒涓�",
+ APPROVED: "瀹℃壒閫氳繃",
+ REJECTED: "瀹℃壒椹冲洖",
+ WITHDRAWN: "宸叉挙鍥�",
+ PAID: "宸蹭粯娆�",
+};
+
+export function getReimbursementTypeByModuleKey(moduleKey) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE) {
+ return FIN_REIMBURSEMENT_TYPE.TRAVEL;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE) {
+ return FIN_REIMBURSEMENT_TYPE.COST;
+ }
+ return "";
+}
+
+export function unwrapFinReimbursementPage(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") {
+ return { records: [], total: 0 };
+ }
+ if (Array.isArray(data.records)) {
+ return { records: data.records, total: Number(data.total ?? 0) };
+ }
+ const nested = data.data;
+ if (nested && typeof nested === "object" && Array.isArray(nested.records)) {
+ return { records: nested.records, total: Number(nested.total ?? 0) };
+ }
+ return { records: [], total: 0 };
+}
+
+/** 璇︽儏鎺ュ彛 data 瑙e寘 */
+export function unwrapFinReimbursementDetail(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") return {};
+ if (data.billNo != null || data.id != null || data.reimbursementType != null) {
+ return data;
+ }
+ const nested = data.data;
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
+ return nested;
+ }
+ if (data.finReimbursementDto && typeof data.finReimbursementDto === "object") {
+ return data.finReimbursementDto;
+ }
+ return data;
+}
+
+export function mapBillStatusToApprovalKey(billStatus) {
+ const upper = String(billStatus ?? "").trim().toUpperCase();
+ if (upper === "DRAFT") return "draft";
+ if (upper === "IN_APPROVAL") return "pending";
+ if (upper === "APPROVED") return "approved";
+ if (upper === "REJECTED") return "rejected";
+ if (upper === "WITHDRAWN") return "cancelled";
+ if (upper === "PAID") return "approved";
+ return normalizeApprovalStatusKey(billStatus);
+}
+
+export function billStatusLabel(billStatus) {
+ const upper = String(billStatus ?? "").trim().toUpperCase();
+ if (BILL_STATUS_LABEL[upper]) return BILL_STATUS_LABEL[upper];
+ const key = mapBillStatusToApprovalKey(billStatus);
+ if (key === "draft") return "鑽夌";
+ if (key === "approved") return "宸插畬鎴�";
+ if (key === "rejected") return "宸查┏鍥�";
+ if (key === "cancelled") return "宸叉挙鍥�";
+ return "杩涜涓�";
+}
+
+export function billStatusCssClass(item) {
+ return businessStatusClass(
+ mapBillStatusToApprovalKey(item?.billStatus ?? item?.status)
+ );
+}
+
+function pickApplicantQuery(searchForm = {}) {
+ const kw = (searchForm.applicantKeyword || "").trim();
+ if (!kw) return {};
+ const out = { applicantName: kw };
+ if (!/[\u4e00-\u9fa5]/.test(kw)) {
+ out.applicantCode = kw;
+ }
+ return out;
+}
+
+export function hasActiveReimbursementSearch(searchForm = {}) {
+ return Boolean((searchForm?.applicantKeyword || "").trim());
+}
+
+export function filterReimbursementRowsBySearch(rows, searchForm = {}) {
+ const list = Array.isArray(rows) ? rows : [];
+ const kw = (searchForm?.applicantKeyword || "").trim().toLowerCase();
+ if (!kw) return list;
+
+ return list.filter((row) => {
+ const parts = [
+ row.applicantName,
+ row.employeeName,
+ row.applicantNo,
+ row.applicantCode,
+ row.employeeNo,
+ ]
+ .filter((v) => v != null && v !== "")
+ .map((v) => String(v).toLowerCase());
+ return parts.some((p) => p.includes(kw));
+ });
+}
+
+/** 鎵佸钩鍖栦负 Spring GET 鍙粦瀹氱殑 query锛坒inReimbursementDto.xxx锛屽嬁鐢ㄦ柟鎷彿锛� */
+function appendDotNotationQuery(target, prefix, fields) {
+ if (!fields || typeof fields !== "object") return;
+ for (const [key, value] of Object.entries(fields)) {
+ if (value == null || value === "") continue;
+ target[`${prefix}.${key}`] = value;
+ }
+}
+
+export function buildFinReimbursementListParams({
+ page,
+ searchForm,
+ reimbursementType,
+ extraDto = {},
+}) {
+ const dto = {
+ reimbursementType,
+ ...pickApplicantQuery(searchForm),
+ ...(extraDto && typeof extraDto === "object" ? extraDto : {}),
+ };
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ "page.current": page.current,
+ "page.size": page.size,
+ ...dto,
+ };
+ appendDotNotationQuery(params, "finReimbursementDto", dto);
+ return params;
+}
+
+function pickTravelField(obj, keys) {
+ if (!obj || typeof obj !== "object") return "";
+ for (const key of keys) {
+ const v = obj[key];
+ if (v != null && v !== "") return v;
+ }
+ return "";
+}
+
+/** 鍏煎 list/detail 澶氱宸梾瀛愬璞$粨鏋� */
+export function pickTravelFromRow(row) {
+ if (!row || typeof row !== "object") return {};
+ const nested =
+ (row.travel && typeof row.travel === "object" ? row.travel : null) ||
+ row.finReimbursementTravel ||
+ row.finReimbursementTravelDto ||
+ row.travelDto ||
+ row.travelVO ||
+ {};
+ const src =
+ nested && typeof nested === "object" && Object.keys(nested).length
+ ? nested
+ : row;
+ return {
+ startTime: pickTravelField(src, [
+ "startTime",
+ "travelStartTime",
+ "startDate",
+ "travelStartDate",
+ "departureTime",
+ ]),
+ endTime: pickTravelField(src, [
+ "endTime",
+ "travelEndTime",
+ "endDate",
+ "travelEndDate",
+ "returnTime",
+ ]),
+ travelDays: src.travelDays,
+ departureCity: pickTravelField(src, [
+ "departureCity",
+ "departurePlace",
+ "departure",
+ ]),
+ destinationCity: pickTravelField(src, [
+ "destinationCity",
+ "destination",
+ "destinationPlace",
+ ]),
+ hotelStandard: src.hotelStandard,
+ lodgingDays: src.lodgingDays ?? src.hotelDays,
+ mealAllowance: src.mealAllowance ?? src.livingSubsidy,
+ transportAllowance: src.transportAllowance ?? src.transportSubsidy,
+ lodgingLimit: src.lodgingLimit,
+ withinStandard: src.withinStandard,
+ standardTag: src.standardTag || "",
+ id: src.id,
+ reimbursementId: src.reimbursementId,
+ };
+}
+
+export function formatReimbursementDateTime(val) {
+ if (val == null || val === "") return "";
+ const d = dayjs(val);
+ if (!d.isValid()) return String(val);
+ const raw = String(val);
+ const hasTime = raw.includes("T") || /:\d{2}/.test(raw);
+ return hasTime ? d.format("YYYY-MM-DD HH:mm:ss") : d.format("YYYY-MM-DD");
+}
+
+export function mapFinReimbursementFromApi(row, { reimbursementType, moduleKey } = {}) {
+ if (!row) return {};
+ const type = resolveReimbursementType(
+ row,
+ reimbursementType || getReimbursementTypeByModuleKey(moduleKey)
+ );
+ const isTravel = type === FIN_REIMBURSEMENT_TYPE.TRAVEL;
+ const travel = isTravel ? pickTravelFromRow(row) : {};
+ const apiNodes = resolveRowApiNodes(row);
+ const approvalFlowNodes = mapNodesToFormFlow(apiNodes);
+ const flowSummary = formatApprovalFlowSummary({
+ ...row,
+ nodes: apiNodes,
+ approvalFlowNodes,
+ });
+ const instanceId = row.approvalInstanceId ?? row.id;
+
+ return {
+ ...row,
+ reimbursementId: row.id,
+ id: instanceId,
+ approvalInstanceId: row.approvalInstanceId,
+ instanceNo: row.billNo || "",
+ billNo: row.billNo || "",
+ reimbursementType: type,
+ reimbursementTypeLabel: reimbursementTypeLabel(type),
+ moduleKey: getModuleKeyByReimbursementType(type),
+ applicantNo: row.applicantCode || "",
+ applicantCode: row.applicantCode || "",
+ applicantName: row.applicantName || "",
+ reason: row.reason || "",
+ expenseType: row.expenseType || "",
+ applyAmount: row.applyAmount,
+ billStatus: row.billStatus,
+ status: row.billStatus,
+ approvalStatus: mapBillStatusToApprovalKey(row.billStatus),
+ title: row.reason || row.billNo || "",
+ summary: row.reason || row.billNo || "",
+ createTime: formatReimbursementDateTime(row.createTime),
+ departurePlace: travel.departureCity || "",
+ destination: travel.destinationCity || "",
+ travelStartTime: formatReimbursementDateTime(travel.startTime),
+ travelEndTime: formatReimbursementDateTime(travel.endTime),
+ travel,
+ details: row.details || [],
+ nodes: apiNodes,
+ flowNodes: apiNodes,
+ approvalFlowNodes,
+ approvalFlowSummary: flowSummary,
+ displayRows: buildFinReimbursementDisplayRows(
+ {
+ billNo: row.billNo,
+ applyAmount: row.applyAmount,
+ billStatus: row.billStatus,
+ departurePlace: travel.departureCity,
+ destination: travel.destinationCity,
+ expenseType: row.expenseType,
+ reason: row.reason,
+ approvalFlowSummary: flowSummary,
+ },
+ type
+ ),
+ };
+}
+
+export function buildFinReimbursementDisplayRows(item, reimbursementType) {
+ const type = normalizeReimbursementType(reimbursementType);
+ const isTravel = type === FIN_REIMBURSEMENT_TYPE.TRAVEL;
+ const rows = [
+ { label: "鎶ラ攢鍗曞彿", value: item.billNo },
+ {
+ label: "鐢宠閲戦",
+ value: item.applyAmount != null ? `${item.applyAmount} 鍏僠 : "",
+ },
+ { label: "鍗曟嵁鐘舵��", value: billStatusLabel(item.billStatus) },
+ ];
+ if (isTravel) {
+ rows.splice(
+ 1,
+ 0,
+ { label: "鍑哄樊鍦�", value: item.departurePlace },
+ { label: "鐩殑鍦�", value: item.destination }
+ );
+ } else {
+ rows.splice(1, 0, { label: "璐圭敤绫诲瀷", value: item.expenseType });
+ }
+ if (item.reason) {
+ rows.push({ label: "鎶ラ攢鍘熷洜", value: item.reason });
+ }
+ if (item.approvalFlowSummary && item.approvalFlowSummary !== "鈥�") {
+ rows.push({ label: "瀹℃壒娴佺▼", value: item.approvalFlowSummary });
+ }
+ return rows;
+}
+
+/** 淇敼鍦烘櫙蹇呴』甯︿富閿� ID锛堜笌 Web 涓�鑷达級 */
+export function validateReimbursementPersistDto(dto, isEdit) {
+ if (!isEdit) return { ok: true };
+ if (dto?.id != null && dto.id !== "") return { ok: true };
+ return { ok: false, message: "鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID" };
+}
+
+export { deleteFinReimbursement, getFinReimbursementDetail, persistFinReimbursement };
+
+/** 鍒楄〃琛屼富閿紙鍒犻櫎/淇敼鐢� fin_reimbursement.id锛屽嬁鐢� item.id 瀹℃壒瀹炰緥 ID锛� */
+export function resolveReimbursementDeleteId(row) {
+ const raw = row?.reimbursementId;
+ if (raw == null || raw === "" || String(raw).startsWith("local_")) {
+ return undefined;
+ }
+ const n = Number(raw);
+ return Number.isNaN(n) ? raw : n;
+}
+
+/** 鏄惁鍏佽鍒犻櫎锛堝鎵逛腑銆佸凡閫氳繃銆佸凡浠樻涓嶅彲鍒狅級 */
+export function canDeleteReimbursementRow(row) {
+ const upper = String(row?.billStatus ?? row?.status ?? "").trim().toUpperCase();
+ if (upper === "PAID") return false;
+ const key = mapBillStatusToApprovalKey(
+ row?.billStatus ?? row?.approvalStatus ?? row?.status
+ );
+ return key !== "pending" && key !== "approved";
+}
+
+export function canEditReimbursementRow(row) {
+ return canDeleteReimbursementRow(row);
+}
+
+/** 鎷夊彇鎶ラ攢璇︽儏锛堝惈鏄庣粏銆佸鎵硅妭鐐癸紝涓� Web mapFinReimbursementDetailRow 涓�鑷达級 */
+export async function fetchFinReimbursementListItemDetail(item, reimbursementTypeOrModuleKey) {
+ const id = resolveReimbursementDeleteId(item);
+ if (id == null) {
+ throw new Error("missing reimbursement id");
+ }
+ const res = await getFinReimbursementDetail(id);
+ const raw = unwrapFinReimbursementDetail(res);
+ const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey);
+ const row = mapFinReimbursementDetailRow(raw, type);
+ return {
+ ...row,
+ reimbursementType: type,
+ reimbursementTypeLabel: reimbursementTypeLabel(type),
+ moduleKey: getModuleKeyByReimbursementType(type),
+ displayRows: buildFinReimbursementDisplayRows(
+ {
+ billNo: row.billNo || row.reimburseNo,
+ applyAmount: row.applyAmount,
+ billStatus: row.billStatus,
+ departurePlace: row.departurePlace,
+ destination: row.destination,
+ expenseType: row.expenseCategory || row.expenseType,
+ reason: row.reimburseReason || row.reason,
+ },
+ type
+ ),
+ };
+}
+
+function toNumber(val) {
+ if (val == null || val === "") return undefined;
+ const n = Number(val);
+ return Number.isNaN(n) ? undefined : n;
+}
+
+function mapSignModeToApi(signMode) {
+ return signMode === "or_sign" ? "OR" : "AND";
+}
+
+function expenseSubjectToCategory(subject) {
+ const hit =
+ TRAVEL_EXPENSE_SUBJECTS.find(x => x.value === subject) ||
+ COST_EXPENSE_SUBJECTS.find(x => x.value === subject);
+ return hit?.label || subject || "";
+}
+
+function mapDetailRowFromApi(d, reimbursementType) {
+ const type = normalizeReimbursementType(reimbursementType);
+ const raw = d.expenseCategory ?? d.expenseSubject ?? "";
+ const opts =
+ type === FIN_REIMBURSEMENT_TYPE.TRAVEL
+ ? TRAVEL_EXPENSE_SUBJECTS
+ : COST_EXPENSE_SUBJECTS;
+ const label = resolveExpenseSubjectLabel(raw, {
+ isTravel: type === FIN_REIMBURSEMENT_TYPE.TRAVEL,
+ subjectOptions: opts,
+ });
+ const hit = opts.find(x => x.value === raw || x.label === raw || x.label === label);
+ return {
+ ...d,
+ expenseSubject: hit?.value || raw,
+ };
+}
+
+function expenseCategoryToType(category) {
+ const hit = EXPENSE_CATEGORY_OPTIONS.find(x => x.value === category);
+ return hit?.label || category || "";
+}
+
+export function resolveRowApiNodes(row) {
+ if (!row || typeof row !== "object") return [];
+ const list =
+ row.nodes ||
+ row.flowNodes ||
+ row.approveNodes ||
+ row.finReimbursementNodes ||
+ row.nodeList ||
+ [];
+ return Array.isArray(list) ? list : [];
+}
+
+function sortFlowNodesByLevel(nodes = []) {
+ return [...(Array.isArray(nodes) ? nodes : [])].sort((a, b) => {
+ const la = Number(a?.levelNo ?? a?.nodeOrder ?? a?.sortOrder ?? 0);
+ const lb = Number(b?.levelNo ?? b?.nodeOrder ?? b?.sortOrder ?? 0);
+ return la - lb;
+ });
+}
+
+function formatApiNodeApproverLabel(node, index) {
+ if (!node || typeof node !== "object") return "";
+ const approvers = Array.isArray(node.approvers) ? node.approvers : [];
+ const names = approvers
+ .map(a => (a?.approverName || "").trim())
+ .filter(Boolean);
+ if (names.length) return names.join("/");
+ return (node.approverName || "").trim() || `鑺傜偣${index + 1}`;
+}
+
+/** 鎺ュ彛 nodes 鈫� 椤甸潰瀹℃壒娴� */
+export function mapNodesToFormFlow(nodes = []) {
+ return sortFlowNodesByLevel(nodes).map((n, i) => {
+ const approvers = Array.isArray(n.approvers) ? n.approvers : [];
+ const first = approvers[0] || null;
+ const names = approvers
+ .map(a => (a?.approverName || "").trim())
+ .filter(Boolean);
+ return {
+ ...n,
+ nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1,
+ signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign",
+ approverId:
+ toNumber(first?.approverId ?? n.approverId) ??
+ first?.approverId ??
+ n.approverId ??
+ "",
+ approverName:
+ names.join("銆�") || first?.approverName || n.approverName || "",
+ nodeStatus: n.nodeStatus,
+ };
+ });
+}
+
+function formatTasksToFlowSummary(tasks = []) {
+ const list = sortFlowNodesByLevel(
+ (Array.isArray(tasks) ? tasks : []).map((t, i) => ({
+ levelNo: t.levelNo ?? t.taskLevel ?? i + 1,
+ approverName:
+ (t.approverName || t.operatorName || t.createUserName || "").trim() ||
+ "",
+ }))
+ );
+ const parts = list.map(t => t.approverName).filter(Boolean);
+ return parts.length ? parts.join(" 鈫� ") : "";
+}
+
+function buildApprovalFlowSummaryForRow(row) {
+ const apiNodes = sortFlowNodesByLevel(resolveRowApiNodes(row));
+ let flowNodes =
+ row?.approvalFlowNodes?.length > 0
+ ? sortFlowNodesByLevel(row.approvalFlowNodes)
+ : mapNodesToFormFlow(apiNodes);
+
+ if (!flowNodes.length && apiNodes.length) {
+ const line = apiNodes
+ .map((n, i) => formatApiNodeApproverLabel(n, i))
+ .filter(Boolean)
+ .join(" 鈫� ");
+ if (line) return line;
+ }
+
+ if (!flowNodes.length) {
+ const fromTasks = formatTasksToFlowSummary(row?.tasks);
+ if (fromTasks) return fromTasks;
+ return "鈥�";
+ }
+
+ return flowNodes
+ .map((n, i) => {
+ const name = (n.approverName || "").trim() || `鑺傜偣${i + 1}`;
+ if (n.nodeStatus === "finish") return `${name}鉁揱;
+ if (n.nodeStatus === "error") return `${name}鉁梎;
+ if (n.nodeStatus === "process") return `${name}鈥;
+ return name;
+ })
+ .join(" 鈫� ");
+}
+
+export function formatApprovalFlowSummary(row) {
+ return buildApprovalFlowSummaryForRow(row);
+}
+
+/** listPage 甯镐笉甯﹀畬鏁� nodes锛屽垪琛ㄥ姞杞藉悗缁熶竴鎷夎鎯呰ˉ鍏ㄥ绾у鎵规祦绋� */
+export async function enrichReimbursementListRowsWithApprovalFlow(
+ rows,
+ reimbursementType
+) {
+ const list = Array.isArray(rows) ? rows : [];
+ if (!list.length) return list;
+
+ const needIds = list
+ .map(r => resolveReimbursementDeleteId(r))
+ .filter(id => id != null);
+
+ if (!needIds.length) return list;
+
+ const detailById = new Map();
+ await Promise.all(
+ needIds.map(async id => {
+ try {
+ const res = await getFinReimbursementDetail(id);
+ detailById.set(String(id), unwrapFinReimbursementDetail(res));
+ } catch {
+ /* ignore */
+ }
+ })
+ );
+
+ const mapRow =
+ reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL
+ ? mapTravelReimbursementRow
+ : mapCostReimbursementRow;
+
+ return list.map(row => {
+ const id = resolveReimbursementDeleteId(row);
+ const detail = id != null ? detailById.get(String(id)) : null;
+ if (!detail) return row;
+ return mapRow({
+ ...row,
+ ...detail,
+ id: row.id ?? detail.id,
+ reimbursementId: row.reimbursementId ?? row.id ?? detail.id,
+ });
+ });
+}
+
+/** 琛ㄥ崟涓婄殑瀹℃壒娴侊紙鍏煎 approvalFlowNodes / nodes / flowNodes锛� */
+export function resolveFormApprovalFlowNodes(form) {
+ const list =
+ form?.approvalFlowNodes ?? form?.nodes ?? form?.flowNodes ?? [];
+ return Array.isArray(list) ? list : [];
+}
+
+/** 椤甸潰瀹℃壒鑺傜偣 鈫� 鎺ュ彛 nodes */
+export function mapApprovalFlowNodesToApi(nodes = [], templateId) {
+ const list = Array.isArray(nodes) ? nodes : [];
+ return list
+ .map((n, i) => {
+ let approvers = [];
+ if (Array.isArray(n.approvers) && n.approvers.length) {
+ approvers = n.approvers
+ .filter(a => a?.approverId != null && a.approverId !== "")
+ .map((a, idx) => {
+ const item = {
+ approverId: toNumber(a.approverId) ?? a.approverId,
+ approverName: a.approverName || "",
+ sortNo: a.sortNo ?? idx + 1,
+ };
+ if (a.id != null) item.id = a.id;
+ if (a.nodeId != null) item.nodeId = a.nodeId;
+ if (a.templateId != null) item.templateId = a.templateId;
+ else if (templateId != null) item.templateId = templateId;
+ if (a.roleKey) item.roleKey = a.roleKey;
+ return item;
+ });
+ } else if (n.approverId != null && n.approverId !== "") {
+ const item = {
+ approverId: toNumber(n.approverId) ?? n.approverId,
+ approverName: n.approverName || "",
+ sortNo: 1,
+ };
+ if (n.roleKey) item.roleKey = n.roleKey;
+ approvers = [item];
+ }
+ if (!approvers.length) return null;
+ const node = {
+ levelNo: n.levelNo ?? n.nodeOrder ?? n.sortOrder ?? i + 1,
+ approveType: n.approveType || mapSignModeToApi(n.signMode),
+ approvers,
+ };
+ if (n.id != null) node.id = n.id;
+ if (n.templateId != null) node.templateId = n.templateId;
+ else if (templateId != null) node.templateId = templateId;
+ if (n.roleKey) node.roleKey = n.roleKey;
+ return node;
+ })
+ .filter(Boolean);
+}
+
+/** 淇濆瓨鍓嶆牎楠� nodes 宸查厤缃� */
+export function validateReimbursementApprovalNodes(dto) {
+ if (Array.isArray(dto?.nodes) && dto.nodes.length > 0) {
+ return { ok: true };
+ }
+ return { ok: false, message: "璇烽厤缃鎵规祦绋嬪苟閫夋嫨瀹℃壒浜�" };
+}
+
+function mapDetailsToApi(details = []) {
+ return (details || []).map((d, i) => {
+ const item = {
+ rowNo: d.rowNo ?? i + 1,
+ invoiceDate: d.invoiceDate || undefined,
+ expenseCategory: expenseSubjectToCategory(d.expenseSubject ?? d.expenseCategory),
+ amount: toNumber(d.amount),
+ description: d.description || "",
+ invoiceNo: d.invoiceNo || undefined,
+ invoiceType: d.invoiceType || undefined,
+ invoiceAmount: toNumber(d.invoiceAmount),
+ taxRate: toNumber(d.taxRate),
+ taxAmount: toNumber(d.taxAmount),
+ remark: d.remark || undefined,
+ };
+ if (d.id != null && !String(d.id).startsWith("ed_")) {
+ item.id = toNumber(d.id) ?? d.id;
+ }
+ if (d.reimbursementId != null) item.reimbursementId = toNumber(d.reimbursementId);
+ return item;
+ });
+}
+
+function sumDetailAmount(details = []) {
+ const sum = (details || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
+ return Math.round(sum * 100) / 100;
+}
+
+/** 琛ㄥ崟闄勪欢鍒楄〃锛堝吋瀹瑰绉嶅瓧娈靛悕锛� */
+export function resolveFormAttachmentList(form) {
+ const list =
+ form?.attachmentList ??
+ form?.storageBlobDTOs ??
+ form?.storageBlobVOList ??
+ form?.invoiceAttachments ??
+ [];
+ return Array.isArray(list) ? list : [];
+}
+
+/** 椤甸潰闄勪欢 鈫� 淇濆瓨 DTO锛坰torageBlobVOList / storageBlobDTOs锛� */
+export function mapFormAttachmentsToApi(list = [], reimbursementId) {
+ const rid =
+ reimbursementId != null
+ ? toNumber(reimbursementId) ?? reimbursementId
+ : undefined;
+
+ return (list || [])
+ .map((item, i) => {
+ if (!item) return null;
+ const url =
+ item.url ||
+ item.fileUrl ||
+ item.downloadUrl ||
+ item.downloadURL ||
+ item.previewUrl ||
+ item.previewURL ||
+ item.link ||
+ "";
+ const name =
+ item.fileName ||
+ item.originalFilename ||
+ item.originalFileName ||
+ item.blobName ||
+ item.name ||
+ `闄勪欢${i + 1}`;
+
+ const idRaw = item.id ?? item.blobId;
+ const isTempId =
+ idRaw != null &&
+ /^(inv_|att_|ed_|local_)/.test(String(idRaw));
+
+ if (!url && (idRaw == null || isTempId)) return null;
+
+ const blob = {
+ fileName: name,
+ originalFilename: name,
+ fileUrl: url || undefined,
+ url: url || undefined,
+ };
+
+ if (idRaw != null && !isTempId) {
+ const n = toNumber(idRaw);
+ blob.id = n != null ? n : idRaw;
+ blob.blobId = blob.id;
+ }
+ if (rid != null) blob.reimbursementId = rid;
+ return blob;
+ })
+ .filter(Boolean);
+}
+
+function applyStorageBlobsToSaveDto(dto, form) {
+ const blobs = mapFormAttachmentsToApi(
+ resolveFormAttachmentList(form),
+ dto?.id ?? form?.reimbursementId ?? form?.id
+ );
+ if (blobs.length) {
+ dto.storageBlobVOList = blobs;
+ dto.storageBlobDTOs = blobs;
+ }
+ return dto;
+}
+
+function applyReimbursementRelations(dto) {
+ const rid = dto?.id;
+ if (rid == null) return dto;
+ if (dto.travel && typeof dto.travel === "object") {
+ dto.travel.reimbursementId = rid;
+ }
+ if (Array.isArray(dto.details)) {
+ dto.details.forEach(d => {
+ d.reimbursementId = rid;
+ });
+ }
+ const blobLists = [dto.storageBlobVOList, dto.storageBlobDTOs].filter(Array.isArray);
+ blobLists.forEach(list => {
+ list.forEach(b => {
+ b.reimbursementId = rid;
+ });
+ });
+ return dto;
+}
+
+function resolveReimbursementId(form) {
+ const rawId = form?.reimbursementId ?? form?.id;
+ if (rawId == null || rawId === "" || String(rawId).startsWith("local_")) {
+ return undefined;
+ }
+ return toNumber(rawId) ?? rawId;
+}
+
+/** 鎺ュ彛琛� 鈫� 宸梾鎶ラ攢琛ㄥ崟琛� */
+export function mapTravelReimbursementRow(row) {
+ if (!row) return {};
+ const travel = pickTravelFromRow(row);
+ const details = Array.isArray(row.details) ? row.details : [];
+ return {
+ ...row,
+ id: row.id,
+ reimbursementId: row.id,
+ approvalInstanceId: row.approvalInstanceId,
+ reimburseNo: row.billNo || "",
+ applicantId: row.applicantId,
+ applicantNo: row.applicantCode || "",
+ applicantName: row.applicantName || "",
+ employeeNo: row.applicantCode || "",
+ employeeName: row.applicantName || "",
+ applicantDeptName: row.applicantDeptName || "",
+ reimburseReason: row.reason || "",
+ travelStartTime: formatReimbursementDateTime(travel.startTime),
+ travelEndTime: formatReimbursementDateTime(travel.endTime),
+ travelDays: travel.travelDays,
+ departurePlace: travel.departureCity || "",
+ destination: travel.destinationCity || "",
+ hotelStandard: travel.hotelStandard,
+ hotelDays: travel.lodgingDays,
+ livingSubsidy: travel.mealAllowance,
+ transportSubsidy: travel.transportAllowance,
+ lodgingLimit: travel.lodgingLimit,
+ needSpecialApproval: travel.withinStandard === "0" || travel.withinStandard === 0,
+ standardTag: travel.standardTag || "",
+ applyAmount: row.applyAmount,
+ payee: row.payeeName || "",
+ payeeAccount: row.payeeAccount || "",
+ payeeBank: row.payeeBank || "",
+ billStatus: row.billStatus,
+ expenseDetails: details.map(d =>
+ mapDetailRowFromApi(d, FIN_REIMBURSEMENT_TYPE.TRAVEL)
+ ),
+ travel:
+ row.travel && typeof row.travel === "object" && Object.keys(row.travel).length
+ ? row.travel
+ : travel,
+ details,
+ nodes: resolveRowApiNodes(row),
+ approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)),
+ tasks: row.tasks || [],
+ approvalFlowSummary: formatApprovalFlowSummary(row),
+ attachmentList: row.attachmentList || row.invoiceAttachments || [],
+ };
+}
+
+/** 鎺ュ彛琛� 鈫� 璐圭敤鎶ラ攢琛ㄥ崟琛� */
+export function mapCostReimbursementRow(row) {
+ if (!row) return {};
+ const details = Array.isArray(row.details) ? row.details : [];
+ const apiNodes = resolveRowApiNodes(row);
+ const approvalFlowNodes = mapNodesToFormFlow(apiNodes);
+ return {
+ ...row,
+ id: row.id,
+ reimbursementId: row.id,
+ approvalInstanceId: row.approvalInstanceId,
+ reimburseNo: row.billNo || "",
+ applicantId: row.applicantId,
+ applicantNo: row.applicantCode || "",
+ applicantName: row.applicantName || "",
+ employeeNo: row.applicantCode || "",
+ employeeName: row.applicantName || "",
+ applicantDeptName: row.applicantDeptName || "",
+ reimburseReason: row.reason || "",
+ expenseCategory: expenseTypeToCategory(row.expenseType),
+ applyAmount: row.applyAmount,
+ applyTime: formatReimbursementDateTime(row.createTime),
+ createTime: formatReimbursementDateTime(row.createTime),
+ payee: row.payeeName || "",
+ payeeAccount: row.payeeAccount || "",
+ bankBranch: row.payeeBank || "",
+ payeeBank: row.payeeBank || "",
+ billStatus: row.billStatus,
+ expenseDetails: details.map(d =>
+ mapDetailRowFromApi(d, FIN_REIMBURSEMENT_TYPE.COST)
+ ),
+ details,
+ nodes: apiNodes,
+ approvalFlowNodes,
+ tasks: row.tasks || [],
+ approvalFlowSummary: formatApprovalFlowSummary({
+ ...row,
+ nodes: apiNodes,
+ approvalFlowNodes,
+ }),
+ attachmentList: row.attachmentList || row.invoiceAttachments || [],
+ };
+}
+
+export function mapFinReimbursementDetailRow(raw, reimbursementTypeOrModuleKey) {
+ const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey);
+ let mapped = {};
+ if (type === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+ mapped = mapTravelReimbursementRow(raw);
+ } else if (type === FIN_REIMBURSEMENT_TYPE.COST) {
+ mapped = mapCostReimbursementRow(raw);
+ } else {
+ mapped = raw || {};
+ }
+
+ let formApprovalFlowNodes = mapNodesToFormFlow(resolveRowApiNodes(raw));
+ if (!formApprovalFlowNodes.length && Array.isArray(raw?.tasks) && raw.tasks.length) {
+ formApprovalFlowNodes = mapNodesToFormFlow(mapTasksToFlowNodes(raw.tasks));
+ }
+
+ const enriched = applyFinReimbursementDetailEnrichment(mapped, raw);
+ return {
+ ...enriched,
+ approvalFlowNodes: formApprovalFlowNodes.length
+ ? formApprovalFlowNodes
+ : enriched.approvalFlowNodes,
+ reimbursementType: type,
+ reimbursementTypeLabel: reimbursementTypeLabel(type),
+ moduleKey: getModuleKeyByReimbursementType(type),
+ };
+}
+
+/** 宸梾琛ㄥ崟 鈫� FinReimbursementDto */
+export function buildTravelReimbursementSaveDto(form, { computeTravelDays } = {}) {
+ const details = mapDetailsToApi(form.expenseDetails);
+ const detailTotal = sumDetailAmount(form.expenseDetails);
+ const applyAmount = toNumber(form.applyAmount) ?? detailTotal;
+ const travelDays =
+ form.travelDays != null
+ ? toNumber(form.travelDays)
+ : computeTravelDays?.(form.travelStartTime, form.travelEndTime);
+
+ const dto = {
+ reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL,
+ expenseType: "宸梾璐�",
+ applicantId: toNumber(form.applicantId),
+ applicantCode: form.employeeNo || form.applicantNo || "",
+ applicantName: form.employeeName || form.applicantName || "",
+ applicantDeptId: toNumber(form.applicantDeptId),
+ applicantDeptName: form.applicantDeptName || form.deptName || "",
+ reason: (form.reimburseReason || "").trim(),
+ applyAmount,
+ detailTotalAmount: detailTotal,
+ payeeName: form.payee || "",
+ payeeAccount: form.payeeAccount || undefined,
+ payeeBank: form.payeeBank || undefined,
+ billStatus: "IN_APPROVAL",
+ deptId: toNumber(form.deptId),
+ travel: {
+ startTime: form.travelStartTime || undefined,
+ endTime: form.travelEndTime || undefined,
+ travelDays,
+ departureCity: form.departurePlace || "",
+ destinationCity: form.destination || "",
+ hotelStandard: toNumber(form.hotelStandard),
+ lodgingDays: toNumber(form.hotelDays),
+ mealAllowance: toNumber(form.livingSubsidy),
+ transportAllowance: toNumber(form.transportSubsidy),
+ lodgingLimit: toNumber(form.lodgingLimit),
+ standardTag: form.standardTag || (form.needSpecialApproval ? "瓒呮爣鐗规壒" : "鍦ㄦ爣鍑嗚寖鍥村唴"),
+ withinStandard: form.needSpecialApproval ? "0" : "1",
+ },
+ details,
+ nodes: mapApprovalFlowNodesToApi(
+ resolveFormApprovalFlowNodes(form),
+ form.templateId
+ ),
+ };
+
+ const id = resolveReimbursementId(form);
+ if (id != null) dto.id = id;
+ if (form.billNo || form.reimburseNo) dto.billNo = form.billNo || form.reimburseNo;
+ if (form.approvalInstanceId != null) dto.approvalInstanceId = toNumber(form.approvalInstanceId);
+ if (form.approveProcessId != null) dto.approveProcessId = toNumber(form.approveProcessId);
+ if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id);
+
+ applyStorageBlobsToSaveDto(dto, form);
+ return applyReimbursementRelations(dto);
+}
+
+/** 璐圭敤琛ㄥ崟 鈫� FinReimbursementDto */
+export function buildCostReimbursementSaveDto(form) {
+ const details = mapDetailsToApi(form.expenseDetails);
+ const detailTotal = sumDetailAmount(form.expenseDetails);
+ const applyAmount = toNumber(form.applyAmount) ?? detailTotal;
+
+ const dto = {
+ reimbursementType: FIN_REIMBURSEMENT_TYPE.COST,
+ expenseType: expenseCategoryToType(form.expenseCategory),
+ applicantId: toNumber(form.applicantId),
+ applicantCode: form.employeeNo || form.applicantNo || "",
+ applicantName: form.employeeName || form.applicantName || "",
+ applicantDeptId: toNumber(form.applicantDeptId),
+ applicantDeptName: form.applicantDeptName || form.deptName || "",
+ reason: (form.reimburseReason || "").trim(),
+ applyAmount,
+ detailTotalAmount: detailTotal,
+ payeeName: form.payee || "",
+ payeeAccount: form.payeeAccount || "",
+ payeeBank: form.bankBranch || form.payeeBank || "",
+ billStatus: "IN_APPROVAL",
+ deptId: toNumber(form.deptId),
+ details,
+ nodes: mapApprovalFlowNodesToApi(
+ resolveFormApprovalFlowNodes(form),
+ form.templateId
+ ),
+ };
+
+ const id = resolveReimbursementId(form);
+ if (id != null) dto.id = id;
+ if (form.billNo || form.reimburseNo) dto.billNo = form.billNo || form.reimburseNo;
+ if (form.approvalInstanceId != null) dto.approvalInstanceId = toNumber(form.approvalInstanceId);
+ if (form.approveProcessId != null) dto.approveProcessId = toNumber(form.approveProcessId);
+
+ applyStorageBlobsToSaveDto(dto, form);
+ return applyReimbursementRelations(dto);
+}
+
+/** 濉姤椤靛姞杞借鎯咃紙涓� Web openFormDialog edit 涓�鑷达級 */
+export async function fetchFinReimbursementFormDetail(item, moduleKey) {
+ const id = resolveReimbursementDeleteId(item);
+ if (id == null) throw new Error("missing reimbursement id");
+ const res = await getFinReimbursementDetail(id);
+ const raw = unwrapFinReimbursementDetail(res);
+ return mapFinReimbursementDetailRow(raw, moduleKey);
+}
diff --git a/src/pages/oa/_utils/oaPageRegistry.js b/src/pages/oa/_utils/oaPageRegistry.js
new file mode 100644
index 0000000..22019bc
--- /dev/null
+++ b/src/pages/oa/_utils/oaPageRegistry.js
@@ -0,0 +1,256 @@
+import { OA_NAV } from "@/config/oaPaths.js";
+
+const STATUS_MAP = {
+ pending: { text: "瀹℃牳涓�", type: "warning" },
+ approved: { text: "宸查�氳繃", type: "success" },
+ rejected: { text: "宸查┏鍥�", type: "error" },
+ draft: { text: "鑽夌", type: "info" },
+ published: { text: "宸插彂甯�", type: "success" },
+};
+
+function baseRow(extra = {}) {
+ return {
+ id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
+ applicantName: "寮犱笁",
+ deptName: "鐮斿彂閮�",
+ status: "pending",
+ createTime: "2026-05-18 09:00:00",
+ summary: "绀轰緥鏁版嵁锛屽彲瀵规帴鍚庣鎺ュ彛",
+ ...extra,
+ };
+}
+
+/** 鍚勫瓙椤甸潰閰嶇疆锛歵itle銆乻torageKey銆佸垪琛ㄥ睍绀哄瓧娈点�佸垵濮� mock */
+export const OA_PAGE_REGISTRY = {
+ "HrManage/staff-archive": {
+ title: "鍛樺伐妗f",
+ module: "浜轰簨绠$悊",
+ storageKey: "oa_hr_staff_archive_v1",
+ path: OA_NAV.staffArchive,
+ fields: [
+ { label: "鍛樺伐缂栧彿", prop: "staffNo" },
+ { label: "宀椾綅", prop: "postJob" },
+ { label: "鑱旂郴鐢佃瘽", prop: "phone" },
+ ],
+ mockRows: [
+ baseRow({
+ staffNo: "E2026001",
+ postJob: "宸ョ▼甯�",
+ phone: "13800000001",
+ summary: "鏉庢槑 路 鍦ㄨ亴",
+ }),
+ ],
+ },
+ "HrManage/staff-contract": {
+ title: "鍛樺伐鍚堝悓",
+ module: "浜轰簨绠$悊",
+ storageKey: "oa_hr_staff_contract_v1",
+ path: OA_NAV.staffContract,
+ fields: [
+ { label: "鍚堝悓缂栧彿", prop: "contractNo" },
+ { label: "鍚堝悓绫诲瀷", prop: "contractType" },
+ { label: "鍒版湡鏃�", prop: "endDate" },
+ ],
+ mockRows: [
+ baseRow({
+ contractNo: "HT-2026-001",
+ contractType: "鍔冲姩鍚堝悓",
+ endDate: "2027-12-31",
+ }),
+ ],
+ },
+ "HrManage/regular-apply": {
+ title: "杞鐢宠",
+ module: "浜轰簨绠$悊",
+ storageKey: "oa_hr_regular_apply_v1",
+ path: OA_NAV.regularApply,
+ fields: [
+ { label: "鍏ヨ亴鏃ユ湡", prop: "entryDate" },
+ { label: "杞鏃ユ湡", prop: "regularDate" },
+ ],
+ mockRows: [baseRow({ entryDate: "2025-11-01", regularDate: "2026-05-20" })],
+ },
+ "HrManage/transfer-apply": {
+ title: "璋冨矖鐢宠",
+ module: "浜轰簨绠$悊",
+ storageKey: "oa_hr_transfer_apply_v1",
+ path: OA_NAV.transferApply,
+ fields: [
+ { label: "鍘熷矖浣�", prop: "fromPost" },
+ { label: "鐩爣宀椾綅", prop: "toPost" },
+ ],
+ mockRows: [
+ baseRow({ fromPost: "寮�鍙戝伐绋嬪笀", toPost: "楂樼骇寮�鍙戝伐绋嬪笀" }),
+ ],
+ },
+ "HrManage/resign-apply": {
+ title: "绂昏亴鐢宠",
+ module: "浜轰簨绠$悊",
+ storageKey: "oa_hr_resign_apply_v1",
+ path: OA_NAV.resignApply,
+ fields: [
+ { label: "棰勮绂昏亴鏃�", prop: "leaveDate" },
+ { label: "绂昏亴鍘熷洜", prop: "reason" },
+ ],
+ mockRows: [baseRow({ leaveDate: "2026-06-30", reason: "涓汉鍙戝睍" })],
+ },
+ "HrManage/work-handover": {
+ title: "宸ヤ綔浜ゆ帴",
+ module: "浜轰簨绠$悊",
+ storageKey: "oa_hr_work_handover_v1",
+ path: OA_NAV.workHandover,
+ fields: [
+ { label: "浜ゆ帴浜�", prop: "handoverTo" },
+ { label: "浜ゆ帴浜嬮」", prop: "handoverItems" },
+ ],
+ mockRows: [
+ baseRow({ handoverTo: "鐜嬩簲", handoverItems: "椤圭洰鏂囨。銆佸鎴疯祫鏂�" }),
+ ],
+ },
+ "HrManage/post-manage": {
+ title: "宀椾綅绠$悊",
+ module: "浜轰簨绠$悊",
+ storageKey: "oa_hr_post_manage_v1",
+ path: OA_NAV.postManage,
+ fields: [
+ { label: "宀椾綅缂栫爜", prop: "postCode" },
+ { label: "鎵�灞為儴闂�", prop: "deptName" },
+ ],
+ mockRows: [baseRow({ postCode: "DEV-01", summary: "寮�鍙戝伐绋嬪笀" })],
+ },
+ "AttendManage/leave-apply": {
+ title: "璇峰亣鐢宠",
+ module: "鍋囧嫟绠$悊",
+ storageKey: "oa_attend_leave_apply_v1",
+ path: OA_NAV.leaveApply,
+ fields: [
+ { label: "璇峰亣绫诲瀷", prop: "leaveType" },
+ { label: "寮�濮嬫椂闂�", prop: "startTime" },
+ { label: "缁撴潫鏃堕棿", prop: "endTime" },
+ ],
+ mockRows: [
+ baseRow({
+ leaveType: "骞村亣",
+ startTime: "2026-05-20 09:00",
+ endTime: "2026-05-21 18:00",
+ }),
+ ],
+ },
+ "AttendManage/overtime-apply": {
+ title: "鍔犵彮鐢宠",
+ module: "鍋囧嫟绠$悊",
+ storageKey: "oa_attend_overtime_apply_v1",
+ path: OA_NAV.overtimeApply,
+ fields: [
+ { label: "鍔犵彮鏃ユ湡", prop: "overtimeDate" },
+ { label: "鏃堕暱(灏忔椂)", prop: "hours" },
+ ],
+ mockRows: [
+ baseRow({ overtimeDate: "2026-05-18", hours: "3", summary: "鐗堟湰涓婄嚎" }),
+ ],
+ },
+ "ReimburseManage/travel-reimburse": {
+ title: "宸梾鎶ラ攢",
+ module: "鎶ラ攢绠$悊",
+ storageKey: "oa_reimburse_travel_v1",
+ path: OA_NAV.travelReimburse,
+ fields: [
+ { label: "鍑哄樊鍦扮偣", prop: "destination" },
+ { label: "閲戦(鍏�)", prop: "amount" },
+ ],
+ mockRows: [
+ baseRow({ destination: "涓婃捣", amount: "2680.50", summary: "瀹㈡埛鎷滆宸梾" }),
+ ],
+ },
+ "ReimburseManage/cost-reimburse": {
+ title: "璐圭敤鎶ラ攢",
+ module: "鎶ラ攢绠$悊",
+ storageKey: "oa_reimburse_cost_v1",
+ path: OA_NAV.costReimburse,
+ fields: [
+ { label: "璐圭敤绉戠洰", prop: "category" },
+ { label: "閲戦(鍏�)", prop: "amount" },
+ ],
+ mockRows: [
+ baseRow({ category: "鍔炲叕鐢ㄥ搧", amount: "356.00", summary: "閲囪喘鏂囧叿" }),
+ ],
+ },
+ "ApproveManage/approve-list": {
+ title: "瀹℃壒鍒楄〃",
+ module: "瀹℃壒绠$悊",
+ storageKey: "oa_unified_approve_list_v1",
+ path: OA_NAV.approveList,
+ fields: [
+ { label: "瀹℃壒绫诲瀷", prop: "approvalTypeLabel" },
+ { label: "褰撳墠鑺傜偣", prop: "currentNode" },
+ ],
+ mockRows: [
+ baseRow({
+ approvalTypeLabel: "璇峰亣鐢宠",
+ currentNode: "閮ㄩ棬璐熻矗浜�",
+ }),
+ ],
+ },
+ "ApproveManage/approve-template": {
+ title: "瀹℃壒妯℃澘",
+ module: "瀹℃壒绠$悊",
+ storageKey: "oa_approve_template_custom_v1",
+ path: OA_NAV.approveTemplate,
+ fields: [
+ { label: "妯℃澘鍚嶇О", prop: "templateName" },
+ { label: "鑺傜偣鏁�", prop: "nodeCount" },
+ ],
+ mockRows: [
+ baseRow({
+ templateName: "閫氱敤瀹℃壒娴�",
+ nodeCount: "3",
+ status: "approved",
+ summary: "绯荤粺鍐呯疆妯℃澘",
+ }),
+ ],
+ },
+ "EnterpriseNews/news-manage": {
+ title: "浼佷笟鏂伴椈",
+ module: "浼佷笟鏂伴椈",
+ storageKey: "oa_enterprise_news_v1",
+ path: OA_NAV.enterpriseNews,
+ fields: [
+ { label: "鏍忕洰", prop: "category" },
+ { label: "闃呰閲�", prop: "readCount" },
+ ],
+ mockRows: [
+ baseRow({
+ category: "鍏徃鍔ㄦ��",
+ readCount: "128",
+ status: "published",
+ summary: "2026骞寸涓�瀛e害缁忚惀閫氭姤",
+ }),
+ ],
+ },
+ "NoticeAnnouncement/notice-manage": {
+ title: "鍏憡閫氱煡",
+ module: "鍏憡閫氱煡",
+ storageKey: "oa_notice_announcement_v1",
+ path: OA_NAV.noticeAnnouncement,
+ fields: [
+ { label: "鍏憡绫诲瀷", prop: "noticeType" },
+ { label: "浼樺厛绾�", prop: "priority" },
+ ],
+ mockRows: [
+ baseRow({
+ noticeType: "浼佷笟鍏憡",
+ priority: "鏅��",
+ status: "published",
+ summary: "浜斾竴鍔冲姩鑺傛斁鍋囧畨鎺�",
+ }),
+ ],
+ },
+};
+
+export function getOaPageConfig(pageKey) {
+ return OA_PAGE_REGISTRY[pageKey] || null;
+}
+
+export function getStatusMeta(status) {
+ return STATUS_MAP[status] || { text: status || "鈥�", type: "info" };
+}
diff --git a/src/pages/oa/_utils/oaStorage.js b/src/pages/oa/_utils/oaStorage.js
new file mode 100644
index 0000000..67b2cd0
--- /dev/null
+++ b/src/pages/oa/_utils/oaStorage.js
@@ -0,0 +1,26 @@
+export function loadList(storageKey, defaultRows = []) {
+ try {
+ const raw = uni.getStorageSync(storageKey);
+ if (!raw) {
+ return defaultRows.map(row => ({ ...row }));
+ }
+ const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
+ return Array.isArray(parsed)
+ ? parsed.map(row => ({ ...row }))
+ : defaultRows.map(row => ({ ...row }));
+ } catch {
+ return defaultRows.map(row => ({ ...row }));
+ }
+}
+
+export function saveList(storageKey, rows) {
+ uni.setStorageSync(storageKey, JSON.stringify(rows));
+}
+
+export function ensureList(storageKey, defaultRows) {
+ const list = loadList(storageKey, defaultRows);
+ if (!uni.getStorageSync(storageKey)) {
+ saveList(storageKey, list);
+ }
+ return list;
+}
diff --git a/src/pages/oa/_utils/oaUi.js b/src/pages/oa/_utils/oaUi.js
new file mode 100644
index 0000000..f6af87e
--- /dev/null
+++ b/src/pages/oa/_utils/oaUi.js
@@ -0,0 +1,13 @@
+export function showToast(title, icon = "none") {
+ uni.showToast({ title, icon });
+}
+
+export function confirmModal(content, title = "鎻愮ず") {
+ return new Promise(resolve => {
+ uni.showModal({
+ title,
+ content,
+ success: res => resolve(Boolean(res.confirm)),
+ });
+ });
+}
diff --git a/src/pages/oa/_utils/reimburseApproveBridge.js b/src/pages/oa/_utils/reimburseApproveBridge.js
new file mode 100644
index 0000000..8d5666d
--- /dev/null
+++ b/src/pages/oa/_utils/reimburseApproveBridge.js
@@ -0,0 +1,99 @@
+import { matchBusinessTypeValue } from "./approvalTemplateType.js";
+import {
+ APPROVAL_MODULE_KEYS,
+ getApprovalModuleConfig,
+} from "./approvalModuleRegistry.js";
+import { fetchFinReimbursementListItemDetail } from "./finReimbursementMappers.js";
+
+export const REIMBURSE_EDIT_FROM_APPROVE_KEY = "oa_reimburse_edit_from_approve";
+export const FIN_REIMBURSE_FORM_ACTION_KEY = "oa_fin_reimburse_form_action";
+
+const REIMBURSE_MODULE_KEYS = [
+ APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE,
+ APPROVAL_MODULE_KEYS.COST_REIMBURSE,
+];
+
+export function inferReimburseModuleKeyFromInstance(row) {
+ if (!row) return "";
+ for (const moduleKey of REIMBURSE_MODULE_KEYS) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) continue;
+ if (
+ cfg.businessType != null &&
+ cfg.businessType !== "" &&
+ matchBusinessTypeValue(row.businessType, cfg.businessType)
+ ) {
+ return moduleKey;
+ }
+ if (matchBusinessTypeValue(row.businessType, cfg.approvalType)) {
+ return moduleKey;
+ }
+ const text = `${row.templateName || ""}${row.title || ""}${row.businessName || ""}`;
+ if ((cfg.typeLabels || []).some(l => l && text.includes(l))) {
+ return moduleKey;
+ }
+ }
+ return "";
+}
+
+export function isReimburseApprovalInstance(row) {
+ return Boolean(inferReimburseModuleKeyFromInstance(row));
+}
+
+export function resolveFinReimbursementIdFromInstance(row) {
+ const raw = row?.businessId ?? row?.formPayload?.reimbursementId;
+ if (raw == null || raw === "") return undefined;
+ const n = Number(raw);
+ return Number.isNaN(n) ? raw : n;
+}
+
+export async function loadReimburseDetailForInstance(instanceRow, moduleKey) {
+ const mk = moduleKey || inferReimburseModuleKeyFromInstance(instanceRow);
+ const id = resolveFinReimbursementIdFromInstance(instanceRow);
+ if (id == null) {
+ throw new Error("missing reimbursement id");
+ }
+ const reimburseRow = await fetchFinReimbursementListItemDetail(
+ { reimbursementId: id },
+ mk
+ );
+ return {
+ reimburseRow,
+ instanceRow,
+ moduleKey: reimburseRow.moduleKey || mk,
+ reimbursementType: reimburseRow.reimbursementType,
+ };
+}
+
+export function stashReimburseEditFromApprove(moduleKey, reimbursementId) {
+ uni.setStorageSync(
+ REIMBURSE_EDIT_FROM_APPROVE_KEY,
+ JSON.stringify({ moduleKey, reimbursementId })
+ );
+}
+
+export function consumeReimburseEditFromApprove() {
+ const raw = uni.getStorageSync(REIMBURSE_EDIT_FROM_APPROVE_KEY);
+ if (!raw) return null;
+ uni.removeStorageSync(REIMBURSE_EDIT_FROM_APPROVE_KEY);
+ try {
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
+ } catch {
+ return null;
+ }
+}
+
+export function stashFinReimburseFormAction(payload) {
+ uni.setStorageSync(FIN_REIMBURSE_FORM_ACTION_KEY, JSON.stringify(payload));
+}
+
+export function consumeFinReimburseFormAction() {
+ const raw = uni.getStorageSync(FIN_REIMBURSE_FORM_ACTION_KEY);
+ if (!raw) return null;
+ uni.removeStorageSync(FIN_REIMBURSE_FORM_ACTION_KEY);
+ try {
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
+ } catch {
+ return null;
+ }
+}
diff --git a/src/pages/oa/_utils/useOaPage.js b/src/pages/oa/_utils/useOaPage.js
new file mode 100644
index 0000000..960344d
--- /dev/null
+++ b/src/pages/oa/_utils/useOaPage.js
@@ -0,0 +1,6 @@
+import { getOaPageConfig } from "./oaPageRegistry.js";
+
+export function useOaPage(pageKey) {
+ const config = getOaPageConfig(pageKey);
+ return { pageKey, config };
+}
diff --git a/src/pages/oa/_utils/userPickerUtils.js b/src/pages/oa/_utils/userPickerUtils.js
new file mode 100644
index 0000000..d812ef1
--- /dev/null
+++ b/src/pages/oa/_utils/userPickerUtils.js
@@ -0,0 +1,53 @@
+/** 鐢ㄦ埛鍒楄〃瑙e寘 */
+export function unwrapUserList(res) {
+ if (Array.isArray(res)) return res;
+ if (Array.isArray(res?.data)) return res.data;
+ if (Array.isArray(res?.rows)) return res.rows;
+ return [];
+}
+
+export function isActiveUser(u) {
+ if (u?.delFlag === "2" || u?.delFlag === 2) return false;
+ if (u?.status == null) return true;
+ return String(u.status) === "0";
+}
+
+export function userSelectLabel(u) {
+ const nick = u?.nickName || "";
+ const name = u?.userName || "";
+ if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
+ return nick || name || `鐢ㄦ埛${u?.userId ?? u?.id ?? ""}`;
+}
+
+export function userSubLabel(u) {
+ const parts = [];
+ const code = u?.userName || u?.userCode || "";
+ if (code) parts.push(`宸ュ彿 ${code}`);
+ const dept = u?.dept?.deptName ?? u?.deptName ?? "";
+ if (dept) parts.push(dept);
+ return parts.join(" 路 ") || "";
+}
+
+const AVATAR_COLORS = ["#409EFF", "#67C23A", "#E6A23C", "#9B59B6", "#1ABC9C", "#F56C6C"];
+
+export function userAvatarColor(name) {
+ if (!name) return "#c0c4cc";
+ let h = 0;
+ for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h);
+ return AVATAR_COLORS[Math.abs(h) % AVATAR_COLORS.length];
+}
+
+/** 鎸夊鍚�/宸ュ彿/ID 鎼滅储锛岀┖鍏抽敭瀛楁椂浼樺厛灞曠ず鍓� limit 鏉� */
+export function filterActiveUsers(list, keyword, limit = 80) {
+ const active = (list || []).filter(isActiveUser);
+ const q = (keyword || "").trim().toLowerCase();
+ if (!q) return active.slice(0, limit);
+ return active
+ .filter(u => {
+ const nick = (u.nickName || "").toLowerCase();
+ const name = (u.userName || "").toLowerCase();
+ const id = String(u.userId ?? u.id ?? "");
+ return nick.includes(q) || name.includes(q) || id.includes(q);
+ })
+ .slice(0, limit);
+}
diff --git a/src/pages/procurementManagement/procurementLedger/detail.vue b/src/pages/procurementManagement/procurementLedger/detail.vue
index d7693cc..397d863 100644
--- a/src/pages/procurementManagement/procurementLedger/detail.vue
+++ b/src/pages/procurementManagement/procurementLedger/detail.vue
@@ -31,8 +31,7 @@
</up-form-item>
<up-form-item label="渚涘簲鍟嗗悕绉�"
prop="supplierName"
- required
- >
+ required>
<up-input v-model="form.supplierName"
readonly
:disabled="isReadOnly"
@@ -82,55 +81,6 @@
placeholder="璇疯緭鍏�"
disabled />
</up-form-item>
- <view class="approval-process">
- <view class="approval-header">
- <text class="approval-title">瀹℃牳娴佺▼</text>
- <text class="approval-desc">姣忎釜姝ラ鍙兘閫夋嫨涓�涓鎵逛汉</text>
- </view>
- <view class="approval-steps">
- <view v-for="(step, stepIndex) in approverNodes"
- :key="stepIndex"
- class="approval-step">
- <view class="step-dot"></view>
- <view class="step-title">
- <text>瀹℃壒浜�</text>
- </view>
- <view class="approver-container">
- <view v-if="step.nickName"
- class="approver-item">
- <view class="approver-avatar">
- <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
- <view class="status-dot"></view>
- </view>
- <view class="approver-info">
- <text class="approver-name">{{ step.nickName }}</text>
- </view>
- <view class="delete-approver-btn"
- v-if="!isReadOnly"
- @click="removeApprover(stepIndex)">脳</view>
- </view>
- <view v-else-if="!isReadOnly"
- class="add-approver-btn"
- @click="addApprover(stepIndex)">
- <view class="add-circle">+</view>
- <text class="add-label">閫夋嫨瀹℃壒浜�</text>
- </view>
- </view>
- <view class="step-line"
- v-if="stepIndex < approverNodes.length - 1"></view>
- <view class="delete-step-btn"
- v-if="approverNodes.length > 1 && !isReadOnly"
- @click="removeApprovalStep(stepIndex)">鍒犻櫎鑺傜偣</view>
- </view>
- </view>
- <view class="add-step-btn" v-if="!isReadOnly">
- <u-button icon="plus"
- plain
- type="primary"
- style="width: 100%"
- @click="addApprovalStep">鏂板鑺傜偣</u-button>
- </view>
- </view>
<up-popup :show="showTimePicker"
mode="bottom"
@close="showTimePicker = false">
diff --git a/src/pages/productionDesign/basicParameters/edit.vue b/src/pages/productionDesign/basicParameters/edit.vue
new file mode 100644
index 0000000..111b465
--- /dev/null
+++ b/src/pages/productionDesign/basicParameters/edit.vue
@@ -0,0 +1,290 @@
+<template>
+ <view class="basic-parameters-edit">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ :errorType="['none']"
+ label-width="110">
+ <up-form-item label="鍙傛暟缂栫爜"
+ prop="paramCode">
+ <up-input v-model="form.paramCode"
+ disabled
+ placeholder="鑷姩鐢熸垚" />
+ </up-form-item>
+ <up-form-item label="鍙傛暟鍚嶇О"
+ prop="paramName"
+ required>
+ <up-input v-model="form.paramName"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍙傛暟绫诲瀷"
+ prop="paramType"
+ required>
+ <up-input v-model="paramTypeText"
+ placeholder="璇烽�夋嫨鍙傛暟绫诲瀷"
+ readonly
+ @click="showParamTypeSheet = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showParamTypeSheet = true"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="鍗曚綅"
+ prop="unit"
+ :required="form.paramType === 1">
+ <up-input v-model="form.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍙栧�兼牸寮�"
+ v-if="form.paramType === 1 || form.paramType === 2"
+ prop="paramFormat">
+ <up-input v-model="form.paramFormat"
+ placeholder="璇疯緭鍏ュ彇鍊兼牸寮�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="涓嬫媺瀛楀吀"
+ v-else-if="form.paramType === 3"
+ prop="paramFormat">
+ <up-input v-model="dictTypeText"
+ placeholder="璇烽�夋嫨涓嬫媺瀛楀吀"
+ readonly
+ @click="showDictTypeSheet = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showDictTypeSheet = true"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="鏃堕棿鏍煎紡"
+ v-else-if="form.paramType === 4"
+ prop="paramFormat">
+ <up-input v-model="form.paramFormat"
+ placeholder="璇烽�夋嫨鏃堕棿鏍煎紡"
+ readonly
+ @click="showTimeFormatSheet = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showTimeFormatSheet = true"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="鏄惁蹇呭~"
+ prop="isRequired">
+ <view style="display: flex; justify-content: flex-end; width: 100%;">
+ <up-switch v-model="form.isRequired"
+ :activeValue="1"
+ :inactiveValue="0" />
+ </view>
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ autoHeight />
+ </up-form-item>
+ </up-form>
+ <FooterButtons :loading="loading"
+ :confirmText="paramId ? '淇濆瓨' : '鏂板'"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ <!-- 鍙傛暟绫诲瀷閫夋嫨 -->
+ <up-action-sheet :show="showParamTypeSheet"
+ title="閫夋嫨鍙傛暟绫诲瀷"
+ :actions="paramTypeActions"
+ @select="onSelectParamType"
+ @close="showParamTypeSheet = false" />
+ <!-- 涓嬫媺瀛楀吀閫夋嫨 -->
+ <up-action-sheet :show="showDictTypeSheet"
+ title="閫夋嫨涓嬫媺瀛楀吀"
+ :actions="dictTypeActions"
+ @select="onSelectDictType"
+ @close="showDictTypeSheet = false" />
+ <!-- 鏃堕棿鏍煎紡閫夋嫨 -->
+ <up-action-sheet :show="showTimeFormatSheet"
+ title="閫夋嫨鏃堕棿鏍煎紡"
+ :actions="timeFormatActions"
+ @select="onSelectTimeFormat"
+ @close="showTimeFormatSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, nextTick, onMounted, ref } from "vue";
+ import { onLoad, onReady } from "@dcloudio/uni-app";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import PageHeader from "@/components/PageHeader.vue";
+ import {
+ addBaseParam,
+ editBaseParam,
+ } from "@/api/basicData/parameterMaintenance";
+ import { listType } from "@/api/system/dict/type";
+
+ const formRef = ref();
+ const loading = ref(false);
+ const paramId = ref("");
+ const showParamTypeSheet = ref(false);
+ const showDictTypeSheet = ref(false);
+ const showTimeFormatSheet = ref(false);
+ const dictTypes = ref([]);
+
+ const form = ref({
+ id: null,
+ paramCode: "",
+ paramName: "",
+ paramType: "",
+ unit: "",
+ remark: "",
+ isRequired: 0,
+ paramFormat: "",
+ });
+
+ const rules = {
+ paramName: [{ required: true, message: "璇疯緭鍏ュ弬鏁板悕绉�" }],
+ paramType: [{ required: true, message: "璇烽�夋嫨鍙傛暟绫诲瀷" }],
+ unit: [
+ {
+ validator: (rule, value, callback) => {
+ if (form.value.paramType === 1 && !value) {
+ callback(new Error("鏁板�肩被鍨嬪繀椤诲~鍐欏崟浣�"));
+ } else {
+ callback();
+ }
+ },
+ },
+ ],
+ };
+
+ const paramTypeActions = [
+ { name: "鏁板�兼牸寮�", value: 1 },
+ { name: "鏂囨湰鏍煎紡", value: 2 },
+ { name: "涓嬫媺閫夐」", value: 3 },
+ { name: "鏃堕棿鏍煎紡", value: 4 },
+ ];
+
+ const timeFormatActions = [
+ { name: "YYYY-MM-DD", value: "YYYY-MM-DD" },
+ { name: "YYYY-MM-DD HH:mm:ss", value: "YYYY-MM-DD HH:mm:ss" },
+ ];
+
+ const dictTypeActions = computed(() => {
+ return dictTypes.value.map(item => ({
+ name: item.dictName,
+ value: item.dictType,
+ }));
+ });
+
+ const pageTitle = computed(() => (paramId.value ? "缂栬緫鍙傛暟" : "鏂板鍙傛暟"));
+
+ const paramTypeText = computed(() => {
+ const action = paramTypeActions.find(
+ item => item.value === form.value.paramType
+ );
+ return action ? action.name : "";
+ });
+
+ const dictTypeText = computed(() => {
+ const action = dictTypes.value.find(
+ item => item.dictType === form.value.paramFormat
+ );
+ return action ? action.dictName : form.value.paramFormat || "";
+ });
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const getDictTypes = () => {
+ listType({ pageNum: 1, pageSize: 1000 }).then(res => {
+ dictTypes.value = res.rows || [];
+ });
+ };
+
+ const onSelectParamType = action => {
+ form.value.paramType = action.value;
+ if (action.value === 1) {
+ form.value.paramFormat = "#.00000";
+ } else if (action.value === 4) {
+ form.value.paramFormat = "YYYY-MM-DD HH:mm:ss";
+ } else {
+ form.value.paramFormat = "";
+ }
+ showParamTypeSheet.value = false;
+ };
+
+ const onSelectDictType = action => {
+ form.value.paramFormat = action.value;
+ showDictTypeSheet.value = false;
+ };
+
+ const onSelectTimeFormat = action => {
+ form.value.paramFormat = action.value;
+ showTimeFormatSheet.value = false;
+ };
+
+ const handleSubmit = () => {
+ formRef.value
+ .validate()
+ .then(() => {
+ if (form.value.paramType === 3 && !form.value.paramFormat) {
+ uni.showToast({ title: "璇烽�夋嫨涓嬫媺瀛楀吀", icon: "none" });
+ return;
+ }
+
+ loading.value = true;
+ const action = paramId.value ? editBaseParam : addBaseParam;
+ action({ ...form.value, id: paramId.value || undefined })
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => {
+ goBack();
+ }, 1500);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ })
+ .catch(errors => {
+ if (errors && errors.length > 0) {
+ uni.showToast({
+ title: errors[0].message,
+ icon: "none",
+ });
+ }
+ });
+ };
+
+ onReady(() => {
+ if (formRef.value) {
+ formRef.value.setRules(rules);
+ }
+ });
+
+ onMounted(() => {
+ getDictTypes();
+ });
+
+ onLoad(options => {
+ if (options?.item) {
+ const item = JSON.parse(decodeURIComponent(options.item));
+ paramId.value = item.id;
+ if (item.paramType) {
+ item.paramType = Number(item.paramType);
+ }
+ Object.assign(form.value, item);
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .basic-parameters-edit {
+ min-height: 100vh;
+ background: #f5f5f5;
+ }
+</style>
diff --git a/src/pages/productionDesign/basicParameters/index.vue b/src/pages/productionDesign/basicParameters/index.vue
new file mode 100644
index 0000000..24a5db0
--- /dev/null
+++ b/src/pages/productionDesign/basicParameters/index.vue
@@ -0,0 +1,245 @@
+<template>
+ <view class="sales-account">
+ <PageHeader title="鍩虹鍙傛暟"
+ @back="goBack" />
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="paramName"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�"
+ clearable
+ @change="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="24"
+ color="#999999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <view v-if="list.length > 0"
+ class="ledger-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="setting-fill"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.paramName || "-" }}</text>
+ </view>
+ <text class="item-index">{{ item.paramCode || "-" }}</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍙傛暟绫诲瀷</text>
+ <up-tag :text="getParamTypeLabel(item.paramType)"
+ :type="getParamTypeTag(item.paramType)"
+ size="mini" />
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ item.unit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏄惁蹇呭~</text>
+ <up-tag :text="item.isRequired === 1 ? '鏄�' : '鍚�'"
+ :type="item.isRequired === 1 ? 'success' : 'info'"
+ size="mini" />
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙栧�兼牸寮�</text>
+ <text class="detail-value">{{ item.paramFormat || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="goEdit(item)">缂栬緫</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ @click="handleDelete(item)">鍒犻櫎</up-button>
+ </view>
+ </view>
+ <up-loadmore :status="page.status" />
+ </view>
+ <view v-else
+ class="no-data">
+ <text>鏆傛棤鍩虹鍙傛暟鏁版嵁</text>
+ </view>
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import {
+ getBaseParamList,
+ removeBaseParam,
+ } from "@/api/basicData/parameterMaintenance";
+
+ const paramName = ref("");
+ const list = ref([]);
+
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ status: "loadmore", // loadmore, loading, nomore
+ });
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const getParamTypeLabel = type => {
+ const map = {
+ 1: "鏁板�兼牸寮�",
+ 2: "鏂囨湰鏍煎紡",
+ 3: "涓嬫媺閫夐」",
+ 4: "鏃堕棿鏍煎紡",
+ };
+ return map[type] || type;
+ };
+
+ const getParamTypeTag = type => {
+ const map = {
+ 1: "primary",
+ 2: "info",
+ 3: "warning",
+ 4: "success",
+ };
+ return map[type] || "info";
+ };
+
+ const goAdd = () => {
+ uni.navigateTo({ url: "/pages/productionDesign/basicParameters/edit" });
+ };
+
+ const goEdit = item => {
+ uni.navigateTo({
+ url: `/pages/productionDesign/basicParameters/edit?item=${encodeURIComponent(
+ JSON.stringify(item)
+ )}`,
+ });
+ };
+
+ const handleDelete = item => {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "纭畾瑕佸垹闄よ鍙傛暟鍚楋紵",
+ success: res => {
+ if (res.confirm) {
+ removeBaseParam(item.id).then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛" });
+ handleSearch();
+ });
+ }
+ },
+ });
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ page.status = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const getList = () => {
+ if (page.status === "loading" || page.status === "nomore") return;
+
+ page.status = "loading";
+ getBaseParamList({
+ current: page.current,
+ size: page.size,
+ paramName: paramName.value,
+ })
+ .then(res => {
+ const records = res?.data?.records || res?.records || [];
+ const total = res?.data?.total || res?.total || 0;
+
+ if (page.current === 1) {
+ list.value = records;
+ } else {
+ list.value = [...list.value, ...records];
+ }
+
+ page.total = total;
+ if (list.value.length >= total) {
+ page.status = "nomore";
+ } else {
+ page.status = "loadmore";
+ page.current++;
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "error" });
+ page.status = "loadmore";
+ });
+ };
+
+ onReachBottom(() => {
+ getList();
+ });
+
+ onShow(() => {
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .no-data {
+ padding-top: 100rpx;
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+ }
+
+ .action-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 20rpx;
+ padding-bottom: 30rpx;
+ }
+
+ .action-btn {
+ width: 140rpx;
+ margin: 0 !important;
+ }
+
+ .fab-button {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #2979ff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.4);
+ z-index: 100;
+ }
+</style>
diff --git a/src/pages/productionDesign/bom/BomStructureItem.vue b/src/pages/productionDesign/bom/BomStructureItem.vue
new file mode 100644
index 0000000..689010c
--- /dev/null
+++ b/src/pages/productionDesign/bom/BomStructureItem.vue
@@ -0,0 +1,256 @@
+<template>
+ <view class="structure-item-wrapper"
+ :class="{ 'is-root': level === 0, 'is-last': isLast }">
+ <!-- 鏍戝舰杩炴帴绾� (闈炴牴鑺傜偣鏄剧ず) -->
+ <template v-if="level > 0">
+ <view class="line-v"></view>
+ <view class="line-h"></view>
+ </template>
+ <view class="structure-item-card"
+ :class="{ 'has-children': hasChildren }">
+ <view class="card-main">
+ <view class="item-header"
+ @click="toggleExpand">
+ <view class="header-left">
+ <view v-if="hasChildren"
+ class="expand-icon"
+ :class="{ 'is-expanded': isExpanded }">
+ <up-icon name="arrow-right"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ <view v-else
+ class="dot-icon"></view>
+ <text class="item-title">{{ item.productName || '鏈�夋嫨浜у搧' }}</text>
+ </view>
+ <up-tag v-if="hasChildren"
+ text="缁勫悎"
+ type="primary"
+ size="mini"
+ plain
+ shape="circle" />
+ </view>
+ <view class="item-body">
+ <view class="info-grid">
+ <view class="info-item">
+ <text class="label">瑙勬牸鍨嬪彿锛�</text>
+ <text class="value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">娑堣�楀伐搴忥細</text>
+ <text class="value">{{ getProcessName(item.processId) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鍗曚綅鏁伴噺锛�</text>
+ <text class="value highlight">{{ item.unitQuantity || 0 }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">闇�姹傛�婚噺锛�</text>
+ <text class="value highlight">{{ item.demandedQuantity || 0 }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鍗曚綅锛�</text>
+ <text class="value">{{ item.unit || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鐩樻暟锛�</text>
+ <text class="value">{{ item.diskQuantity || 0 }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <!-- 閫掑綊灞曠ず瀛愯妭鐐� -->
+ <view v-if="hasChildren && isExpanded"
+ class="children-container">
+ <BomStructureItem v-for="(child, index) in item.children"
+ :key="index"
+ :item="child"
+ :level="level + 1"
+ :isLast="index === item.children.length - 1"
+ :processOptions="processOptions" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, computed, defineProps } from "vue";
+
+ const props = defineProps({
+ item: {
+ type: Object,
+ required: true,
+ },
+ level: {
+ type: Number,
+ default: 0,
+ },
+ isLast: {
+ type: Boolean,
+ default: false,
+ },
+ processOptions: {
+ type: Array,
+ default: () => [],
+ },
+ });
+
+ const isExpanded = ref(true);
+ const hasChildren = computed(
+ () => props.item.children && props.item.children.length > 0
+ );
+
+ const toggleExpand = () => {
+ if (hasChildren.value) {
+ isExpanded.value = !isExpanded.value;
+ }
+ };
+
+ const getProcessName = id => {
+ const process = props.processOptions.find(p => p.id === id);
+ return process ? process.name : "-";
+ };
+</script>
+
+<script>
+ export default {
+ name: "BomStructureItem",
+ };
+</script>
+
+<style scoped lang="scss">
+ .structure-item-wrapper {
+ position: relative;
+ padding-left: 44rpx;
+
+ &.is-root {
+ padding-left: 0;
+ }
+ }
+
+ // 鍨傜洿杩炴帴绾挎
+ .line-v {
+ position: absolute;
+ left: 18rpx; // 灞呬腑浜� 44rpx 鐨勭缉杩涘唴
+ top: -20rpx; // 鍚戜笂寤朵几瑕嗙洊涓婁竴涓妭鐐圭殑 margin-bottom
+ bottom: 0;
+ width: 2rpx;
+ background-color: #ddd;
+ z-index: 1;
+ }
+
+ // 鏈�鍚庝竴涓妭鐐圭殑鍨傜洿绾垮彧寤朵几鍒版按骞崇嚎浣嶇疆
+ .is-last > .line-v {
+ bottom: auto;
+ height: 60rpx; // 20rpx (top offset) + 40rpx (to horizontal line)
+ }
+
+ // 姘村钩杩炴帴绾�
+ .line-h {
+ position: absolute;
+ left: 18rpx;
+ top: 40rpx; // 瀵归綈鍒板崱鐗囧唴閮ㄥ浘鏍囦腑蹇� (padding 24 + icon 32/2)
+ width: 26rpx;
+ height: 2rpx;
+ background-color: #ddd;
+ z-index: 1;
+ }
+
+ .structure-item-card {
+ position: relative;
+ background: #fff;
+ border-radius: 16rpx;
+ margin-bottom: 20rpx;
+ padding: 24rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+ border: 1rpx solid #f0f0f0;
+ transition: all 0.3s;
+ z-index: 2;
+
+ &:active {
+ background-color: #f9f9f9;
+ }
+
+ &.has-children {
+ border-left: 6rpx solid #3c9cff;
+ }
+ }
+
+ .card-main {
+ .item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+
+ .header-left {
+ display: flex;
+ align-items: center;
+ flex: 1;
+
+ .expand-icon {
+ margin-right: 12rpx;
+ transition: transform 0.3s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32rpx;
+ height: 32rpx;
+
+ &.is-expanded {
+ transform: rotate(90deg);
+ }
+ }
+
+ .dot-icon {
+ width: 12rpx;
+ height: 12rpx;
+ border-radius: 50%;
+ background-color: #ccc;
+ margin-right: 20rpx;
+ margin-left: 10rpx;
+ }
+
+ .item-title {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ line-height: 1.4;
+ }
+ }
+ }
+
+ .item-body {
+ .info-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 12rpx 20rpx;
+
+ .info-item {
+ display: flex;
+ font-size: 24rpx;
+ line-height: 1.5;
+
+ .label {
+ color: #999;
+ white-space: nowrap;
+ }
+
+ .value {
+ color: #666;
+ word-break: break-all;
+
+ &.highlight {
+ color: #3c9cff;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .children-container {
+ position: relative;
+ }
+</style>
diff --git a/src/pages/productionDesign/bom/index.vue b/src/pages/productionDesign/bom/index.vue
new file mode 100644
index 0000000..97ba5f6
--- /dev/null
+++ b/src/pages/productionDesign/bom/index.vue
@@ -0,0 +1,179 @@
+<template>
+ <view class="bom-list">
+ <PageHeader title="BOM绠$悊"
+ @back="goBack" />
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="queryParams.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @change="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="24"
+ color="#999999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <view v-if="list.length > 0"
+ class="ledger-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="list-dot"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.bomNo || "-" }}</text>
+ </view>
+ <up-tag :text="'V' + (item.version || '1.0')"
+ type="primary"
+ size="mini" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value">{{ item.productName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.productModelName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="goStructure(item)">鏌ョ湅璇︽儏</up-button>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty text="鏆傛棤BOM鏁版嵁"
+ mode="list"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import { listPage } from "@/api/productionManagement/bom";
+
+ const queryParams = reactive({
+ productName: "",
+ });
+ const list = ref([]);
+ const pageStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 3,
+ total: 0,
+ });
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const getList = () => {
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+ pageStatus.value = "loading";
+ listPage({
+ current: page.current,
+ size: page.size,
+ productName: queryParams.productName,
+ })
+ .then(res => {
+ const records = res?.data?.records || res?.records || [];
+ const total = res?.data?.total || res?.total || 0;
+
+ if (page.current === 1) {
+ list.value = records;
+ } else {
+ list.value = [...list.value, ...records];
+ }
+
+ page.total = total;
+ if (list.value.length >= total) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current++;
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "error" });
+ pageStatus.value = "loadmore";
+ });
+ };
+
+ const goStructure = item => {
+ uni.navigateTo({
+ url: `/pages/productionDesign/bom/structure?id=${
+ item.id
+ }&bomNo=${encodeURIComponent(item.bomNo)}&productName=${encodeURIComponent(
+ item.productName || ""
+ )}&productModelName=${encodeURIComponent(
+ item.productModelName || ""
+ )}&remark=${encodeURIComponent(
+ item.remark || ""
+ )}&version=${encodeURIComponent(item.version || 1)}`,
+ });
+ };
+
+ onReachBottom(() => {
+ getList();
+ });
+
+ onShow(() => {
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .no-data {
+ padding-top: 100rpx;
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+ }
+
+ .action-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15rpx;
+ padding: 0 30rpx 30rpx;
+ flex-wrap: wrap;
+ }
+
+ .action-btn {
+ width: calc(50% - 15rpx);
+ margin: 0 !important;
+ margin-bottom: 15rpx !important;
+ }
+</style>
diff --git a/src/pages/productionDesign/bom/structure.vue b/src/pages/productionDesign/bom/structure.vue
new file mode 100644
index 0000000..5b7c2a7
--- /dev/null
+++ b/src/pages/productionDesign/bom/structure.vue
@@ -0,0 +1,100 @@
+<template>
+ <view class="structure-page">
+ <PageHeader :title="'BOM缁撴瀯 - ' + bomNo"
+ @back="goBack" />
+ <view class="info-card">
+ <view class="info-row">
+ <text class="info-label">浜у搧鍚嶇О锛�</text>
+ <text class="info-value">{{ productName }}-{{ productModelName }}</text>
+ </view>
+ </view>
+ <view class="structure-list"
+ v-if="dataList.length > 0">
+ <BomStructureItem v-for="(item, index) in dataList"
+ :key="index"
+ :item="item"
+ :level="0"
+ :isLast="index === dataList.length - 1"
+ :processOptions="processOptions" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty text="鏆傛棤缁撴瀯鏁版嵁"
+ mode="list"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import { queryStructureList } from "@/api/productionManagement/bom";
+ import { list as getProcessList } from "@/api/productionManagement/processManagement";
+ import BomStructureItem from "./BomStructureItem.vue";
+
+ const bomId = ref(null);
+ const bomNo = ref("");
+ const productName = ref("");
+ const dataList = ref([]);
+ const processOptions = ref([]);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const fetchData = () => {
+ queryStructureList(bomId.value).then(res => {
+ dataList.value = res.data || [];
+ });
+ };
+
+ const fetchProcess = () => {
+ getProcessList().then(res => {
+ processOptions.value = res.data || [];
+ });
+ };
+
+ const productModelName = ref("");
+
+ onLoad(options => {
+ bomId.value = options.id;
+ bomNo.value = decodeURIComponent(options.bomNo);
+ productName.value = decodeURIComponent(options.productName);
+ productModelName.value = decodeURIComponent(options.productModelName);
+ fetchData();
+ fetchProcess();
+ });
+</script>
+
+<style scoped lang="scss">
+ .structure-page {
+ background-color: #f5f5f5;
+ min-height: 100vh;
+ padding-bottom: 120rpx;
+ }
+
+ .info-card {
+ background: #fff;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ .info-row {
+ display: flex;
+ font-size: 28rpx;
+ .info-label {
+ color: #666;
+ }
+ .info-value {
+ color: #333;
+ font-weight: bold;
+ }
+ }
+ }
+
+ .structure-list {
+ padding: 20rpx;
+ }
+
+ .no-data {
+ padding-top: 100rpx;
+ }
+</style>
diff --git a/src/pages/productionDesign/processManagement/edit.vue b/src/pages/productionDesign/processManagement/edit.vue
new file mode 100644
index 0000000..425be43
--- /dev/null
+++ b/src/pages/productionDesign/processManagement/edit.vue
@@ -0,0 +1,236 @@
+<template>
+ <view class="process-edit">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ :errorType="['none']"
+ label-width="110">
+ <up-form-item label="宸ュ簭缂栫爜"
+ prop="no">
+ <up-input v-model="form.no"
+ placeholder="璇疯緭鍏ュ伐搴忕紪鐮�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="宸ュ簭鍚嶇О"
+ prop="name"
+ required>
+ <up-input v-model="form.name"
+ placeholder="璇疯緭鍏ュ伐搴忓悕绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="宸ヨ祫瀹氶"
+ prop="salaryQuota">
+ <up-input v-model="form.salaryQuota"
+ type="number"
+ placeholder="璇疯緭鍏ュ伐璧勫畾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璁¤垂绫诲瀷"
+ prop="type">
+ <up-input v-model="typeText"
+ placeholder="璇烽�夋嫨璁¤垂绫诲瀷"
+ readonly
+ @click="showTypeSheet = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showTypeSheet = true"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="鏄惁璐ㄦ"
+ prop="isQuality">
+ <view style="display: flex; justify-content: flex-end; width: 100%;">
+ <up-switch v-model="form.isQuality" />
+ </view>
+ </up-form-item>
+ <up-form-item label="鏄惁鐢熶骇"
+ prop="isProduction">
+ <view style="display: flex; justify-content: flex-end; width: 100%;">
+ <up-switch v-model="form.isProduction" />
+ </view>
+ </up-form-item>
+ <up-form-item label="鍏宠仈璁惧"
+ prop="deviceLedgerId">
+ <up-input v-model="deviceText"
+ placeholder="璇烽�夋嫨鍏宠仈璁惧"
+ readonly
+ @click="showDeviceSheet = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showDeviceSheet = true"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="宸ュ簭鎻忚堪"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ伐搴忔弿杩�"
+ autoHeight />
+ </up-form-item>
+ </up-form>
+ <FooterButtons :loading="loading"
+ :confirmText="processId ? '淇濆瓨' : '鏂板'"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ <!-- 璁¤垂绫诲瀷閫夋嫨 -->
+ <up-action-sheet :show="showTypeSheet"
+ title="閫夋嫨璁¤垂绫诲瀷"
+ :actions="typeActions"
+ @select="onSelectType"
+ @close="showTypeSheet = false" />
+ <!-- 璁惧閫夋嫨 -->
+ <up-action-sheet :show="showDeviceSheet"
+ title="閫夋嫨鍏宠仈璁惧"
+ :actions="deviceActions"
+ @select="onSelectDevice"
+ @close="showDeviceSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref, computed, onMounted } from "vue";
+ import { onLoad, onReady } from "@dcloudio/uni-app";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import {
+ add,
+ update,
+ getDeviceLedger,
+ } from "@/api/productionManagement/processManagement";
+
+ const formRef = ref(null);
+ const loading = ref(false);
+ const processId = ref(null);
+ const pageTitle = computed(() => (processId.value ? "缂栬緫宸ュ簭" : "鏂板宸ュ簭"));
+
+ const form = ref({
+ no: "",
+ name: "",
+ salaryQuota: "",
+ isQuality: false,
+ isProduction: false,
+ remark: "",
+ deviceLedgerId: null,
+ type: 0,
+ });
+
+ const rules = {
+ name: [{ required: true, message: "璇疯緭鍏ュ伐搴忓悕绉�" }],
+ salaryQuota: [
+ {
+ validator: (rule, value, callback) => {
+ if (value !== "" && value !== null && (isNaN(value) || value < 0)) {
+ callback(new Error("宸ヨ祫瀹氶蹇呴』鏄潪璐熸暟瀛�"));
+ } else {
+ callback();
+ }
+ },
+ },
+ ],
+ };
+
+ const showTypeSheet = ref(false);
+ const typeActions = [
+ { name: "璁℃椂", value: 0 },
+ { name: "璁′欢", value: 1 },
+ ];
+ const typeText = computed(() => {
+ const action = typeActions.find(a => a.value === form.value.type);
+ return action ? action.name : "";
+ });
+
+ const showDeviceSheet = ref(false);
+ const deviceActions = ref([]);
+ const deviceText = ref("");
+
+ const onSelectType = e => {
+ form.value.type = e.value;
+ showTypeSheet.value = false;
+ };
+
+ const onSelectDevice = e => {
+ form.value.deviceLedgerId = e.id;
+ deviceText.value = e.name;
+ showDeviceSheet.value = false;
+ };
+
+ const loadDevices = async () => {
+ try {
+ const { data } = await getDeviceLedger();
+ deviceActions.value = (data || []).map(item => ({
+ name: item.deviceName,
+ id: item.id,
+ }));
+ if (form.value.deviceLedgerId) {
+ const device = deviceActions.value.find(
+ d => d.id === Number(form.value.deviceLedgerId)
+ );
+ if (device) deviceText.value = device.name;
+ }
+ } catch (error) {
+ console.error("鍔犺浇璁惧澶辫触", error);
+ }
+ };
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const handleSubmit = () => {
+ formRef.value
+ .validate()
+ .then(() => {
+ loading.value = true;
+ const promise = processId.value ? update(form.value) : add(form.value);
+ promise
+ .then(() => {
+ uni.showToast({ title: processId.value ? "淇濆瓨鎴愬姛" : "鏂板鎴愬姛" });
+ setTimeout(() => {
+ goBack();
+ }, 1500);
+ })
+ .catch(err => {
+ uni.showToast({ title: err.msg || "鎻愪氦澶辫触", icon: "error" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ })
+ .catch(errors => {
+ if (errors && errors.length > 0) {
+ uni.showToast({
+ title: errors[0].message,
+ icon: "none",
+ });
+ }
+ });
+ };
+
+ onLoad(option => {
+ if (option.item) {
+ const item = JSON.parse(decodeURIComponent(option.item));
+ processId.value = item.id;
+ Object.assign(form.value, item);
+ // 澶勭悊绫诲瀷杞崲锛岀‘淇濇槸鏁板瓧
+ form.value.type = Number(form.value.type);
+ form.value.isQuality = !!form.value.isQuality;
+ form.value.isProduction = !!form.value.isProduction;
+ }
+ });
+
+ onReady(() => {
+ formRef.value.setRules(rules);
+ });
+
+ onMounted(() => {
+ loadDevices();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .process-edit {
+ min-height: 100vh;
+ background: #f5f5f5;
+ }
+</style>
\ No newline at end of file
diff --git a/src/pages/productionDesign/processManagement/index.vue b/src/pages/productionDesign/processManagement/index.vue
new file mode 100644
index 0000000..6ca2f76
--- /dev/null
+++ b/src/pages/productionDesign/processManagement/index.vue
@@ -0,0 +1,261 @@
+<template>
+ <view class="sales-account">
+ <PageHeader title="宸ュ簭绠$悊"
+ @back="goBack" />
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ伐搴忓悕绉�"
+ clearable
+ @change="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="handleSearch">
+ <up-icon name="search"
+ size="24"
+ color="#999999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <view v-if="list.length > 0"
+ class="ledger-list">
+ <view v-for="item in list"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="list-dot"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.name || "-" }}</text>
+ </view>
+ <text class="item-index">{{ item.no || "-" }}</text>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍏宠仈璁惧</text>
+ <text class="detail-value">{{ getDeviceName(item.deviceLedgerId) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ヨ祫瀹氶</text>
+ <text class="detail-value highlight">楼{{ item.salaryQuota || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭鐘舵��</text>
+ <view class="detail-value">
+ <up-tag :text="item.isQuality ? '璐ㄦ' : '闈炶川妫�'"
+ :type="item.isQuality ? 'warning' : 'info'"
+ size="mini"
+ style="margin-left: 8rpx" />
+ <up-tag :text="item.isProduction ? '鐢熶骇' : '涓嶇敓浜�'"
+ :type="item.isProduction ? 'warning' : 'info'"
+ size="mini"
+ style="margin-left: 8rpx" />
+ <up-tag v-if="item.type !== null && item.type !== undefined"
+ :text="item.type == 0 ? '璁℃椂' : '璁′欢'"
+ :type="item.type == 1 ? 'primary' : 'success'"
+ size="mini"
+ style="margin-left: 8rpx" />
+ </view>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="goEdit(item)">缂栬緫</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="warning"
+ @click="goParams(item)">鍙傛暟閰嶇疆</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ @click="handleDelete(item)">鍒犻櫎</up-button>
+ </view>
+ </view>
+ <up-loadmore :status="pageStatus" />
+ </view>
+ <view v-else
+ class="no-data">
+ <text>鏆傛棤宸ュ簭鏁版嵁</text>
+ </view>
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onReachBottom, onShow } from "@dcloudio/uni-app";
+ import {
+ getProcessList,
+ del,
+ getDeviceLedger,
+ } from "@/api/productionManagement/processManagement";
+
+ const queryParams = reactive({
+ name: "",
+ });
+ const list = ref([]);
+ const deviceOptions = ref([]);
+ const pageStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const getDeviceName = deviceId => {
+ if (!deviceId) return "鏈叧鑱�";
+ const device = deviceOptions.value.find(item => item.id === Number(deviceId));
+ return device?.deviceName || "鏈叧鑱�";
+ };
+
+ const loadDevices = async () => {
+ try {
+ const { data } = await getDeviceLedger();
+ deviceOptions.value = data || [];
+ } catch (error) {
+ console.error("鍔犺浇璁惧鍒楄〃澶辫触", error);
+ }
+ };
+
+ const handleSearch = () => {
+ page.current = 1;
+ pageStatus.value = "loadmore";
+ list.value = [];
+ getList();
+ };
+
+ const getList = () => {
+ if (pageStatus.value === "loading" || pageStatus.value === "nomore") return;
+
+ pageStatus.value = "loading";
+ getProcessList({
+ current: page.current,
+ size: page.size,
+ name: queryParams.name,
+ })
+ .then(res => {
+ const records = res?.data?.records || res?.records || [];
+ const total = res?.data?.total || res?.total || 0;
+
+ if (page.current === 1) {
+ list.value = records;
+ } else {
+ list.value = [...list.value, ...records];
+ }
+
+ page.total = total;
+ if (list.value.length >= total) {
+ pageStatus.value = "nomore";
+ } else {
+ pageStatus.value = "loadmore";
+ page.current++;
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "error" });
+ pageStatus.value = "loadmore";
+ });
+ };
+
+ const goAdd = () => {
+ uni.navigateTo({ url: "/pages/productionDesign/processManagement/edit" });
+ };
+
+ const goEdit = item => {
+ uni.navigateTo({
+ url: `/pages/productionDesign/processManagement/edit?item=${encodeURIComponent(
+ JSON.stringify(item)
+ )}`,
+ });
+ };
+
+ const goParams = item => {
+ uni.navigateTo({
+ url: `/pages/productionDesign/processManagement/params?id=${item.id}&name=${encodeURIComponent(item.name)}`,
+ });
+ };
+
+ const handleDelete = item => {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "纭畾瑕佸垹闄よ宸ュ簭鍚楋紵",
+ success: res => {
+ if (res.confirm) {
+ del([item.id]).then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛" });
+ handleSearch();
+ });
+ }
+ },
+ });
+ };
+
+ onReachBottom(() => {
+ getList();
+ });
+
+ onShow(async () => {
+ await loadDevices();
+ handleSearch();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .no-data {
+ padding-top: 100rpx;
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+ }
+
+ .action-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 20rpx;
+ padding-bottom: 30rpx;
+ }
+
+ .action-btn {
+ flex: 1;
+ margin: 0 !important;
+ }
+
+ .fab-button {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #2979ff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.4);
+ z-index: 100;
+ }
+</style>
\ No newline at end of file
diff --git a/src/pages/productionDesign/processManagement/params.vue b/src/pages/productionDesign/processManagement/params.vue
new file mode 100644
index 0000000..0fa0bc0
--- /dev/null
+++ b/src/pages/productionDesign/processManagement/params.vue
@@ -0,0 +1,413 @@
+<template>
+ <view class="process-params">
+ <PageHeader :title="processName + ' - 鍙傛暟閰嶇疆'"
+ @back="goBack" />
+ <view class="ledger-list">
+ <view v-if="paramList.length > 0">
+ <view v-for="item in paramList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="setting-fill"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.paramName || "-" }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鏍囧噯鍊�</text>
+ <text class="detail-value highlight">{{ item.standardValue || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ item.unit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙傛暟绫诲瀷</text>
+ <up-tag :text="getParamTypeText(item.paramType)"
+ :type="getParamTypeTag(item.paramType)"
+ size="mini" />
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙栧�兼牸寮�</text>
+ <text class="detail-value">{{ item.paramFormat || "-" }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="handleEditParam(item)">缂栬緫</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ @click="handleDeleteParam(item)">鍒犻櫎</up-button>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty text="鏆傛棤鍙傛暟閰嶇疆"
+ icon="account"
+ iconSize="60"></up-empty>
+ </view>
+ </view>
+ <!-- 娴姩鏂板鎸夐挳 -->
+ <view class="fab-button"
+ @click="openSelectModal">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+ <!-- 閫夋嫨鍙傛暟寮圭獥 -->
+ <up-modal :show="selectModalVisible"
+ title="閫夋嫨鍙傛暟"
+ width="650rpx"
+ @confirm="handleSelectSubmit"
+ @cancel="selectModalVisible = false"
+ :closeOnClickOverlay="false"
+ showCancelButton>
+ <view class="modal-content">
+ <view class="search-box">
+ <up-input v-model="searchKeyword"
+ placeholder="鎼滅储鍩虹鍙傛暟鍚嶇О"
+ clearable
+ @confirm="handleSearch"
+ @change="handleSearch" />
+ </view>
+ <scroll-view scroll-y
+ class="param-scroll-list"
+ @scrolltolower="loadMoreParams">
+ <view v-for="param in availableParams"
+ :key="param.id"
+ class="param-select-item"
+ :class="{ active: selectedBaseParam?.id === param.id }"
+ @click="selectParam(param)">
+ <view class="param-main">
+ <text class="param-name">{{ param.paramName }}</text>
+ <up-tag :text="getParamTypeText(param.paramType)"
+ :type="getParamTypeTag(param.paramType)"
+ size="mini" />
+ </view>
+ <text class="param-code">{{ param.paramCode }}</text>
+ </view>
+ <up-loadmore :status="availablePageStatus" />
+ </scroll-view>
+ <view v-if="selectedBaseParam"
+ class="standard-input-box">
+ <text class="label">鏍囧噯鍊硷細</text>
+ <up-input v-model="selectedStandardValue"
+ placeholder="璇疯緭鍏ヨ宸ュ簭鐨勬爣鍑嗗��" />
+ </view>
+ </view>
+ </up-modal>
+ <!-- 缂栬緫鍙傛暟鏍囧噯鍊煎脊绐� -->
+ <up-modal :show="editModalVisible"
+ title="缂栬緫鏍囧噯鍊�"
+ width="500rpx"
+ @confirm="handleEditSubmit"
+ @cancel="editModalVisible = false"
+ :closeOnClickOverlay="false"
+ showCancelButton>
+ <view class="modal-content">
+ <view class="edit-info">
+ <text class="edit-label">鍙傛暟锛歿{ currentEditParam?.paramName }}</text>
+ <up-input v-model="currentEditValue"
+ placeholder="璇疯緭鍏ユ柊鐨勬爣鍑嗗��" />
+ </view>
+ </view>
+ </up-modal>
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref, onMounted } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import {
+ getProcessParamList,
+ addProcessParam,
+ editProcessParam,
+ deleteProcessParam,
+ getBaseParamList,
+ } from "@/api/productionManagement/processManagement";
+
+ const processId = ref(null);
+ const processName = ref("");
+ const paramList = ref([]);
+ const loading = ref(false);
+
+ // 閫夋嫨鍙傛暟鐩稿叧
+ const selectModalVisible = ref(false);
+ const availableParams = ref([]);
+ const searchKeyword = ref("");
+ const selectedBaseParam = ref(null);
+ const selectedStandardValue = ref("");
+ const availablePage = reactive({ current: 1, size: 20, total: 0 });
+ const availablePageStatus = ref("loadmore");
+
+ // 缂栬緫鍙傛暟鐩稿叧
+ const editModalVisible = ref(false);
+ const currentEditParam = ref(null);
+ const currentEditValue = ref("");
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const getParamList = () => {
+ loading.value = true;
+ getProcessParamList({ technologyOperationId: processId.value })
+ .then(res => {
+ paramList.value = res?.data || [];
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇鍒楄〃澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ const openSelectModal = () => {
+ searchKeyword.value = "";
+ selectedBaseParam.value = null;
+ selectedStandardValue.value = "";
+ availableParams.value = [];
+ availablePage.current = 1;
+ availablePageStatus.value = "loadmore";
+ selectModalVisible.value = true;
+ loadAvailableParams(true);
+ };
+
+ const handleSearch = () => {
+ availablePage.current = 1;
+ availableParams.value = [];
+ availablePageStatus.value = "loadmore";
+ loadAvailableParams(true);
+ };
+
+ const loadMoreParams = () => {
+ if (
+ availablePageStatus.value === "nomore" ||
+ availablePageStatus.value === "loading"
+ )
+ return;
+ loadAvailableParams(false);
+ };
+
+ const loadAvailableParams = (isReset = false) => {
+ if (availablePageStatus.value === "loading") return;
+ if (isReset) {
+ availablePage.current = 1;
+ availableParams.value = [];
+ availablePageStatus.value = "loading";
+ } else if (availablePageStatus.value === "nomore") {
+ return;
+ } else {
+ availablePageStatus.value = "loading";
+ }
+ getBaseParamList({
+ paramName: searchKeyword.value,
+ current: availablePage.current,
+ size: availablePage.size,
+ })
+ .then(res => {
+ const records = res?.data?.records || res?.records || [];
+ const total = res?.data?.total || res?.total || 0;
+
+ if (isReset || availablePage.current === 1) {
+ availableParams.value = records;
+ } else {
+ availableParams.value = [...availableParams.value, ...records];
+ }
+
+ availablePage.total = total;
+ if (availableParams.value.length >= total) {
+ availablePageStatus.value = "nomore";
+ } else {
+ availablePageStatus.value = "loadmore";
+ availablePage.current++;
+ }
+ })
+ .catch(() => {
+ availablePageStatus.value = "loadmore";
+ });
+ };
+
+ const selectParam = param => {
+ selectedBaseParam.value = param;
+ selectedStandardValue.value = param.standardValue || "";
+ };
+
+ const handleSelectSubmit = () => {
+ if (!selectedBaseParam.value) {
+ uni.showToast({ title: "璇烽�夋嫨涓�涓熀纭�鍙傛暟", icon: "none" });
+ return;
+ }
+ if (!selectedStandardValue.value) {
+ uni.showToast({ title: "璇疯緭鍏ユ爣鍑嗗��", icon: "none" });
+ return;
+ }
+ addProcessParam({
+ technologyOperationId: processId.value,
+ technologyParamId: selectedBaseParam.value.id,
+ standardValue: selectedStandardValue.value,
+ })
+ .then(() => {
+ uni.showToast({ title: "娣诲姞鎴愬姛" });
+ selectModalVisible.value = false;
+ getParamList();
+ })
+ .catch(err => {
+ uni.showToast({ title: err.msg || "娣诲姞澶辫触", icon: "error" });
+ });
+ };
+
+ const handleEditParam = item => {
+ currentEditParam.value = item;
+ currentEditValue.value = item.standardValue;
+ editModalVisible.value = true;
+ };
+
+ const handleEditSubmit = () => {
+ if (!currentEditValue.value) {
+ uni.showToast({ title: "璇疯緭鍏ユ爣鍑嗗��", icon: "none" });
+ return;
+ }
+ editProcessParam({
+ id: currentEditParam.value.id,
+ technologyOperationId: processId.value,
+ technologyParamId: currentEditParam.value.technologyParamId,
+ standardValue: currentEditValue.value,
+ })
+ .then(() => {
+ uni.showToast({ title: "淇敼鎴愬姛" });
+ editModalVisible.value = false;
+ getParamList();
+ })
+ .catch(err => {
+ uni.showToast({ title: err.msg || "淇敼澶辫触", icon: "error" });
+ });
+ };
+
+ const handleDeleteParam = item => {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "纭畾瑕佸垹闄よ鍙傛暟閰嶇疆鍚楋紵",
+ success: res => {
+ if (res.confirm) {
+ deleteProcessParam(item.id).then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛" });
+ getParamList();
+ });
+ }
+ },
+ });
+ };
+
+ const getParamTypeText = type => {
+ const typeMap = { 1: "鏁板��", 2: "鏂囨湰", 3: "涓嬫媺", 4: "鏃堕棿" };
+ return typeMap[type] || "鏈煡";
+ };
+
+ const getParamTypeTag = type => {
+ const typeMap = { 1: "primary", 2: "info", 3: "warning", 4: "success" };
+ return typeMap[type] || "default";
+ };
+
+ onLoad(option => {
+ if (option.id) {
+ processId.value = option.id;
+ processName.value = decodeURIComponent(option.name || "");
+ getParamList();
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .process-params {
+ min-height: 100vh;
+ background: #f5f5f5;
+ }
+
+ .modal-content {
+ padding: 20rpx 0;
+ width: 100%;
+ }
+
+ .param-scroll-list {
+ height: 500rpx;
+ margin-top: 20rpx;
+ border: 1px solid #eee;
+ border-radius: 8rpx;
+ }
+
+ .param-select-item {
+ padding: 20rpx;
+ border-bottom: 1px solid #f5f5f5;
+ width: 100%;
+ &.active {
+ background-color: #e3f2fd;
+ }
+ }
+
+ .param-main {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8rpx;
+ }
+
+ .param-name {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .param-code {
+ font-size: 24rpx;
+ color: #999;
+ }
+
+ .standard-input-box {
+ margin-top: 30rpx;
+ display: flex;
+ align-items: center;
+ .label {
+ width: 120rpx;
+ font-size: 28rpx;
+ color: #333;
+ }
+ }
+
+ .edit-info {
+ .edit-label {
+ display: block;
+ margin-bottom: 20rpx;
+ font-size: 28rpx;
+ color: #666;
+ }
+ }
+
+ .fab-button {
+ position: fixed;
+ right: 40rpx;
+ bottom: 60rpx;
+ width: 100rpx;
+ height: 100rpx;
+ background: #2979ff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.4);
+ z-index: 100;
+ }
+</style>
\ No newline at end of file
diff --git a/src/pages/productionManagement/mainProductionPlan/detail.vue b/src/pages/productionManagement/mainProductionPlan/detail.vue
new file mode 100644
index 0000000..867dfe6
--- /dev/null
+++ b/src/pages/productionManagement/mainProductionPlan/detail.vue
@@ -0,0 +1,252 @@
+<template>
+ <view class="production-plan-detail">
+ <PageHeader title="璁″垝璇︽儏"
+ @back="goBack" />
+ <view class="detail-container"
+ v-if="detailData">
+ <!-- 鍩烘湰淇℃伅鍗$墖 -->
+ <view class="detail-card">
+ <view class="card-title">
+ <up-icon name="info-circle"
+ size="18"
+ color="#3c9cff"></up-icon>
+ <text class="title-text">鍩烘湰淇℃伅</text>
+ </view>
+ <view class="card-content">
+ <view class="info-item">
+ <text class="label">涓荤敓浜ц鍒掑彿</text>
+ <text class="value">{{ detailData.mpsNo || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鏉ユ簮</text>
+ <up-tag :text="detailData.source === '閿�鍞�' ? '閿�鍞�' : '鍐呴儴'"
+ :type="detailData.source === '閿�鍞�' ? 'primary' : 'info'"
+ size="mini" />
+ </view>
+ <view class="info-item">
+ <text class="label">涓嬪彂鐘舵��</text>
+ <up-tag :text="getStatusText(detailData.status)"
+ :type="getStatusType(detailData.status)"
+ size="mini" />
+ </view>
+ </view>
+ </view>
+ <!-- 浜у搧淇℃伅鍗$墖 -->
+ <view class="detail-card">
+ <view class="card-title">
+ <up-icon name="order"
+ size="18"
+ color="#3c9cff"></up-icon>
+ <text class="title-text">浜у搧淇℃伅</text>
+ </view>
+ <view class="card-content">
+ <view class="info-item">
+ <text class="label">浜у搧鍚嶇О</text>
+ <text class="value font-bold">{{ detailData.productName || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">瑙勬牸鍨嬪彿</text>
+ <text class="value">{{ detailData.model || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鎵�闇�鏁伴噺</text>
+ <text class="value highlight">{{ detailData.qtyRequired || 0 }} {{ detailData.unit || '鏂�' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">宸蹭笅鍙戞暟閲�</text>
+ <text class="value">{{ detailData.quantityIssued || 0 }} {{ detailData.unit || '鏂�' }}</text>
+ </view>
+ </view>
+ </view>
+ <!-- 鏃ユ湡涓庡叧鑱斿崱鐗� -->
+ <view class="detail-card">
+ <view class="card-title">
+ <up-icon name="calendar"
+ size="18"
+ color="#3c9cff"></up-icon>
+ <text class="title-text">鏃ユ湡涓庡叧鑱�</text>
+ </view>
+ <view class="card-content">
+ <view class="info-item">
+ <text class="label">闇�姹傛棩鏈�</text>
+ <text class="value">{{ formatDate(detailData.requiredDate) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鎵胯鏃ユ湡</text>
+ <text class="value">{{ formatDate(detailData.promisedDeliveryDate) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">閿�鍞悎鍚屽彿</text>
+ <text class="value">{{ detailData.salesContractNo || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">瀹㈡埛鍚嶇О</text>
+ <text class="value">{{ detailData.customerName || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">椤圭洰鍚嶇О</text>
+ <text class="value">{{ detailData.projectName || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ <!-- 澶囨敞淇℃伅 -->
+ <view class="detail-card">
+ <view class="card-title">
+ <up-icon name="edit-pen"
+ size="18"
+ color="#3c9cff"></up-icon>
+ <text class="title-text">澶囨敞</text>
+ </view>
+ <view class="card-content">
+ <view class="remark-box">
+ <text class="remark-text">{{ detailData.remark || '鏃犲娉�' }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤璇︽儏鏁版嵁"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ import PageHeader from "@/components/PageHeader.vue";
+
+ const detailData = ref(null);
+
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 鏍煎紡鍖栨棩鏈�
+ const formatDate = date => {
+ return date ? dayjs(date).format("YYYY-MM-DD") : "-";
+ };
+
+ // 鑾峰彇鐘舵�佹枃鏈�
+ const getStatusText = status => {
+ const statusMap = {
+ 0: "寰呬笅鍙�",
+ 1: "閮ㄥ垎涓嬪彂",
+ 2: "宸蹭笅鍙�",
+ };
+ return statusMap[status] || "鏈煡";
+ };
+
+ // 鑾峰彇鐘舵�佺被鍨�
+ const getStatusType = status => {
+ const typeMap = {
+ 0: "warning",
+ 1: "primary",
+ 2: "info",
+ };
+ return typeMap[status] || "info";
+ };
+
+ onLoad(options => {
+ if (options.data) {
+ try {
+ detailData.value = JSON.parse(decodeURIComponent(options.data));
+ } catch (e) {
+ console.error("瑙f瀽鏁版嵁澶辫触", e);
+ uni.showToast({
+ title: "鏁版嵁鍔犺浇澶辫触",
+ icon: "error",
+ });
+ }
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ .production-plan-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 40rpx;
+ }
+
+ .detail-container {
+ padding: 20rpx;
+ }
+
+ .detail-card {
+ background: #fff;
+ border-radius: 16rpx;
+ margin-bottom: 24rpx;
+ overflow: hidden;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
+
+ .card-title {
+ display: flex;
+ align-items: center;
+ padding: 24rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+ background: #fafafa;
+
+ .title-text {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ margin-left: 12rpx;
+ }
+ }
+
+ .card-content {
+ padding: 10rpx 24rpx;
+
+ .info-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20rpx 0;
+ border-bottom: 1rpx solid #f9f9f9;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .label {
+ font-size: 26rpx;
+ color: #999;
+ }
+
+ .value {
+ font-size: 26rpx;
+ color: #333;
+ text-align: right;
+ max-width: 70%;
+
+ &.font-bold {
+ font-weight: bold;
+ }
+
+ &.highlight {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+ }
+ }
+
+ .remark-box {
+ padding: 20rpx 0;
+
+ .remark-text {
+ font-size: 26rpx;
+ color: #666;
+ line-height: 1.5;
+ }
+ }
+ }
+ }
+
+ .no-data {
+ padding-top: 200rpx;
+ }
+</style>
diff --git a/src/pages/productionManagement/mainProductionPlan/index.vue b/src/pages/productionManagement/mainProductionPlan/index.vue
new file mode 100644
index 0000000..5b4eec1
--- /dev/null
+++ b/src/pages/productionManagement/mainProductionPlan/index.vue
@@ -0,0 +1,300 @@
+<template>
+ <view class="main-production-plan">
+ <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
+ <PageHeader title="涓荤敓浜ц鍒�" @back="goBack" />
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input
+ class="search-text"
+ placeholder="璇疯緭鍏ヨ鍒掑彿鎴栦骇鍝佸悕绉�"
+ v-model="searchForm.keyword"
+ @change="handleQuery"
+ clearable
+ />
+ </view>
+ <view class="filter-button" @click="handleQuery">
+ <up-icon name="search" size="24" color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍒楄〃鍖哄煙 -->
+ <scroll-view scroll-y class="list-container" v-if="tableData.length > 0" @scrolltolower="loadMore">
+ <view v-for="(item, index) in tableData" :key="item.id || index" @click="goDetail(item)">
+ <view class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.mpsNo }}</text>
+ </view>
+ <view class="item-right">
+ <up-tag :text="getStatusText(item.status)" :type="getStatusType(item.status)" size="mini" />
+ </view>
+ </view>
+ <up-divider></up-divider>
+
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value">{{ item.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵�闇�鏁伴噺</text>
+ <text class="detail-value highlight">{{ item.qtyRequired || 0 }} {{ item.unit || '鏂�' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">闇�姹傛棩鏈�</text>
+ <text class="detail-value">{{ formatDate(item.requiredDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏉ユ簮</text>
+ <text class="detail-value">{{ item.source === '閿�鍞�' ? '閿�鍞�' : '鍐呴儴' }}</text>
+ </view>
+ </view>
+ <view class="item-footer">
+ <text class="more-detail">鏌ョ湅璇︽儏</text>
+ <up-icon name="arrow-right" size="14" color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus" v-if="tableData.length >= page.size" />
+ </scroll-view>
+
+ <view v-else class="no-data">
+ <up-empty mode="data" text="鏆傛棤涓荤敓浜ц鍒掓暟鎹�"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref, reactive, toRefs, getCurrentInstance } from "vue";
+import { onShow } from '@dcloudio/uni-app';
+import dayjs from "dayjs";
+import { productionPlanListPage } from "@/api/productionManagement/productionPlan.js";
+import PageHeader from "@/components/PageHeader.vue";
+
+const { proxy } = getCurrentInstance();
+
+// 鍔犺浇鐘舵��
+const loading = ref(false);
+const loadStatus = ref('loadmore');
+// 鍒楄〃鏁版嵁
+const tableData = ref([]);
+
+// 鍒嗛〉閰嶇疆
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+
+// 鎼滅储琛ㄥ崟鏁版嵁
+const data = reactive({
+ searchForm: {
+ keyword: "",
+ mpsNo: "",
+ productName: ""
+ },
+});
+const { searchForm } = toRefs(data);
+
+// 杩斿洖涓婁竴椤�
+const goBack = () => {
+ uni.navigateBack();
+};
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (date) => {
+ return date ? dayjs(date).format('YYYY-MM-DD') : '-';
+};
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ 0: "寰呬笅鍙�",
+ 1: "閮ㄥ垎涓嬪彂",
+ 2: "宸蹭笅鍙�",
+ };
+ return statusMap[status] || "鏈煡";
+};
+
+// 鑾峰彇鐘舵�佺被鍨� (uView tag type)
+const getStatusType = (status) => {
+ const typeMap = {
+ 0: "warning",
+ 1: "primary",
+ 2: "info",
+ };
+ return typeMap[status] || "info";
+};
+
+// 鏌ヨ鍒楄〃
+const handleQuery = () => {
+ page.current = 1;
+ tableData.value = [];
+ getList();
+};
+
+// 鍔犺浇鏇村
+const loadMore = () => {
+ if (loadStatus.value === 'nomore' || loading.value) return;
+ page.current++;
+ getList();
+};
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = () => {
+ loading.value = true;
+ loadStatus.value = 'loading';
+
+ // 鏋勯�犺姹傚弬鏁�
+ // PC绔帴鍙f敮鎸� mpsNo, productName 绛夛紝杩欓噷绠�鍗曞鐞嗭紝濡傛灉 keyword 瀛樺湪锛屽垯灏濊瘯鍖归厤
+ const params = {
+ current: page.current,
+ size: page.size,
+ mpsNo: searchForm.value.keyword, // 绠�鍗曞鐞嗭細鎼滅储鍙�
+ productName: searchForm.value.keyword // 绠�鍗曞鐞嗭細鎼滅储鍚嶇О
+ };
+
+ productionPlanListPage(params).then((res) => {
+ loading.value = false;
+ const records = res.data.records || [];
+ if (page.current === 1) {
+ tableData.value = records;
+ } else {
+ tableData.value = [...tableData.value, ...records];
+ }
+
+ if (records.length < page.size) {
+ loadStatus.value = 'nomore';
+ } else {
+ loadStatus.value = 'loadmore';
+ }
+ page.total = res.data.total || 0;
+ }).catch(() => {
+ loading.value = false;
+ loadStatus.value = 'loadmore';
+ uni.showToast({
+ title: '鍔犺浇澶辫触',
+ icon: 'error'
+ });
+ });
+};
+
+// 璺宠浆璇︽儏
+const goDetail = (item) => {
+ uni.navigateTo({
+ url: `/pages/productionManagement/mainProductionPlan/detail?data=${encodeURIComponent(JSON.stringify(item))}`
+ });
+};
+
+// 椤甸潰鏄剧ず鏃跺姞杞芥暟鎹�
+onShow(() => {
+ handleQuery();
+});
+</script>
+
+<style scoped lang="scss">
+@import '@/styles/sales-common.scss';
+
+.main-production-plan {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+}
+
+.list-container {
+ flex: 1;
+ height: 0;
+}
+
+.ledger-item {
+ background: #fff;
+ margin: 20rpx;
+ padding: 20rpx;
+ border-radius: 12rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ .item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 10rpx;
+
+ .item-left {
+ display: flex;
+ align-items: center;
+
+ .document-icon {
+ width: 40rpx;
+ height: 40rpx;
+ background: #3c9cff;
+ border-radius: 8rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-right: 16rpx;
+ }
+
+ .item-id {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+ }
+
+ .item-details {
+ padding: 10rpx 0;
+
+ .detail-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+
+ .detail-label {
+ font-size: 26rpx;
+ color: #999;
+ }
+
+ .detail-value {
+ font-size: 26rpx;
+ color: #333;
+
+ &.highlight {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+
+ .item-footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding-top: 16rpx;
+ border-top: 1rpx solid #f0f0f0;
+
+ .more-detail {
+ font-size: 24rpx;
+ color: #999;
+ margin-right: 8rpx;
+ }
+ }
+}
+
+.no-data {
+ padding-top: 200rpx;
+}
+</style>
diff --git a/src/pages/productionManagement/processRoute/index.vue b/src/pages/productionManagement/processRoute/index.vue
new file mode 100644
index 0000000..b993d99
--- /dev/null
+++ b/src/pages/productionManagement/processRoute/index.vue
@@ -0,0 +1,287 @@
+<template>
+ <view class="process-route">
+ <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
+ <PageHeader title="宸ヨ壓璺嚎"
+ @back="goBack" />
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ヨ鏍煎悕绉版悳绱�"
+ v-model="searchForm.model"
+ @change="handleQuery"
+ clearable />
+ </view>
+ <view class="filter-button"
+ @click="handleQuery">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <!-- 鍒楄〃鍖哄煙 -->
+ <scroll-view scroll-y
+ class="list-container"
+ v-if="tableData.length > 0"
+ @scrolltolower="loadMore">
+ <view v-for="(item, index) in tableData"
+ :key="item.id || index"
+ @click="goDetail(item)">
+ <view class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="share-square"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.processRouteCode }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍚嶇О</text>
+ <text class="detail-value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">BOM缂栧彿</text>
+ <text class="detail-value">{{ item.bomNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎻忚堪</text>
+ <text class="detail-value">{{ item.description || '-' }}</text>
+ </view>
+ </view>
+ <view class="item-footer">
+ <text class="more-detail">璺嚎椤圭洰</text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus"
+ v-if="tableData.length >= page.size" />
+ </scroll-view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤宸ヨ壓璺嚎鏁版嵁"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, toRefs, getCurrentInstance } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import { listPage } from "@/api/productionManagement/processRoute.js";
+ import PageHeader from "@/components/PageHeader.vue";
+
+ const { proxy } = getCurrentInstance();
+
+ // 鍔犺浇鐘舵��
+ const loading = ref(false);
+ const loadStatus = ref("loadmore");
+ // 鍒楄〃鏁版嵁
+ const tableData = ref([]);
+
+ // 鍒嗛〉閰嶇疆
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ // 鎼滅储琛ㄥ崟鏁版嵁
+ const data = reactive({
+ searchForm: {
+ model: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 鏌ヨ鍒楄〃
+ const handleQuery = () => {
+ page.current = 1;
+ tableData.value = [];
+ getList();
+ };
+
+ // 鍔犺浇鏇村
+ const loadMore = () => {
+ if (loadStatus.value === "nomore" || loading.value) return;
+ page.current++;
+ getList();
+ };
+
+ // 鑾峰彇鍒楄〃鏁版嵁
+ const getList = () => {
+ loading.value = true;
+ loadStatus.value = "loading";
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ model: searchForm.value.model,
+ };
+
+ listPage(params)
+ .then(res => {
+ loading.value = false;
+ const records = res.data.records || [];
+ if (page.current === 1) {
+ tableData.value = records;
+ } else {
+ tableData.value = [...tableData.value, ...records];
+ }
+
+ if (records.length < page.size) {
+ loadStatus.value = "nomore";
+ } else {
+ loadStatus.value = "loadmore";
+ }
+ page.total = res.data.total || 0;
+ })
+ .catch(() => {
+ loading.value = false;
+ loadStatus.value = "loadmore";
+ uni.showToast({
+ title: "鍔犺浇澶辫触",
+ icon: "error",
+ });
+ });
+ };
+
+ // 璺宠浆璺嚎椤圭洰
+ const goDetail = item => {
+ uni.navigateTo({
+ url: `/pages/productionManagement/processRoute/items?id=${
+ item.id
+ }&processRouteCode=${
+ item.processRouteCode
+ }&productName=${encodeURIComponent(
+ item.productName || ""
+ )}&model=${encodeURIComponent(item.model || "")}&bomNo=${
+ item.bomNo || ""
+ }&bomId=${item.bomId || ""}&description=${encodeURIComponent(
+ item.description || ""
+ )}`,
+ });
+ };
+
+ // 椤甸潰鏄剧ず鏃跺姞杞芥暟鎹�
+ onShow(() => {
+ handleQuery();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .process-route {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .list-container {
+ flex: 1;
+ height: 0;
+ }
+
+ .ledger-item {
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+
+ .item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 12rpx;
+
+ .item-left {
+ display: flex;
+ align-items: center;
+
+ .document-icon {
+ width: 44rpx;
+ height: 44rpx;
+ background: #3c9cff;
+ border-radius: 10rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-right: 20rpx;
+ }
+
+ .item-id {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+ }
+
+ .item-details {
+ padding: 16rpx 0;
+
+ .detail-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 16rpx;
+
+ .detail-label {
+ font-size: 26rpx;
+ color: #999;
+ min-width: 140rpx;
+ }
+
+ .detail-value {
+ font-size: 26rpx;
+ color: #333;
+ text-align: right;
+ flex: 1;
+
+ &.font-bold {
+ font-weight: bold;
+ }
+ }
+ }
+ }
+
+ .item-footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding-top: 16rpx;
+ border-top: 1rpx solid #f0f0f0;
+
+ .more-detail {
+ font-size: 24rpx;
+ color: #3c9cff;
+ margin-right: 8rpx;
+ }
+ }
+ }
+
+ .no-data {
+ padding-top: 200rpx;
+ }
+</style>
diff --git a/src/pages/productionManagement/processRoute/items.vue b/src/pages/productionManagement/processRoute/items.vue
new file mode 100644
index 0000000..9c2b266
--- /dev/null
+++ b/src/pages/productionManagement/processRoute/items.vue
@@ -0,0 +1,554 @@
+<template>
+ <view class="process-route-items">
+ <PageHeader title="璺嚎椤圭洰"
+ @back="goBack" />
+ <!-- 璺嚎鍩虹淇℃伅鍗$墖 -->
+ <view class="route-info-card">
+ <view class="info-row">
+ <text class="label">宸ヨ壓璺嚎缂栧彿</text>
+ <text class="value">{{ routeInfo.processRouteCode || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">浜у搧鍚嶇О</text>
+ <text class="value">{{ routeInfo.productName || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">瑙勬牸鍚嶇О</text>
+ <text class="value">{{ routeInfo.model || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">BOM缂栧彿</text>
+ <text class="value">{{ routeInfo.bomNo || '-' }}</text>
+ </view>
+ </view>
+ <!-- 閫夐」鍗″垏鎹� -->
+ <view class="tabs-box">
+ <up-tabs :list="tabsList"
+ @click="handleTabClick"
+ :current="currentTab"></up-tabs>
+ </view>
+ <!-- 宸ュ簭椤圭洰鍒楄〃 -->
+ <scroll-view scroll-y
+ class="content-scroll"
+ v-if="currentTab === 0">
+ <view v-if="itemsList.length > 0">
+ <view v-for="(item, index) in itemsList"
+ :key="index"
+ class="process-card">
+ <view class="card-header">
+ <view class="index-badge">{{ index + 1 }}</view>
+ <text class="process-name">{{ item.technologyOperationName || item.operationName || '-' }}</text>
+ </view>
+ <view class="card-content">
+ <view class="detail-row">
+ <text class="detail-label">鍏宠仈浜у搧</text>
+ <text class="detail-value">{{ item.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ item.unit || '-' }}</text>
+ </view>
+ <view class="tag-row">
+ <up-tag v-if="item.isQuality"
+ text="璐ㄦ"
+ type="primary"
+ size="mini"
+ plain />
+ <up-tag v-if="item.isProduction"
+ text="鐢熶骇"
+ type="success"
+ size="mini"
+ plain />
+ <up-tag v-if="item.type==0"
+ text="璁℃椂"
+ type="info"
+ size="mini"
+ plain />
+ <up-tag v-else
+ text="璁′欢"
+ type="warning"
+ size="mini"
+ plain />
+ </view>
+ </view>
+ <view class="card-footer"
+ @click="showParams(item)">
+ <text class="action-text">鏌ョ湅鍙傛暟鍒楄〃</text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#3c9cff"></up-icon>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤璺嚎椤圭洰"></up-empty>
+ </view>
+ </scroll-view>
+ <!-- BOM 缁撴瀯灞曠ず -->
+ <scroll-view scroll-y
+ class="content-scroll"
+ v-if="currentTab === 1">
+ <view v-if="bomList.length > 0"
+ class="bom-tree">
+ <view v-for="(node, nIndex) in flatBomList"
+ :key="nIndex"
+ class="bom-node"
+ :style="{ paddingLeft: (node.level * 40) + 'rpx' }">
+ <view class="bom-node-inner">
+ <view class="bom-line"
+ v-if="node.level > 0"></view>
+ <view class="bom-content">
+ <view class="bom-header">
+ <text class="bom-product">{{ node.productName }}</text>
+ <text class="bom-model"
+ v-if="node.model">({{ node.model }})</text>
+ </view>
+ <view class="bom-details">
+ <text class="bom-info">宸ュ簭: {{ node.operationName || '-' }}</text>
+ <text class="bom-info">鎵�闇�: {{ node.unitQuantity || 0 }} {{ node.unit || '' }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤 BOM 缁撴瀯"></up-empty>
+ </view>
+ </scroll-view>
+ <!-- 鍙傛暟鍒楄〃寮圭獥 -->
+ <up-popup :show="showPopup"
+ mode="bottom"
+ @close="showPopup = false"
+ round="10">
+ <view class="popup-content">
+ <view class="popup-header">
+ <text class="title">鍙傛暟鍒楄〃 - {{ currentItem.technologyOperationName || currentItem.operationName }}</text>
+ <up-icon name="close"
+ size="20"
+ @click="showPopup = false"></up-icon>
+ </view>
+ <scroll-view scroll-y
+ class="param-list">
+ <view v-if="paramList.length > 0">
+ <view v-for="(param, pIndex) in paramList"
+ :key="pIndex"
+ class="param-item">
+ <view class="param-row">
+ <text class="param-label">鍙傛暟鍚嶇О锛�</text>
+ <text class="param-value">{{ param.paramName || '-' }}</text>
+ </view>
+ <view class="param-row">
+ <text class="param-label">鏍囧噯鍊硷細</text>
+ <text class="param-value">{{ param.standardValue || '-' }}</text>
+ </view>
+ <view class="param-row">
+ <text class="param-label">鍗曚綅锛�</text>
+ <text class="param-value">{{ param.unit || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-record">
+ <text>鏆傛棤鍙傛暟璁板綍</text>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import {
+ findProcessRouteItemList,
+ getProcessParamList,
+ queryBomList,
+ } from "@/api/productionManagement/processRoute.js";
+ import {
+ queryOrderBomList,
+ findProcessParamListOrder,
+ } from "@/api/productionManagement/productionOrder.js";
+ import PageHeader from "@/components/PageHeader.vue";
+
+ const routeInfo = ref({});
+ const itemsList = ref([]);
+ const bomList = ref([]);
+ const loading = ref(false);
+ const pageType = ref("route"); // route | order
+
+ // 閫夐」鍗�
+ const tabsList = reactive([{ name: "璺嚎椤圭洰" }, { name: "BOM缁撴瀯" }]);
+ const currentTab = ref(0);
+
+ // 寮圭獥鐩稿叧
+ const showPopup = ref(false);
+ const currentItem = ref({});
+ const paramList = ref([]);
+ const paramLoading = ref(false);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const handleTabClick = item => {
+ currentTab.value = item.index;
+ if (item.index === 1 && bomList.value.length === 0) {
+ fetchBom();
+ }
+ };
+
+ // 鎵佸钩鍖� BOM 鏍戠敤浜庡睍绀�
+ const flatBomList = computed(() => {
+ const result = [];
+ const flatten = (nodes, level = 0) => {
+ nodes.forEach(node => {
+ result.push({ ...node, level });
+ if (node.children && node.children.length > 0) {
+ flatten(node.children, level + 1);
+ }
+ });
+ };
+ flatten(bomList.value);
+ return result;
+ });
+
+ onLoad(options => {
+ if (options.id) {
+ pageType.value = options.type || "route";
+ routeInfo.value = {
+ id: options.id,
+ processRouteCode: options.processRouteCode || "",
+ productName: decodeURIComponent(options.productName || ""),
+ model: decodeURIComponent(options.model || ""),
+ bomNo: options.bomNo || "",
+ bomId: options.bomId || "",
+ description: decodeURIComponent(options.description || ""),
+ orderId: options.orderId || "",
+ };
+ fetchItems(options.id);
+ }
+ });
+
+ const fetchItems = id => {
+ loading.value = true;
+ findProcessRouteItemList({ routeId: id, orderId: routeInfo.value.orderId })
+ .then(res => {
+ itemsList.value = res.data || [];
+ loading.value = false;
+ })
+ .catch(() => {
+ loading.value = false;
+ uni.showToast({
+ title: "鑾峰彇椤圭洰澶辫触",
+ icon: "error",
+ });
+ });
+ };
+
+ const fetchBom = () => {
+ console.log(routeInfo.value.bomId, "routeInfo.value.bomId");
+
+ if (!routeInfo.value.bomId) return;
+ loading.value = true;
+ const api = pageType.value === "order" ? queryOrderBomList : queryBomList;
+ api(routeInfo.value.bomId)
+ .then(res => {
+ bomList.value = res.data || [];
+ loading.value = false;
+ })
+ .catch(() => {
+ loading.value = false;
+ uni.showToast({
+ title: "鑾峰彇 BOM 澶辫触",
+ icon: "error",
+ });
+ });
+ };
+
+ const showParams = item => {
+ currentItem.value = item;
+ showPopup.value = true;
+ paramLoading.value = true;
+ paramList.value = [];
+
+ const api =
+ pageType.value === "order"
+ ? findProcessParamListOrder
+ : getProcessParamList;
+ const params =
+ pageType.value === "order"
+ ? {
+ productionOrderRoutingOperationId: item.id,
+ productionOrderId: routeInfo.value.orderId,
+ }
+ : { technologyRoutingOperationId: item.id };
+
+ api(params)
+ .then(res => {
+ paramList.value = res.data || [];
+ paramLoading.value = false;
+ })
+ .catch(() => {
+ paramLoading.value = false;
+ uni.showToast({
+ title: "鑾峰彇鍙傛暟澶辫触",
+ icon: "error",
+ });
+ });
+ };
+</script>
+
+<style scoped lang="scss">
+ .process-route-items {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .route-info-card {
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ .info-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .label {
+ font-size: 26rpx;
+ color: #999;
+ }
+ .value {
+ font-size: 26rpx;
+ color: #333;
+ font-weight: bold;
+ }
+ }
+ }
+
+ .tabs-box {
+ background: #fff;
+ margin-bottom: 10rpx;
+ }
+
+ .content-scroll {
+ flex: 1;
+ height: 0;
+ padding: 0 20rpx;
+ }
+
+ .process-card {
+ background: #fff;
+ margin-bottom: 24rpx;
+ border-radius: 16rpx;
+ overflow: hidden;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
+
+ .card-header {
+ display: flex;
+ align-items: center;
+ padding: 20rpx 24rpx;
+ background: #fcfcfc;
+ border-bottom: 1rpx solid #f5f5f5;
+
+ .index-badge {
+ width: 40rpx;
+ height: 40rpx;
+ background: #3c9cff;
+ color: #fff;
+ border-radius: 20rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 24rpx;
+ margin-right: 20rpx;
+ }
+
+ .process-name {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .card-content {
+ padding: 24rpx;
+
+ .detail-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+
+ .detail-label {
+ font-size: 24rpx;
+ color: #999;
+ }
+ .detail-value {
+ font-size: 24rpx;
+ color: #666;
+ }
+ }
+
+ .tag-row {
+ display: flex;
+ gap: 16rpx;
+ margin-top: 10rpx;
+ }
+ }
+
+ .card-footer {
+ padding: 16rpx 24rpx;
+ border-top: 1rpx dashed #eee;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .action-text {
+ font-size: 24rpx;
+ color: #3c9cff;
+ }
+ }
+ }
+
+ /* BOM 鏍戞牱寮� */
+ .bom-tree {
+ padding: 20rpx 0;
+ }
+
+ .bom-node {
+ position: relative;
+ margin-bottom: 20rpx;
+ }
+
+ .bom-node-inner {
+ background: #fff;
+ padding: 20rpx;
+ border-radius: 12rpx;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
+ display: flex;
+ align-items: center;
+ }
+
+ .bom-line {
+ position: absolute;
+ left: -20rpx;
+ top: 50%;
+ width: 20rpx;
+ height: 2rpx;
+ background: #ddd;
+ }
+
+ .bom-content {
+ flex: 1;
+ }
+
+ .bom-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8rpx;
+
+ .bom-product {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ }
+
+ .bom-model {
+ font-size: 24rpx;
+ color: #999;
+ margin-left: 10rpx;
+ }
+ }
+
+ .bom-details {
+ display: flex;
+ justify-content: space-between;
+
+ .bom-info {
+ font-size: 24rpx;
+ color: #666;
+ }
+ }
+
+ .no-data {
+ padding-top: 100rpx;
+ }
+
+ /* 寮圭獥鏍峰紡 */
+ .popup-content {
+ background: #fff;
+ padding: 30rpx;
+ max-height: 70vh;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 30rpx;
+ border-bottom: 1rpx solid #eee;
+
+ .title {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .param-list {
+ flex: 1;
+ height: 0;
+ padding-top: 20rpx;
+ }
+
+ .param-item {
+ padding: 20rpx;
+ background: #f9f9f9;
+ border-radius: 12rpx;
+ margin-bottom: 16rpx;
+
+ .param-row {
+ display: flex;
+ margin-bottom: 8rpx;
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .param-label {
+ font-size: 24rpx;
+ color: #999;
+ width: 140rpx;
+ }
+ .param-value {
+ font-size: 24rpx;
+ color: #333;
+ flex: 1;
+ }
+ }
+ }
+
+ .no-record {
+ padding: 100rpx 0;
+ text-align: center;
+ color: #999;
+ font-size: 26rpx;
+ }
+</style>
diff --git a/src/pages/productionManagement/processStatistics/index.vue b/src/pages/productionManagement/processStatistics/index.vue
new file mode 100644
index 0000000..a7cdd95
--- /dev/null
+++ b/src/pages/productionManagement/processStatistics/index.vue
@@ -0,0 +1,370 @@
+<template>
+ <view class="process-statistics">
+ <PageHeader title="宸ュ簭鐢熶骇瀹炲喌"
+ @back="goBack" />
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="date-picker-container"
+ @click="showCalendar = true">
+ <view class="date-input">
+ <up-icon name="calendar"
+ size="20"
+ color="#999"></up-icon>
+ <text class="date-text"
+ :class="{ 'placeholder': !searchForm.startDate }">{{ dateRangeText }}</text>
+ <view v-if="searchForm.startDate"
+ class="clear-icon-wrapper"
+ @click.stop="handleClearDate">
+ <up-icon name="close-circle-fill"
+ size="18"
+ color="#c0c4cc"></up-icon>
+ </view>
+ </view>
+ <view class="search-btn-wrapper">
+ <up-button type="primary"
+ size="small"
+ text="鎼滅储"
+ @click.stop="handleQuery"></up-button>
+ </view>
+ </view>
+ </view>
+ <!-- 缁熻鍗$墖鍒楄〃 -->
+ <scroll-view scroll-y
+ class="stats-list">
+ <view v-if="loading"
+ class="loading-box">
+ <up-loading-icon text="鍔犺浇涓�..."></up-loading-icon>
+ </view>
+ <view v-else-if="statsData.length > 0"
+ class="card-grid">
+ <view v-for="(item, index) in statsData"
+ :key="index"
+ class="stats-card">
+ <view class="card-header">
+ <text class="process-tag">{{ item.name }}</text>
+ <view class="header-details">
+ <view class="detail-row">
+ <text class="label">璁″垝鏁�</text>
+ <text class="value">{{ item.planned }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">鑹搧鏁�</text>
+ <text class="value good">{{ item.good }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="label">涓嶈壇鍝�</text>
+ <text class="value bad">{{ item.bad }}</text>
+ </view>
+ </view>
+ </view>
+ <view class="card-body">
+ <view class="main-stat">
+ <text class="big-number">{{ item.total }}</text>
+ <text class="sub-label">鐢熶骇浠诲姟鏁�</text>
+ </view>
+ </view>
+ <view class="card-footer">
+ <view class="progress-section">
+ <view class="progress-header">
+ <text class="progress-label">鐢熶骇杩涘害</text>
+ <text class="percentage-text">{{ item.percentage }}%</text>
+ </view>
+ <up-line-progress :percentage="Math.min(item.percentage, 100)"
+ :activeColor="getProgressColor(item.percentage)"
+ :show-text="false"
+ height="8"></up-line-progress>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤宸ュ簭缁熻鏁版嵁"></up-empty>
+ </view>
+ </scroll-view>
+ <!-- 鏃ュ巻閫夋嫨鍣� -->
+ <up-calendar :show="showCalendar"
+ mode="range"
+ :maxDate="maxDate"
+ minDate="2026-01-01"
+ :monthNum="monthNum"
+ @confirm="onDateConfirm"
+ @close="showCalendar = false"></up-calendar>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, computed } from "vue";
+ import { getOperationStatistics } from "@/api/productionManagement/workOrder.js";
+ import PageHeader from "@/components/PageHeader.vue";
+ import dayjs from "dayjs";
+
+ const loading = ref(false);
+ const showCalendar = ref(false);
+ const dateRange = ref([]);
+ const maxDate = dayjs().format("YYYY-MM-DD");
+ const monthNum = computed(() => {
+ const min = dayjs("2022-02-01");
+ const max = dayjs(maxDate);
+ return max.diff(min, "month") + 1;
+ });
+ // const minDate = dayjs().subtract(7, "day").format("YYYY-MM-DD");
+
+ const searchForm = reactive({
+ startDate: "",
+ endDate: "",
+ });
+
+ const statsData = ref([]);
+
+ const dateRangeText = computed(() => {
+ if (searchForm.startDate && searchForm.endDate) {
+ return `${searchForm.startDate} 鑷� ${searchForm.endDate}`;
+ }
+ return "璇烽�夋嫨鏃ユ湡鍖洪棿";
+ });
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const getProgressColor = percentage => {
+ if (percentage >= 100) return "#67c23a";
+ if (percentage >= 50) return "#3c9cff";
+ if (percentage >= 25) return "#e6a23c";
+ return "#f56c6c";
+ };
+
+ const onDateConfirm = e => {
+ searchForm.startDate = e[0];
+ searchForm.endDate = e[e.length - 1];
+ showCalendar.value = false;
+ handleQuery();
+ };
+
+ const getList = () => {
+ loading.value = true;
+ const params = {
+ startDate: searchForm.startDate,
+ endDate: searchForm.endDate,
+ };
+ getOperationStatistics(params)
+ .then(res => {
+ statsData.value = (res.data || []).map(item => ({
+ name: item.operationName || "-",
+ total: item.productionTaskCount || 0,
+ planned: item.planQuantity || 0,
+ good: item.goodQuantity || 0,
+ bad: item.scrapQty || 0,
+ percentage: Number(item.completionStatus || 0),
+ }));
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ getList();
+ };
+
+ const handleClearDate = () => {
+ searchForm.startDate = "";
+ searchForm.endDate = "";
+ handleQuery();
+ };
+
+ onMounted(() => {
+ // 榛樿鏃堕棿缃┖
+ searchForm.startDate = "";
+ searchForm.endDate = "";
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .process-statistics {
+ min-height: 100vh;
+ background-color: #f5f7fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .search-section {
+ background-color: #fff;
+ padding: 24rpx 30rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
+ }
+
+ .date-picker-container {
+ display: flex;
+ align-items: center;
+ width: 100%;
+
+ .date-input {
+ flex: 1;
+ height: 80rpx;
+ background-color: #f5f7fa;
+ border: 1rpx solid #e4e7ed;
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ padding: 0 24rpx;
+ margin-right: 20rpx;
+ transition: all 0.3s;
+
+ &:active {
+ background-color: #ebedf0;
+ }
+
+ .date-text {
+ font-size: 28rpx;
+ color: #303133;
+ margin-left: 16rpx;
+ flex: 1;
+
+ &.placeholder {
+ color: #c0c4cc;
+ }
+ }
+
+ .clear-icon {
+ padding: 10rpx;
+ margin-right: -10rpx;
+ }
+ }
+
+ .search-btn-wrapper {
+ width: 140rpx;
+ }
+ }
+
+ .stats-list {
+ flex: 1;
+ height: 0;
+ padding: 0 24rpx 40rpx;
+ }
+
+ .loading-box {
+ display: flex;
+ justify-content: center;
+ padding-top: 100rpx;
+ }
+
+ .card-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 24rpx;
+ }
+
+ .stats-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 30rpx;
+
+ .process-tag {
+ background-color: #e6f7ff;
+ color: #1890ff;
+ padding: 6rpx 16rpx;
+ border-radius: 8rpx;
+ font-size: 26rpx;
+ font-weight: bold;
+ }
+
+ .header-details {
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+
+ .detail-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12rpx;
+
+ .label {
+ font-size: 22rpx;
+ color: #999;
+ }
+
+ .value {
+ font-size: 24rpx;
+ color: #333;
+ font-weight: bold;
+ min-width: 60rpx;
+ text-align: right;
+
+ &.good {
+ color: #52c41a;
+ }
+ &.bad {
+ color: #f56c6c;
+ }
+ }
+ }
+ }
+ }
+
+ .card-body {
+ padding-bottom: 30rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .main-stat {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .big-number {
+ font-size: 56rpx;
+ font-weight: bold;
+ color: #333;
+ line-height: 1;
+ }
+
+ .sub-label {
+ font-size: 26rpx;
+ color: #666;
+ margin-top: 12rpx;
+ }
+ }
+ }
+
+ .card-footer {
+ padding-top: 24rpx;
+
+ .progress-section {
+ .progress-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+
+ .progress-label {
+ font-size: 24rpx;
+ color: #999;
+ }
+
+ .percentage-text {
+ font-size: 24rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+ }
+ }
+ }
+
+ .no-data {
+ padding-top: 100rpx;
+ }
+</style>
diff --git a/src/pages/productionManagement/productionAccounting/index.vue b/src/pages/productionManagement/productionAccounting/index.vue
new file mode 100644
index 0000000..87ec382
--- /dev/null
+++ b/src/pages/productionManagement/productionAccounting/index.vue
@@ -0,0 +1,506 @@
+<template>
+ <view class="production-accounting">
+ <PageHeader title="鐢熶骇鏍哥畻"
+ @back="goBack" />
+ <!-- 绛涢�夊尯鍩� -->
+ <view class="filter-section">
+ <view class="date-type-selector">
+ <up-tabs :list="dateTypeList"
+ :current="currentDateTypeIndex"
+ @change="handleDateTypeChange"
+ :activeStyle="{ color: '#2979ff', fontWeight: 'bold' }"
+ lineWidth="30"
+ lineHeight="3" />
+ </view>
+ <view class="date-picker-bar"
+ @click="showDatePicker = true">
+ <view class="date-display">
+ <up-icon name="calendar"
+ size="20"
+ color="#2979ff"></up-icon>
+ <text class="date-text">{{ dateDisplayText }}</text>
+ </view>
+ <up-icon name="arrow-right"
+ size="16"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ <!-- 姹囨�诲垪琛� -->
+ <view class="summary-section"
+ v-if="!showDetail">
+ <view class="section-header">
+ <text class="section-title">鐢熶骇浜哄憳姹囨��</text>
+ </view>
+ <view class="ledger-list"
+ v-if="summaryList.length > 0">
+ <view v-for="(item, index) in summaryList"
+ :key="index"
+ class="ledger-item"
+ @click="handleRowClick(item)">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="user-icon">
+ <up-icon name="account"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.schedulingUserName || '鏈煡' }}</text>
+ </view>
+ <view class="item-right">
+ <up-icon name="arrow-right"
+ size="16"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-grid">
+ <view class="grid-item">
+ <text class="grid-label">浜ч噺</text>
+ <text class="grid-value">{{ item.finishedNum || 0 }}</text>
+ </view>
+ <view class="grid-item">
+ <text class="grid-label">宸ヨ祫</text>
+ <text class="grid-value highlight">楼{{ item.wages || 0 }}</text>
+ </view>
+ <view class="grid-item">
+ <text class="grid-label">鍚堟牸鐜�</text>
+ <text class="grid-value">{{ formatOutputRate(item.outputRate) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤姹囨�绘暟鎹�" />
+ </view>
+ </view>
+ <!-- 鏄庣粏鍒楄〃 (鐐瑰嚮姹囨�昏鍚庢樉绀�) -->
+ <view class="detail-section"
+ v-else>
+ <view class="section-header back-bar"
+ @click="showDetail = false">
+ <up-icon name="arrow-left"
+ size="16"
+ color="#2979ff"></up-icon>
+ <text class="back-text">杩斿洖姹囨�� ({{ currentUserName }})</text>
+ </view>
+ <view class="ledger-list"
+ v-if="detailList.length > 0">
+ <view v-for="(item, index) in detailList"
+ :key="index"
+ class="ledger-item no-click">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="product-icon">
+ <up-icon name="shopping-cart"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.productName }}</text>
+ </view>
+ <view class="item-tag">
+ <text class="tag-text">{{ item.schedulingDate }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鐢熶骇鏃ユ湡</text>
+ <text class="detail-value">{{ item.schedulingDate || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐢熶骇浜�</text>
+ <text class="detail-value">{{ item.schedulingUserName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.productModelName }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭</text>
+ <text class="detail-value">{{ item.process }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鐢熶骇鏁伴噺</text>
+ <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ユ椂(h)</text>
+ <text class="detail-value">{{ item.workHour || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ユ椂瀹氶</text>
+ <text class="detail-value">{{ item.workHours }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ヨ祫</text>
+ <text class="detail-value highlight">楼{{ item.wages }}</text>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus"
+ @loadmore="getDetailList" />
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鏄庣粏鏁版嵁" />
+ </view>
+ </view>
+ <!-- 鏃ユ湡閫夋嫨鍣� -->
+ <up-datetime-picker :show="showDatePicker"
+ v-model="pickerValue"
+ :mode="currentDateType === 'day' ? 'date' : 'year-month'"
+ @confirm="handleDateConfirm"
+ @cancel="showDatePicker = false" />
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, onMounted } from "vue";
+ import {
+ salesLedgerProductionAccountingList,
+ salesLedgerProductionAccountingListProductionDetails,
+ } from "@/api/productionManagement/productionCosting";
+ import PageHeader from "@/components/PageHeader.vue";
+ import dayjs from "dayjs";
+
+ // 绛涢�夌浉鍏�
+ const dateTypeList = [{ name: "鏃�" }, { name: "鏈�" }];
+ const currentDateTypeIndex = ref(0);
+ const currentDateType = computed(() =>
+ currentDateTypeIndex.value === 0 ? "day" : "month"
+ );
+ const showDatePicker = ref(false);
+ const pickerValue = ref(Date.now());
+ const selectedDate = ref(dayjs().format("YYYY-MM-DD"));
+
+ const dateDisplayText = computed(() => {
+ return currentDateType.value === "day"
+ ? selectedDate.value
+ : dayjs(selectedDate.value).format("YYYY-MM");
+ });
+
+ // 鏁版嵁鐩稿叧
+ const summaryList = ref([]);
+ const detailList = ref([]);
+ const showDetail = ref(false);
+ const currentUserName = ref("");
+ const loadStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 20,
+ total: 0,
+ });
+
+ const page1 = reactive({
+ current: 1,
+ size: 20,
+ total: 0,
+ });
+
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ // 鍒囨崲鏃ユ湡绫诲瀷
+ const handleDateTypeChange = index => {
+ currentDateTypeIndex.value = index.index;
+ if (currentDateType.value === "day") {
+ selectedDate.value = dayjs().format("YYYY-MM-DD");
+ } else {
+ selectedDate.value = dayjs().startOf("month").format("YYYY-MM-DD");
+ }
+ reloadData();
+ };
+
+ // 纭鏃ユ湡閫夋嫨
+ const handleDateConfirm = e => {
+ selectedDate.value = dayjs(e.value).format("YYYY-MM-DD");
+ showDatePicker.value = false;
+ reloadData();
+ };
+
+ // 鏍煎紡鍖栧悎鏍肩巼
+ const formatOutputRate = val => {
+ if (val == null || val === "") return "-";
+ return parseFloat(val).toFixed(2) + "%";
+ };
+
+ // 鍔犺浇姹囨�诲垪琛�
+ const getSummaryList = () => {
+ uni.showLoading({ title: "鍔犺浇涓�..." });
+ const params = {
+ dateType: currentDateType.value,
+ entryDate: currentDateType.value === "day" ? selectedDate.value : undefined,
+ entryDateStart:
+ currentDateType.value === "month"
+ ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD")
+ : undefined,
+ entryDateEnd:
+ currentDateType.value === "month"
+ ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD")
+ : undefined,
+ pageNum: page.current,
+ pageSize: page.size,
+ };
+
+ salesLedgerProductionAccountingList(params)
+ .then(res => {
+ summaryList.value = res.data.records || [];
+ page.total = res.data.total || 0;
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ };
+
+ // 鍔犺浇鏄庣粏鍒楄〃
+ const getDetailList = (isLoadMore = false) => {
+ if (!isLoadMore) {
+ page1.current = 1;
+ detailList.value = [];
+ }
+ loadStatus.value = "loading";
+
+ const params = {
+ schedulingUserName: currentUserName.value,
+ dateType: currentDateType.value,
+ entryDate: currentDateType.value === "day" ? selectedDate.value : undefined,
+ entryDateStart:
+ currentDateType.value === "month"
+ ? dayjs(selectedDate.value).startOf("month").format("YYYY-MM-DD")
+ : undefined,
+ entryDateEnd:
+ currentDateType.value === "month"
+ ? dayjs(selectedDate.value).endOf("month").format("YYYY-MM-DD")
+ : undefined,
+ pageNum: page1.current,
+ pageSize: page1.size,
+ };
+
+ salesLedgerProductionAccountingListProductionDetails(params)
+ .then(res => {
+ const records = res.data.records || [];
+ detailList.value = isLoadMore
+ ? [...detailList.value, ...records]
+ : records;
+ page1.total = res.data.total || 0;
+
+ if (detailList.value.length >= page1.total) {
+ loadStatus.value = "nomore";
+ } else {
+ loadStatus.value = "loadmore";
+ page1.current++;
+ }
+ })
+ .catch(() => {
+ loadStatus.value = "loadmore";
+ });
+ };
+
+ // 鐐瑰嚮姹囨�昏
+ const handleRowClick = item => {
+ currentUserName.value = item.schedulingUserName;
+ showDetail.value = true;
+ getDetailList();
+ };
+
+ // 閲嶆柊鍔犺浇鏁版嵁
+ const reloadData = () => {
+ page.current = 1;
+ showDetail.value = false;
+ getSummaryList();
+ };
+
+ onMounted(() => {
+ getSummaryList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .production-accounting {
+ background-color: #f5f7fa;
+ min-height: 100vh;
+ padding-bottom: 30rpx;
+
+ .filter-section {
+ background-color: #ffffff;
+ padding: 20rpx 30rpx;
+ margin-bottom: 20rpx;
+
+ .date-type-selector {
+ margin-bottom: 20rpx;
+ }
+
+ .date-picker-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #f0f4ff;
+ padding: 16rpx 24rpx;
+ border-radius: 8rpx;
+
+ .date-display {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+
+ .date-text {
+ font-size: 28rpx;
+ color: #2979ff;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+
+ .section-header {
+ padding: 20rpx 30rpx;
+
+ .section-title {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ border-left: 8rpx solid #2979ff;
+ padding-left: 16rpx;
+ }
+
+ &.back-bar {
+ display: flex;
+ align-items: center;
+ gap: 10rpx;
+ background-color: #ffffff;
+ margin-bottom: 20rpx;
+
+ .back-text {
+ font-size: 28rpx;
+ color: #2979ff;
+ }
+ }
+ }
+
+ .ledger-list {
+ padding: 0 20rpx;
+
+ .ledger-item {
+ background-color: #ffffff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ &:active {
+ background-color: #f9f9f9;
+ }
+
+ &.no-click:active {
+ background-color: #ffffff;
+ }
+
+ .item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16rpx;
+
+ .item-left {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+
+ .user-icon,
+ .product-icon {
+ width: 48rpx;
+ height: 48rpx;
+ background: linear-gradient(135deg, #2979ff, #64a1ff);
+ border-radius: 8rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .item-id {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .item-tag {
+ background-color: #f0f4ff;
+ padding: 4rpx 12rpx;
+ border-radius: 4rpx;
+
+ .tag-text {
+ font-size: 24rpx;
+ color: #2979ff;
+ }
+ }
+ }
+
+ .item-details {
+ padding-top: 10rpx;
+
+ .detail-grid {
+ display: flex;
+ justify-content: space-between;
+
+ .grid-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ flex: 1;
+
+ .grid-label {
+ font-size: 24rpx;
+ color: #999;
+ margin-bottom: 8rpx;
+ }
+
+ .grid-value {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 500;
+
+ &.highlight {
+ color: #ff5a5f;
+ }
+ }
+ }
+ }
+
+ .detail-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+
+ .detail-label {
+ font-size: 26rpx;
+ color: #999;
+ }
+
+ .detail-value {
+ font-size: 26rpx;
+ color: #333;
+
+ &.highlight {
+ color: #ff5a5f;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .no-data {
+ padding-top: 100rpx;
+ }
+ }
+</style>
diff --git a/src/pages/productionManagement/productionDispatching/components/DispatchModal.vue b/src/pages/productionManagement/productionDispatching/components/DispatchModal.vue
index 95c3705..2a836e9 100644
--- a/src/pages/productionManagement/productionDispatching/components/DispatchModal.vue
+++ b/src/pages/productionManagement/productionDispatching/components/DispatchModal.vue
@@ -1,399 +1,379 @@
<template>
- <up-popup
- v-model:show="show"
- mode="bottom"
- :round="20"
- :safeAreaInsetBottom="true"
- @close="handleClose"
- @open="handleOpen"
- >
- <view class="dispatch-modal">
- <!-- 澶撮儴 -->
- <view class="modal-header">
- <text class="modal-title">鐢熶骇娲惧伐</text>
- <view class="close-btn" @click="handleClose">
- <up-icon name="close" size="20" color="#999"></up-icon>
- </view>
- </view>
-
- <!-- 琛ㄥ崟鍐呭 -->
- <view class="modal-content">
- <up-form
- :model="form"
- ref="formRef"
- :rules="rules"
- labelWidth="120"
- >
- <!-- 椤圭洰鍩烘湰淇℃伅 -->
- <view class="form-section">
- <text class="section-title">椤圭洰淇℃伅</text>
- <up-form-item label="椤圭洰鍚嶇О" prop="projectName">
- <up-input
- v-model="form.projectName"
- disabled
- placeholder="椤圭洰鍚嶇О"
- />
- </up-form-item>
- <up-form-item label="浜у搧澶х被" prop="productCategory">
- <up-input
- v-model="form.productCategory"
- disabled
- placeholder="浜у搧澶х被"
- />
- </up-form-item>
- </view>
-
- <!-- 鏁伴噺淇℃伅 -->
- <view class="form-section">
- <text class="section-title">鏁伴噺淇℃伅</text>
- <up-form-item label="鎬绘暟閲�" prop="quantity">
- <up-input
- v-model="form.quantity"
- disabled
- placeholder="鎬绘暟閲�"
- />
- </up-form-item>
- <up-form-item label="寰呮帓浜ф暟閲�" prop="pendingQuantity">
- <up-input
- v-model="form.pendingQuantity"
- disabled
- placeholder="寰呮帓浜ф暟閲�"
- />
- </up-form-item>
- <up-form-item label="鏈鎺掍骇鏁伴噺" prop="schedulingNum" required>
- <up-number-box
- v-model="form.schedulingNum"
- :min="0"
- :max="form.pendingQuantity"
- :step="0.1"
- :precision="2"
- @change="handleNumChange"
- />
- </up-form-item>
- </view>
-
- <!-- 娲惧伐淇℃伅 -->
- <view class="form-section">
- <text class="section-title">娲惧伐淇℃伅</text>
- <up-form-item label="娲惧伐浜�" prop="schedulingUserId" required>
- <up-input
- v-model="selectedUserName"
- placeholder="璇烽�夋嫨娲惧伐浜�"
- readonly
- @click="showUserPicker = true"
- suffixIcon="arrow-down"
- />
- </up-form-item>
- <up-form-item label="娲惧伐鏃ユ湡" prop="schedulingDate" required>
- <up-input
- v-model="form.schedulingDate"
- placeholder="璇烽�夋嫨娲惧伐鏃ユ湡"
- readonly
- @click="showDatePicker = true"
- suffixIcon="calendar"
- />
- </up-form-item>
- </view>
- </up-form>
- </view>
-
- <!-- 搴曢儴鎸夐挳 -->
- <view class="modal-footer">
- <up-button
- @click="handleClose"
- text="鍙栨秷"
- type="info"
- plain
- :customStyle="{ marginRight: '12px', flex: 1 }"
- />
- <up-button
- @click="handleConfirm"
- text="纭娲惧伐"
- type="primary"
- :customStyle="{ flex: 1 }"
- :loading="submitting"
- />
- </view>
- </view>
-
- <!-- 浜哄憳閫夋嫨鍣� -->
- <up-picker
- v-model="showUserPicker"
- :columns="userColumns"
- @confirm="handleUserSelect"
- @cancel="showUserPicker = false"
- />
-
- <!-- 鏃ユ湡閫夋嫨鍣� -->
- <up-datetime-picker
- v-model="showDatePicker"
- mode="date"
- @confirm="handleDateSelect"
- @cancel="showDatePicker = false"
- />
- </up-popup>
+ <up-popup v-model:show="show"
+ mode="bottom"
+ :round="20"
+ :safeAreaInsetBottom="true"
+ @close="handleClose"
+ @open="handleOpen">
+ <view class="dispatch-modal">
+ <!-- 澶撮儴 -->
+ <view class="modal-header">
+ <text class="modal-title">鐢熶骇娲惧伐</text>
+ <view class="close-btn"
+ @click="handleClose">
+ <up-icon name="close"
+ size="20"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ <!-- 琛ㄥ崟鍐呭 -->
+ <view class="modal-content">
+ <up-form :model="form"
+ ref="formRef"
+ :rules="rules"
+ labelWidth="120">
+ <!-- 椤圭洰鍩烘湰淇℃伅 -->
+ <view class="form-section">
+ <text class="section-title">椤圭洰淇℃伅</text>
+ <up-form-item label="椤圭洰鍚嶇О"
+ prop="projectName">
+ <up-input v-model="form.projectName"
+ disabled
+ placeholder="椤圭洰鍚嶇О" />
+ </up-form-item>
+ <up-form-item label="浜у搧澶х被"
+ prop="productCategory">
+ <up-input v-model="form.productCategory"
+ disabled
+ placeholder="浜у搧澶х被" />
+ </up-form-item>
+ </view>
+ <!-- 鏁伴噺淇℃伅 -->
+ <view class="form-section">
+ <text class="section-title">鏁伴噺淇℃伅</text>
+ <up-form-item label="鎬绘暟閲�"
+ prop="quantity">
+ <up-input v-model="form.quantity"
+ disabled
+ placeholder="鎬绘暟閲�" />
+ </up-form-item>
+ <up-form-item label="寰呮帓浜ф暟閲�"
+ prop="pendingQuantity">
+ <up-input v-model="form.pendingQuantity"
+ disabled
+ placeholder="寰呮帓浜ф暟閲�" />
+ </up-form-item>
+ <up-form-item label="鏈鎺掍骇鏁伴噺"
+ prop="schedulingNum"
+ required>
+ <up-number-box v-model="form.schedulingNum"
+ :min="0"
+ :max="form.pendingQuantity"
+ :step="0.1"
+ :precision="2"
+ @change="handleNumChange" />
+ </up-form-item>
+ </view>
+ <!-- 娲惧伐淇℃伅 -->
+ <view class="form-section">
+ <text class="section-title">娲惧伐淇℃伅</text>
+ <up-form-item label="娲惧伐浜�"
+ prop="schedulingUserId"
+ required>
+ <up-input v-model="selectedUserName"
+ placeholder="璇烽�夋嫨娲惧伐浜�"
+ readonly
+ @click="showUserPicker = true"
+ suffixIcon="arrow-down" />
+ </up-form-item>
+ <up-form-item label="娲惧伐鏃ユ湡"
+ prop="schedulingDate"
+ required>
+ <up-input v-model="form.schedulingDate"
+ placeholder="璇烽�夋嫨娲惧伐鏃ユ湡"
+ readonly
+ @click="showDatePicker = true"
+ suffixIcon="calendar" />
+ </up-form-item>
+ </view>
+ </up-form>
+ </view>
+ <!-- 搴曢儴鎸夐挳 -->
+ <view class="modal-footer">
+ <up-button @click="handleClose"
+ text="鍙栨秷"
+ type="info"
+ plain
+ :customStyle="{ marginRight: '12px', flex: 1 }" />
+ <up-button @click="handleConfirm"
+ text="纭娲惧伐"
+ type="primary"
+ :customStyle="{ flex: 1 }"
+ :loading="submitting" />
+ </view>
+ </view>
+ <!-- 浜哄憳閫夋嫨鍣� -->
+ <up-picker v-model="showUserPicker"
+ :columns="userColumns"
+ @confirm="handleUserSelect"
+ @cancel="showUserPicker = false" />
+ <!-- 鏃ユ湡閫夋嫨鍣� -->
+ <up-datetime-picker v-model="showDatePicker"
+ mode="date"
+ @confirm="handleDateSelect"
+ @cancel="showDatePicker = false" />
+ </up-popup>
</template>
<script setup>
-import { ref, reactive, computed, getCurrentInstance } from 'vue';
-import { userListNoPageByTenantId } from "@/api/system/user.js";
-import { productionDispatch } from "@/api/productionManagement/productionOrder.js";
-import useUserStore from "@/store/modules/user";
-import dayjs from "dayjs";
+ import { ref, reactive, computed, getCurrentInstance } from "vue";
+ import { userListNoPageByTenantId } from "@/api/system/user.js";
+ // import { productionDispatch } from "@/api/productionManagement/productionOrder.js";
+ import useUserStore from "@/store/modules/user";
+ import dayjs from "dayjs";
-const { proxy } = getCurrentInstance();
-const userStore = useUserStore();
-const emit = defineEmits(['confirm']);
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
+ const emit = defineEmits(["confirm"]);
-// 寮圭獥鏄剧ず鐘舵��
-const show = ref(false);
-const submitting = ref(false);
+ // 寮圭獥鏄剧ず鐘舵��
+ const show = ref(false);
+ const submitting = ref(false);
-// 閫夋嫨鍣ㄦ樉绀虹姸鎬�
-const showUserPicker = ref(false);
-const showDatePicker = ref(false);
+ // 閫夋嫨鍣ㄦ樉绀虹姸鎬�
+ const showUserPicker = ref(false);
+ const showDatePicker = ref(false);
-// 鐢ㄦ埛鍒楄〃
-const userList = ref([]);
-const userColumns = computed(() => [
- userList.value.map(user => ({
- label: user.nickName,
- value: user.userId
- }))
-]);
+ // 鐢ㄦ埛鍒楄〃
+ const userList = ref([]);
+ const userColumns = computed(() => [
+ userList.value.map(user => ({
+ label: user.nickName,
+ value: user.userId,
+ })),
+ ]);
-// 閫変腑鐨勭敤鎴峰悕绉帮紙鐢ㄤ簬鏄剧ず锛�
-const selectedUserName = computed(() => {
- const user = userList.value.find(u => u.userId === form.schedulingUserId);
- return user ? user.nickName : '';
-});
+ // 閫変腑鐨勭敤鎴峰悕绉帮紙鐢ㄤ簬鏄剧ず锛�
+ const selectedUserName = computed(() => {
+ const user = userList.value.find(u => u.userId === form.schedulingUserId);
+ return user ? user.nickName : "";
+ });
-// 琛ㄥ崟鏁版嵁
-const form = reactive({
- projectName: "",
- productCategory: "",
- quantity: "",
- schedulingNum: 0,
- schedulingUserId: "",
- schedulingDate: "",
- pendingQuantity: 0,
- id: "" // 鍘熷璁板綍ID
-});
+ // 琛ㄥ崟鏁版嵁
+ const form = reactive({
+ projectName: "",
+ productCategory: "",
+ quantity: "",
+ schedulingNum: 0,
+ schedulingUserId: "",
+ schedulingDate: "",
+ pendingQuantity: 0,
+ id: "", // 鍘熷璁板綍ID
+ });
-// 琛ㄥ崟楠岃瘉瑙勫垯
-const rules = reactive({
- schedulingNum: [
- { required: true, message: "璇疯緭鍏ユ帓浜ф暟閲�", trigger: "blur" }
- ],
- schedulingUserId: [
- { required: true, message: "璇烽�夋嫨娲惧伐浜�", trigger: "change" }
- ],
- schedulingDate: [
- { required: true, message: "璇烽�夋嫨娲惧伐鏃ユ湡", trigger: "change" }
- ]
-});
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = reactive({
+ schedulingNum: [
+ { required: true, message: "璇疯緭鍏ユ帓浜ф暟閲�", trigger: "blur" },
+ ],
+ schedulingUserId: [
+ { required: true, message: "璇烽�夋嫨娲惧伐浜�", trigger: "change" },
+ ],
+ schedulingDate: [
+ { required: true, message: "璇烽�夋嫨娲惧伐鏃ユ湡", trigger: "change" },
+ ],
+ });
-// 琛ㄥ崟寮曠敤
-const formRef = ref();
+ // 琛ㄥ崟寮曠敤
+ const formRef = ref();
-// 鎵撳紑寮圭獥
-const open = async (rowData) => {
- try {
- // 鍔犺浇鐢ㄦ埛鍒楄〃
- const res = await userListNoPageByTenantId();
- userList.value = res.data || [];
-
- // 濉厖琛ㄥ崟鏁版嵁
- Object.assign(form, {
- ...rowData,
- schedulingNum: 0,
- schedulingUserId: userStore.id,
- schedulingDate: dayjs().format("YYYY-MM-DD")
- });
-
- show.value = true;
- } catch (error) {
- uni.showToast({
- title: '鍔犺浇鐢ㄦ埛鍒楄〃澶辫触',
- icon: 'error'
- });
- }
-};
+ // 鎵撳紑寮圭獥
+ const open = async rowData => {
+ try {
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ const res = await userListNoPageByTenantId();
+ userList.value = res.data || [];
-// 澶勭悊鏁伴噺鍙樺寲
-const handleNumChange = (value) => {
- if (value > form.pendingQuantity) {
- form.schedulingNum = form.pendingQuantity;
- uni.showToast({
- title: '鎺掍骇鏁伴噺涓嶅彲澶т簬寰呮帓浜ф暟閲�',
- icon: 'none'
- });
- }
-};
+ // 濉厖琛ㄥ崟鏁版嵁
+ Object.assign(form, {
+ ...rowData,
+ schedulingNum: 0,
+ schedulingUserId: userStore.id,
+ schedulingDate: dayjs().format("YYYY-MM-DD"),
+ });
-// 澶勭悊鐢ㄦ埛閫夋嫨
-const handleUserSelect = (params) => {
- if (params.value && params.value.length > 0) {
- form.schedulingUserId = params.value[0];
- }
- showUserPicker.value = false;
-};
+ show.value = true;
+ } catch (error) {
+ uni.showToast({
+ title: "鍔犺浇鐢ㄦ埛鍒楄〃澶辫触",
+ icon: "error",
+ });
+ }
+ };
-// 澶勭悊鏃ユ湡閫夋嫨
-const handleDateSelect = (params) => {
- if (params.value) {
- form.schedulingDate = dayjs(params.value).format("YYYY-MM-DD");
- }
- showDatePicker.value = false;
-};
+ // 澶勭悊鏁伴噺鍙樺寲
+ const handleNumChange = value => {
+ if (value > form.pendingQuantity) {
+ form.schedulingNum = form.pendingQuantity;
+ uni.showToast({
+ title: "鎺掍骇鏁伴噺涓嶅彲澶т簬寰呮帓浜ф暟閲�",
+ icon: "none",
+ });
+ }
+ };
-// 纭娲惧伐
-const handleConfirm = async () => {
- try {
- // 琛ㄥ崟楠岃瘉
- const valid = await formRef.value?.validate();
- if (!valid) return;
-
- if (form.schedulingNum <= 0) {
- uni.showToast({
- title: '鎺掍骇鏁伴噺蹇呴』澶т簬0',
- icon: 'none'
- });
- return;
- }
-
- submitting.value = true;
-
- // 鎻愪氦娲惧伐鏁版嵁
- await productionDispatch(form);
-
- uni.showToast({
- title: '娲惧伐鎴愬姛',
- icon: 'success'
- });
-
- handleClose();
- emit('confirm');
-
- } catch (error) {
- uni.showToast({
- title: '娲惧伐澶辫触',
- icon: 'error'
- });
- } finally {
- submitting.value = false;
- }
-};
+ // 澶勭悊鐢ㄦ埛閫夋嫨
+ const handleUserSelect = params => {
+ if (params.value && params.value.length > 0) {
+ form.schedulingUserId = params.value[0];
+ }
+ showUserPicker.value = false;
+ };
-// 寮圭獥鎵撳紑浜嬩欢
-const handleOpen = () => {
- // 寮圭獥鎵撳紑鏃剁殑澶勭悊
-};
+ // 澶勭悊鏃ユ湡閫夋嫨
+ const handleDateSelect = params => {
+ if (params.value) {
+ form.schedulingDate = dayjs(params.value).format("YYYY-MM-DD");
+ }
+ showDatePicker.value = false;
+ };
-// 鍏抽棴寮圭獥
-const handleClose = () => {
- show.value = false;
- showUserPicker.value = false;
- showDatePicker.value = false;
-
- // 閲嶇疆琛ㄥ崟
- Object.assign(form, {
- projectName: "",
- productCategory: "",
- quantity: "",
- schedulingNum: 0,
- schedulingUserId: "",
- schedulingDate: "",
- pendingQuantity: 0,
- id: ""
- });
-};
+ // 纭娲惧伐
+ const handleConfirm = async () => {
+ try {
+ // 琛ㄥ崟楠岃瘉
+ const valid = await formRef.value?.validate();
+ if (!valid) return;
-// 鏆撮湶鏂规硶
-defineExpose({
- open
-});
+ if (form.schedulingNum <= 0) {
+ uni.showToast({
+ title: "鎺掍骇鏁伴噺蹇呴』澶т簬0",
+ icon: "none",
+ });
+ return;
+ }
+
+ submitting.value = true;
+
+ // 鎻愪氦娲惧伐鏁版嵁
+ // await productionDispatch(form);
+
+ uni.showToast({
+ title: "娲惧伐鎴愬姛",
+ icon: "success",
+ });
+
+ handleClose();
+ emit("confirm");
+ } catch (error) {
+ uni.showToast({
+ title: "娲惧伐澶辫触",
+ icon: "error",
+ });
+ } finally {
+ submitting.value = false;
+ }
+ };
+
+ // 寮圭獥鎵撳紑浜嬩欢
+ const handleOpen = () => {
+ // 寮圭獥鎵撳紑鏃剁殑澶勭悊
+ };
+
+ // 鍏抽棴寮圭獥
+ const handleClose = () => {
+ show.value = false;
+ showUserPicker.value = false;
+ showDatePicker.value = false;
+
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form, {
+ projectName: "",
+ productCategory: "",
+ quantity: "",
+ schedulingNum: 0,
+ schedulingUserId: "",
+ schedulingDate: "",
+ pendingQuantity: 0,
+ id: "",
+ });
+ };
+
+ // 鏆撮湶鏂规硶
+ defineExpose({
+ open,
+ });
</script>
<style scoped lang="scss">
-.dispatch-modal {
- background: #ffffff;
- border-radius: 20px 20px 0 0;
- max-height: 80vh;
- overflow: hidden;
- display: flex;
- flex-direction: column;
-}
+ .dispatch-modal {
+ background: #ffffff;
+ border-radius: 20px 20px 0 0;
+ max-height: 80vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
-.modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 20px 20px 0 20px;
- border-bottom: 1px solid #f0f0f0;
- padding-bottom: 16px;
- margin-bottom: 20px;
-}
+ .modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 20px 20px 0 20px;
+ border-bottom: 1px solid #f0f0f0;
+ padding-bottom: 16px;
+ margin-bottom: 20px;
+ }
-.modal-title {
- font-size: 18px;
- font-weight: 600;
- color: #333;
-}
+ .modal-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #333;
+ }
-.close-btn {
- padding: 4px;
-
- &:active {
- opacity: 0.7;
- }
-}
+ .close-btn {
+ padding: 4px;
-.modal-content {
- flex: 1;
- padding: 0 20px;
- overflow-y: auto;
-}
+ &:active {
+ opacity: 0.7;
+ }
+ }
-.form-section {
- margin-bottom: 24px;
-}
+ .modal-content {
+ flex: 1;
+ padding: 0 20px;
+ overflow-y: auto;
+ }
-.section-title {
- display: block;
- font-size: 16px;
- font-weight: 600;
- color: #333;
- margin-bottom: 16px;
- padding-left: 8px;
- border-left: 3px solid #2979ff;
-}
+ .form-section {
+ margin-bottom: 24px;
+ }
-.modal-footer {
- display: flex;
- gap: 12px;
- padding: 20px;
- border-top: 1px solid #f0f0f0;
- background: #fafafa;
-}
+ .section-title {
+ display: block;
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 16px;
+ padding-left: 8px;
+ border-left: 3px solid #2979ff;
+ }
-// uView 缁勪欢鏍峰紡璋冩暣
-:deep(.up-form-item) {
- margin-bottom: 20px;
-}
+ .modal-footer {
+ display: flex;
+ gap: 12px;
+ padding: 20px;
+ border-top: 1px solid #f0f0f0;
+ background: #fafafa;
+ }
-:deep(.up-input) {
- background: #f8f9fa;
- border-radius: 8px;
-}
+ // uView 缁勪欢鏍峰紡璋冩暣
+ :deep(.up-form-item) {
+ margin-bottom: 20px;
+ }
-:deep(.up-input--disabled) {
- background: #f0f0f0;
- color: #999;
-}
+ :deep(.up-input) {
+ background: #f8f9fa;
+ border-radius: 8px;
+ }
-:deep(.up-number-box) {
- background: #f8f9fa;
- border-radius: 8px;
-}
+ :deep(.up-input--disabled) {
+ background: #f0f0f0;
+ color: #999;
+ }
+
+ :deep(.up-number-box) {
+ background: #f8f9fa;
+ border-radius: 8px;
+ }
</style>
diff --git a/src/pages/productionManagement/productionDispatching/components/formDia.vue b/src/pages/productionManagement/productionDispatching/components/formDia.vue
index 72227ac..e7e6662 100644
--- a/src/pages/productionManagement/productionDispatching/components/formDia.vue
+++ b/src/pages/productionManagement/productionDispatching/components/formDia.vue
@@ -1,87 +1,101 @@
<template>
<div>
- <el-dialog
- v-model="dialogFormVisible"
- title="鐢熶骇娲惧伐"
- width="50%"
- @close="closeDia"
- >
- <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-dialog v-model="dialogFormVisible"
+ title="鐢熶骇娲惧伐"
+ width="50%"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="椤圭洰鍚嶇О锛�" prop="projectName">
- <el-input v-model="form.projectName" placeholder="璇疯緭鍏�" clearable disabled/>
+ <el-form-item label="椤圭洰鍚嶇О锛�"
+ prop="projectName">
+ <el-input v-model="form.projectName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ disabled />
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item label="浜у搧澶х被锛�" prop="productCategory">
- <el-input v-model="form.productCategory" placeholder="璇疯緭鍏�" clearable disabled/>
+ <el-form-item label="浜у搧澶х被锛�"
+ prop="productCategory">
+ <el-input v-model="form.productCategory"
+ placeholder="璇疯緭鍏�"
+ clearable
+ disabled />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="鎬绘暟閲忥細" prop="quantity">
- <el-input v-model="form.quantity" placeholder="璇疯緭鍏�" clearable disabled/>
+ <el-form-item label="鎬绘暟閲忥細"
+ prop="quantity">
+ <el-input v-model="form.quantity"
+ placeholder="璇疯緭鍏�"
+ clearable
+ disabled />
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item label="寰呮帓浜ф暟閲忥細" prop="pendingQuantity">
- <el-input v-model="form.pendingQuantity" placeholder="璇疯緭鍏�" clearable disabled/>
- </el-form-item>
+ <el-form-item label="寰呮帓浜ф暟閲忥細"
+ prop="pendingQuantity">
+ <el-input v-model="form.pendingQuantity"
+ placeholder="璇疯緭鍏�"
+ clearable
+ disabled />
+ </el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="鏈鎺掍骇鏁伴噺锛�" prop="schedulingNum">
- <el-input-number
- v-model="form.schedulingNum"
- placeholder="璇疯緭鍏�"
- :min="0"
- :step="0.1"
- :precision="2"
- clearable
- @change="changeNum"
- style="width: 100%"
- />
- </el-form-item>
+ <el-form-item label="鏈鎺掍骇鏁伴噺锛�"
+ prop="schedulingNum">
+ <el-input-number v-model="form.schedulingNum"
+ placeholder="璇疯緭鍏�"
+ :min="0"
+ :step="0.1"
+ :precision="2"
+ clearable
+ @change="changeNum"
+ style="width: 100%" />
+ </el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="娲惧伐浜猴細" prop="schedulingUserId">
- <el-select
- v-model="form.schedulingUserId"
- placeholder="閫夋嫨浜哄憳"
- style="width: 100%;"
- >
- <el-option
- v-for="user in userList"
- :key="user.userId"
- :label="user.nickName"
- :value="user.userId"
- />
- </el-select>
- </el-form-item>
- </el-col>
<el-col :span="12">
- <el-form-item label="娲惧伐鏃ユ湡锛�" prop="schedulingDate">
- <el-date-picker
- v-model="form.schedulingDate"
- type="date"
- placeholder="璇烽�夋嫨鏃ユ湡"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- clearable
- style="width: 100%"
- />
+ <el-form-item label="娲惧伐浜猴細"
+ prop="schedulingUserId">
+ <el-select v-model="form.schedulingUserId"
+ placeholder="閫夋嫨浜哄憳"
+ style="width: 100%;">
+ <el-option v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="娲惧伐鏃ユ湡锛�"
+ prop="schedulingDate">
+ <el-date-picker v-model="form.schedulingDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
- <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
<el-button @click="closeDia">鍙栨秷</el-button>
</div>
</template>
@@ -90,80 +104,85 @@
</template>
<script setup>
-import {ref} from "vue";
-import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
-import {userListNoPageByTenantId} from "@/api/system/user.js";
-import {productionDispatch} from "@/api/productionManagement/productionOrder.js";
-import useUserStore from "@/store/modules/user";
-import dayjs from "dayjs";
-const { proxy } = getCurrentInstance()
-const emit = defineEmits(['close'])
+ import { ref } from "vue";
+ import {
+ getStaffJoinInfo,
+ staffJoinAdd,
+ staffJoinUpdate,
+ } from "@/api/personnelManagement/onboarding.js";
+ import { userListNoPageByTenantId } from "@/api/system/user.js";
+ // import {productionDispatch} from "@/api/productionManagement/productionOrder.js";
+ import useUserStore from "@/store/modules/user";
+ import dayjs from "dayjs";
+ const { proxy } = getCurrentInstance();
+ const emit = defineEmits(["close"]);
-const dialogFormVisible = ref(false);
-const operationType = ref('')
-const data = reactive({
- form: {
- projectName: "",
- productCategory: "",
- quantity: "",
- schedulingNum: "",
- schedulingUserId: "",
- schedulingDate: "",
- pendingQuantity: "",
- },
- rules: {
- schedulingNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" },],
- schedulingUserId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" },],
- schedulingDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" },],
- },
-});
-const { form, rules } = toRefs(data);
-const userList = ref([])
-const userStore = useUserStore()
+ const dialogFormVisible = ref(false);
+ const operationType = ref("");
+ const data = reactive({
+ form: {
+ projectName: "",
+ productCategory: "",
+ quantity: "",
+ schedulingNum: "",
+ schedulingUserId: "",
+ schedulingDate: "",
+ pendingQuantity: "",
+ },
+ rules: {
+ schedulingNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ schedulingUserId: [
+ { required: true, message: "璇烽�夋嫨", trigger: "change" },
+ ],
+ schedulingDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const { form, rules } = toRefs(data);
+ const userList = ref([]);
+ const userStore = useUserStore();
-// 鎵撳紑寮规
-const openDialog = (type, row) => {
- operationType.value = type;
- dialogFormVisible.value = true;
- userListNoPageByTenantId().then((res) => {
- userList.value = res.data;
- });
- form.value = {...row}
- form.value.schedulingNum = 0
- form.value.schedulingUserId = userStore.id
- form.value.schedulingDate = dayjs().format("YYYY-MM-DD");
-}
+ // 鎵撳紑寮规
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ userListNoPageByTenantId().then(res => {
+ userList.value = res.data;
+ });
+ form.value = { ...row };
+ form.value.schedulingNum = 0;
+ form.value.schedulingUserId = userStore.id;
+ form.value.schedulingDate = dayjs().format("YYYY-MM-DD");
+ };
-//
-const changeNum = (value) => {
- if (value > form.value.pendingQuantity) {
- form.value.schedulingNum = form.value.pendingQuantity;
- proxy.$modal.msgWarning('鎺掍骇鏁伴噺涓嶅彲澶т簬寰呮帓浜ф暟閲�')
- }
-}
-// 鎻愪氦浜у搧琛ㄥ崟
-const submitForm = () => {
- proxy.$refs.formRef.validate(valid => {
- if (valid) {
- productionDispatch(form.value).then(res => {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- closeDia();
- })
+ //
+ const changeNum = value => {
+ if (value > form.value.pendingQuantity) {
+ form.value.schedulingNum = form.value.pendingQuantity;
+ proxy.$modal.msgWarning("鎺掍骇鏁伴噺涓嶅彲澶т簬寰呮帓浜ф暟閲�");
}
- })
-}
+ };
+ // 鎻愪氦浜у搧琛ㄥ崟
+ const submitForm = () => {
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ // productionDispatch(form.value).then(res => {
+ // proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ // closeDia();
+ // })
+ }
+ });
+ };
-// 鍏抽棴寮规
-const closeDia = () => {
- proxy.resetForm("formRef");
- dialogFormVisible.value = false;
- emit('close')
-};
-defineExpose({
- openDialog,
-});
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit("close");
+ };
+ defineExpose({
+ openDialog,
+ });
</script>
<style scoped>
-
</style>
\ No newline at end of file
diff --git a/src/pages/productionManagement/productionDispatching/index.vue b/src/pages/productionManagement/productionDispatching/index.vue
index 7aea197..d6ae5e3 100644
--- a/src/pages/productionManagement/productionDispatching/index.vue
+++ b/src/pages/productionManagement/productionDispatching/index.vue
@@ -1,235 +1,236 @@
<template>
- <view class="production-dispatching">
- <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
- <PageHeader title="鐢熶骇娲惧伐" @back="goBack" />
-
- <!-- 鎼滅储鍖哄煙 -->
- <view class="search-section">
- <view class="search-bar">
- <view class="search-input">
- <up-input
- class="search-text"
- placeholder="璇疯緭鍏ュ鎴峰悕绉版悳绱�"
- v-model="searchForm.customerName"
- @change="handleQuery"
- clearable
- />
- </view>
- <view class="filter-button" @click="handleQuery">
- <up-icon name="search" size="24" color="#999"></up-icon>
- </view>
- </view>
- </view>
-
- <!-- 鐢熶骇娲惧伐鍒楄〃 -->
- <view class="ledger-list" v-if="tableData.length > 0">
- <view v-for="(item, index) in tableData" :key="item.id || index">
- <view class="ledger-item">
- <view class="item-header">
- <view class="item-left">
- <view class="document-icon">
- <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
- </view>
- <text class="item-id">{{ item.salesContractNo }}</text>
- </view>
- </view>
- <up-divider></up-divider>
-
- <view class="item-details">
- <view class="detail-row">
- <text class="detail-label">褰曞叆鏃ユ湡</text>
- <text class="detail-value">{{ item.entryDate }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">瀹㈡埛鍚堝悓鍙�</text>
- <text class="detail-value">{{ item.customerContractNo }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">瀹㈡埛鍚嶇О</text>
- <text class="detail-value">{{ item.customerName }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">椤圭洰鍚嶇О</text>
- <text class="detail-value">{{ item.projectName }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">浜у搧澶х被</text>
- <text class="detail-value">{{ item.productCategory }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">瑙勬牸鍨嬪彿</text>
- <text class="detail-value">{{ item.specificationModel }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鎬绘暟閲�</text>
- <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鎺掍骇鏁伴噺</text>
- <text class="detail-value highlight">{{ item.schedulingNum }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">寰呮帓鏁伴噺</text>
- <text class="detail-value" :class="{ 'danger': item.pendingQuantity <= 0 }">{{ item.pendingQuantity }}</text>
- </view>
- </view>
-
- <!-- 鎿嶄綔鎸夐挳鍖哄煙 -->
- <view class="action-buttons">
- <up-button
- type="primary"
- size="small"
- @click="handleDispatch(item)"
- class="action-btn"
- :disabled="item.pendingQuantity <= 0"
- >
- 鐢熶骇娲惧伐
- </up-button>
- </view>
- </view>
- </view>
- </view>
- <view v-else class="no-data">
- <text>鏆傛棤鐢熶骇娲惧伐鏁版嵁</text>
- </view>
-
- <!-- 娲惧伐寮圭獥 -->
- <DispatchModal ref="dispatchModalRef" @confirm="handleDispatchConfirm" />
- </view>
+ <view class="production-dispatching">
+ <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
+ <PageHeader title="鐢熶骇娲惧伐"
+ @back="goBack" />
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉版悳绱�"
+ v-model="searchForm.customerName"
+ @change="handleQuery"
+ clearable />
+ </view>
+ <view class="filter-button"
+ @click="handleQuery">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <!-- 鐢熶骇娲惧伐鍒楄〃 -->
+ <view class="ledger-list"
+ v-if="tableData.length > 0">
+ <view v-for="(item, index) in tableData"
+ :key="item.id || index">
+ <view class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.salesContractNo }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">褰曞叆鏃ユ湡</text>
+ <text class="detail-value">{{ item.entryDate }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹㈡埛鍚堝悓鍙�</text>
+ <text class="detail-value">{{ item.customerContractNo }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹㈡埛鍚嶇О</text>
+ <text class="detail-value">{{ item.customerName }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">椤圭洰鍚嶇О</text>
+ <text class="detail-value">{{ item.projectName }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜у搧澶х被</text>
+ <text class="detail-value">{{ item.productCategory }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.specificationModel }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎬绘暟閲�</text>
+ <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎺掍骇鏁伴噺</text>
+ <text class="detail-value highlight">{{ item.schedulingNum }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">寰呮帓鏁伴噺</text>
+ <text class="detail-value"
+ :class="{ 'danger': item.pendingQuantity <= 0 }">{{ item.pendingQuantity }}</text>
+ </view>
+ </view>
+ <!-- 鎿嶄綔鎸夐挳鍖哄煙 -->
+ <view class="action-buttons">
+ <up-button type="primary"
+ size="small"
+ @click="handleDispatch(item)"
+ class="action-btn"
+ :disabled="item.pendingQuantity <= 0">
+ 鐢熶骇娲惧伐
+ </up-button>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <text>鏆傛棤鐢熶骇娲惧伐鏁版嵁</text>
+ </view>
+ <!-- 娲惧伐寮圭獥 -->
+ <DispatchModal ref="dispatchModalRef"
+ @confirm="handleDispatchConfirm" />
+ </view>
</template>
<script setup>
-import { ref, reactive, toRefs, getCurrentInstance } from "vue";
-import { onShow } from '@dcloudio/uni-app';
-import dayjs from "dayjs";
-import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
-import PageHeader from "@/components/PageHeader.vue";
-import DispatchModal from "./components/DispatchModal.vue";
-const { proxy } = getCurrentInstance();
+ import { ref, reactive, toRefs, getCurrentInstance } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ // import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
+ import PageHeader from "@/components/PageHeader.vue";
+ import DispatchModal from "./components/DispatchModal.vue";
+ const { proxy } = getCurrentInstance();
-// 鍔犺浇鐘舵��
-const loading = ref(false);
+ // 鍔犺浇鐘舵��
+ const loading = ref(false);
-// 鍒楄〃鏁版嵁
-const tableData = ref([]);
+ // 鍒楄〃鏁版嵁
+ const tableData = ref([]);
+ // 鎼滅储琛ㄥ崟鏁版嵁
+ const data = reactive({
+ searchForm: {
+ customerName: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
-// 鎼滅储琛ㄥ崟鏁版嵁
-const data = reactive({
- searchForm: {
- customerName: "",
- },
-});
-const { searchForm } = toRefs(data);
+ // 鍒嗛〉閰嶇疆
+ const page = reactive({
+ current: -1,
+ size: -1,
+ });
-// 鍒嗛〉閰嶇疆
-const page = reactive({
- current: -1,
- size: -1,
-});
+ // 娲惧伐寮圭獥寮曠敤
+ const dispatchModalRef = ref();
-// 娲惧伐寮圭獥寮曠敤
-const dispatchModalRef = ref();
+ // 閫氱敤鎻愮ず鍑芥暟
+ const showLoadingToast = message => {
+ uni.showLoading({
+ title: message,
+ mask: true,
+ });
+ };
-// 閫氱敤鎻愮ず鍑芥暟
-const showLoadingToast = (message) => {
- uni.showLoading({
- title: message,
- mask: true
- });
-};
+ const closeToast = () => {
+ uni.hideLoading();
+ };
-const closeToast = () => {
- uni.hideLoading();
-};
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ uni.navigateBack();
+ };
-// 杩斿洖涓婁竴椤�
-const goBack = () => {
- uni.navigateBack();
-};
+ // 鏌ヨ鍒楄〃
+ const handleQuery = () => {
+ getList();
+ };
-// 鏌ヨ鍒楄〃
-const handleQuery = () => {
- getList();
-};
+ // 鑾峰彇鍒楄〃鏁版嵁
+ const getList = () => {
+ loading.value = true;
+ showLoadingToast("鍔犺浇涓�...");
-// 鑾峰彇鍒楄〃鏁版嵁
-const getList = () => {
- loading.value = true;
- showLoadingToast('鍔犺浇涓�...');
-
- // 鏋勯�犺姹傚弬鏁�
- const params = { ...searchForm.value, ...page };
-
- schedulingListPage(params).then((res) => {
- loading.value = false;
- closeToast();
-
- // 澶勭悊姣忔潯鏁版嵁锛屽鍔爌endingQuantity瀛楁
- tableData.value = (res.data.records || []).map(item => ({
- ...item,
- pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0)
- }));
- }).catch(() => {
- loading.value = false;
- closeToast();
- uni.showToast({
- title: '鍔犺浇澶辫触',
- icon: 'error'
- });
- });
-};
+ // 鏋勯�犺姹傚弬鏁�
+ const params = { ...searchForm.value, ...page };
-// 澶勭悊娲惧伐鎿嶄綔
-const handleDispatch = (item) => {
- if (item.pendingQuantity <= 0) {
- uni.showToast({
- title: '璇ラ」鐩棤闇�鍐嶆淳宸�',
- icon: 'none'
- });
- return;
- }
-
- dispatchModalRef.value?.open(item);
-};
+ // schedulingListPage(params).then((res) => {
+ // loading.value = false;
+ // closeToast();
-// 澶勭悊娲惧伐纭
-const handleDispatchConfirm = () => {
- getList(); // 鍒锋柊鍒楄〃
-};
+ // // 澶勭悊姣忔潯鏁版嵁锛屽鍔爌endingQuantity瀛楁
+ // tableData.value = (res.data.records || []).map(item => ({
+ // ...item,
+ // pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0)
+ // }));
+ // }).catch(() => {
+ // loading.value = false;
+ // closeToast();
+ // uni.showToast({
+ // title: '鍔犺浇澶辫触',
+ // icon: 'error'
+ // });
+ // });
+ };
-// 椤甸潰鏄剧ず鏃跺姞杞芥暟鎹�
-onShow(() => {
- // 鍔犺浇鍒楄〃鏁版嵁
- getList();
-});
+ // 澶勭悊娲惧伐鎿嶄綔
+ const handleDispatch = item => {
+ if (item.pendingQuantity <= 0) {
+ uni.showToast({
+ title: "璇ラ」鐩棤闇�鍐嶆淳宸�",
+ icon: "none",
+ });
+ return;
+ }
+
+ dispatchModalRef.value?.open(item);
+ };
+
+ // 澶勭悊娲惧伐纭
+ const handleDispatchConfirm = () => {
+ getList(); // 鍒锋柊鍒楄〃
+ };
+
+ // 椤甸潰鏄剧ず鏃跺姞杞芥暟鎹�
+ onShow(() => {
+ // 鍔犺浇鍒楄〃鏁版嵁
+ getList();
+ });
</script>
<style scoped lang="scss">
-@import '@/styles/sales-common.scss';
+ @import "@/styles/sales-common.scss";
-// 鐢熶骇娲惧伐椤甸潰鏍峰紡
-.production-dispatching {
- min-height: 100vh;
- background: #f8f9fa;
- position: relative;
-}
+ // 鐢熶骇娲惧伐椤甸潰鏍峰紡
+ .production-dispatching {
+ min-height: 100vh;
+ background: #f8f9fa;
+ position: relative;
+ }
-// 鍒楄〃椤规牱寮�
-.ledger-item {
- .detail-value.highlight {
- color: #ff6b35;
- font-weight: 600;
- }
-
- .detail-value.danger {
- color: #ee0a24;
- font-weight: 600;
- }
-}
+ // 鍒楄〃椤规牱寮�
+ .ledger-item {
+ .detail-value.highlight {
+ color: #ff6b35;
+ font-weight: 600;
+ }
-// 閫傞厤 uView 缁勪欢鏍峰紡
-:deep(.up-input) {
- background: transparent;
-}
+ .detail-value.danger {
+ color: #ee0a24;
+ font-weight: 600;
+ }
+ }
+
+ // 閫傞厤 uView 缁勪欢鏍峰紡
+ :deep(.up-input) {
+ background: transparent;
+ }
</style>
diff --git a/src/pages/productionManagement/productionOrder/index.vue b/src/pages/productionManagement/productionOrder/index.vue
index 7adcc4d..5f31fb1 100644
--- a/src/pages/productionManagement/productionOrder/index.vue
+++ b/src/pages/productionManagement/productionOrder/index.vue
@@ -1,193 +1,546 @@
<template>
- <view class="production-order">
- <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
- <PageHeader title="鐢熶骇璁㈠崟" @back="goBack" />
-
- <!-- 鎼滅储鍖哄煙 -->
- <view class="search-section">
- <view class="search-bar">
- <view class="search-input">
- <up-input
- class="search-text"
- placeholder="璇疯緭鍏ュ鎴峰悕绉版悳绱�"
- v-model="searchForm.customerName"
- @change="handleQuery"
- clearable
- />
- </view>
- <view class="filter-button" @click="handleQuery">
- <up-icon name="search" size="24" color="#999"></up-icon>
- </view>
- </view>
- </view>
-
- <!-- 鐢熶骇璁㈠崟鍒楄〃 -->
- <view class="ledger-list" v-if="tableData.length > 0">
- <view v-for="(item, index) in tableData" :key="item.id || index">
- <view class="ledger-item">
- <view class="item-header">
- <view class="item-left">
- <view class="document-icon">
- <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
- </view>
- <text class="item-id">{{ item.salesContractNo }}</text>
- </view>
- </view>
- <up-divider></up-divider>
-
- <view class="item-details">
- <view class="detail-row">
- <text class="detail-label">褰曞叆鏃ユ湡</text>
- <text class="detail-value">{{ item.entryDate }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">瀹㈡埛鍚堝悓鍙�</text>
- <text class="detail-value">{{ item.customerContractNo }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">瀹㈡埛鍚嶇О</text>
- <text class="detail-value">{{ item.customerName }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">椤圭洰鍚嶇О</text>
- <text class="detail-value">{{ item.projectName }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">浜у搧澶х被</text>
- <text class="detail-value">{{ item.productCategory }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">瑙勬牸鍨嬪彿</text>
- <text class="detail-value">{{ item.specificationModel }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鏁伴噺</text>
- <text class="detail-value">{{ item.quantity }} {{ item.unit }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鎺掍骇鏁伴噺</text>
- <text class="detail-value highlight">{{ item.schedulingNum }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">瀹屽伐鏁伴噺</text>
- <text class="detail-value highlight">{{ item.successNum }}</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- <view v-else class="no-data">
- <text>鏆傛棤鐢熶骇璁㈠崟鏁版嵁</text>
- </view>
- </view>
+ <view class="production-order">
+ <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
+ <PageHeader title="鐢熶骇璁㈠崟"
+ @back="goBack" />
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ヨ鍗曞彿鎴栦骇鍝佸悕绉�"
+ v-model="searchForm.keyword"
+ @change="handleQuery"
+ clearable />
+ </view>
+ <view class="filter-button"
+ @click="handleQuery">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <!-- 鍒楄〃鍖哄煙 -->
+ <scroll-view scroll-y
+ class="list-container"
+ v-if="tableData.length > 0"
+ @scrolltolower="loadMore">
+ <view v-for="(item, index) in tableData"
+ :key="item.id || index">
+ <view class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.npsNo }}</text>
+ </view>
+ <view class="item-right">
+ <up-tag :text="getStatusText(item.status)"
+ :type="getStatusType(item.status)"
+ size="mini" />
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁㈠崟鏁伴噺</text>
+ <text class="detail-value">{{ item.quantity || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹屾垚杩涘害</text>
+ <view class="progress-box">
+ <up-line-progress :percentage="toProgressPercentage(item.completionStatus)"
+ :activeColor="progressColor(item.completionStatus)"
+ height="10"></up-line-progress>
+ <text class="progress-text">{{ item.completeQuantity || 0 }} / {{ item.quantity || 0 }}</text>
+ </view>
+ </view>
+ <!-- 宸ュ簭鐢熶骇杩涘害灞曠ず -->
+ <view class="detail-row process-row">
+ <text class="detail-label">宸ュ簭杩涘害</text>
+ <scroll-view scroll-x
+ class="process-scroll">
+ <view class="process-container">
+ <view v-for="(process, pIdx) in item.processRouteStatus"
+ :key="pIdx"
+ class="process-item">
+ <view class="process-node">
+ <view class="node-circle"
+ :class="{ 'is-complete': process.percentage >= 100 }">
+ <text class="node-percentage"
+ :style="{ color: process.percentage >= 100 ? '#52c41a' : (process.percentage >= 70 ? '#f56c6c' : '#3c9cff') }">{{ process.percentage }}%</text>
+ </view>
+ <text class="node-name">{{ process.name }}</text>
+ </view>
+ <view v-if="pIdx < item.processRouteStatus.length - 1"
+ class="node-line"></view>
+ </view>
+ <view v-if="!item.processRouteStatus || !item.processRouteStatus.length"
+ class="no-process">-</view>
+ </view>
+ </scroll-view>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁″垝瀹屾垚</text>
+ <text class="detail-value">{{ formatDate(item.planCompleteTime) }}</text>
+ </view>
+ </view>
+ <view class="item-footer">
+ <view class="action-btns">
+ <up-button type="info"
+ size="small"
+ plain
+ text="鐢熶骇杩芥函"
+ @click="goTraceability(item)"></up-button>
+ <up-button type="info"
+ size="small"
+ plain
+ text="宸ヨ壓璺嚎"
+ @click="goProcessRoute(item)"></up-button>
+ <up-button type="primary"
+ size="small"
+ plain
+ text="鏉ユ簮"
+ @click="goSource(item)"></up-button>
+ <up-button type="success"
+ size="small"
+ plain
+ text="棰嗘枡璇︽儏"
+ @click="goPickingDetail(item)"></up-button>
+ </view>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus"
+ v-if="tableData.length >= page.size" />
+ </scroll-view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鐢熶骇璁㈠崟鏁版嵁"></up-empty>
+ </view>
+ </view>
</template>
<script setup>
-import { ref, reactive, toRefs, getCurrentInstance } from "vue";
-import { onShow } from '@dcloudio/uni-app';
-import dayjs from "dayjs";
-import {schedulingListPage} from "@/api/productionManagement/productionOrder.js";
-import PageHeader from "@/components/PageHeader.vue";
-const { proxy } = getCurrentInstance();
+ import { ref, reactive, toRefs, getCurrentInstance } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import dayjs from "dayjs";
+ import {
+ productOrderListPage,
+ getOrderProcessRouteMain,
+ } from "@/api/productionManagement/productionOrder.js";
+ import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
+ import PageHeader from "@/components/PageHeader.vue";
-// 鍔犺浇鐘舵��
-const loading = ref(false);
-// 鍒楄〃鏁版嵁
-const tableData = ref([]);
+ const { proxy } = getCurrentInstance();
-// 鍒嗛〉閰嶇疆
-const page = reactive({
- current: -1,
- size: -1,
- total: 0,
-});
+ // 鍔犺浇鐘舵��
+ const loading = ref(false);
+ const loadStatus = ref("loadmore");
+ // 鍒楄〃鏁版嵁
+ const tableData = ref([]);
-// 鎼滅储琛ㄥ崟鏁版嵁
-const data = reactive({
- searchForm: {
- customerName: "",
- },
-});
-const { searchForm } = toRefs(data);
+ // 鍒嗛〉閰嶇疆
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
-// 閫氱敤鎻愮ず鍑芥暟
-const showLoadingToast = (message) => {
- uni.showLoading({
- title: message,
- mask: true
- });
-};
+ // 鎼滅储琛ㄥ崟鏁版嵁
+ const data = reactive({
+ searchForm: {
+ keyword: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
-const closeToast = () => {
- uni.hideLoading();
-};
+ // 杩斿洖涓婁竴椤�
+ const goBack = () => {
+ uni.navigateBack();
+ };
-// 杩斿洖涓婁竴椤�
-const goBack = () => {
- uni.navigateBack();
-};
+ // 鏍煎紡鍖栨棩鏈�
+ const formatDate = date => {
+ return date ? dayjs(date).format("YYYY-MM-DD") : "-";
+ };
-// 鏌ヨ鍒楄〃
-const handleQuery = () => {
- page.current = 1;
- tableData.value = []; // 閲嶇疆鍒楄〃鏁版嵁
- getList();
-};
+ // 鑾峰彇鐘舵�佹枃鏈�
+ const getStatusText = status => {
+ const statusMap = {
+ 1: "寰呭紑濮�",
+ 2: "杩涜涓�",
+ 3: "宸插畬鎴�",
+ 4: "宸插彇娑�",
+ 5: "宸茬粨鏉�",
+ };
+ return statusMap[status] || "鏈煡";
+ };
-// 鑾峰彇鍒楄〃鏁版嵁
-const getList = () => {
- loading.value = true;
- showLoadingToast('鍔犺浇涓�...');
-
- // 鏋勯�犺姹傚弬鏁�
- const params = { ...searchForm.value, ...page };
-
- schedulingListPage(params).then((res) => {
- loading.value = false;
- closeToast();
-
- tableData.value = res.data.records || [];
- }).catch(() => {
- loading.value = false;
- closeToast();
- uni.showToast({
- title: '鍔犺浇澶辫触',
- icon: 'error'
- });
- });
-};
+ // 鑾峰彇鐘舵�佺被鍨�
+ const getStatusType = status => {
+ const typeMap = {
+ 1: "primary",
+ 2: "warning",
+ 3: "success",
+ 4: "info",
+ 5: "error",
+ };
+ return typeMap[status] || "info";
+ };
-// 椤甸潰鏄剧ず鏃跺姞杞芥暟鎹�
-onShow(() => {
- // 鍔犺浇鍒楄〃鏁版嵁
- getList();
-});
+ // 瀹屾垚杩涘害鐧惧垎姣�
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
+
+ // 杩涘害鏉¢鑹�
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
+
+ // 鏌ヨ鍒楄〃
+ const handleQuery = () => {
+ page.current = 1;
+ tableData.value = [];
+ getList();
+ };
+
+ // 鍔犺浇鏇村
+ const loadMore = () => {
+ if (loadStatus.value === "nomore" || loading.value) return;
+ page.current++;
+ getList();
+ };
+
+ // 鑾峰彇鍒楄〃鏁版嵁
+ const getList = () => {
+ loading.value = true;
+ loadStatus.value = "loading";
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ npsNo: searchForm.value.keyword,
+ productName: searchForm.value.keyword,
+ };
+
+ productOrderListPage(params)
+ .then(async res => {
+ const records = res.data.records || [];
+
+ // 涓烘瘡涓鍗曞苟琛屾煡璇㈠伐搴忚繘搴�
+ const processPromises = records.map(async item => {
+ if (item.npsNo) {
+ try {
+ const workOrderRes = await productWorkOrderPage({
+ npsNo: item.npsNo,
+ size: 100,
+ });
+ const workOrders = workOrderRes.data.records || [];
+ const processRouteStatus = workOrders.map(wo => ({
+ name: wo.operationName || "鏈煡宸ュ簭",
+ percentage:
+ Number(wo.completionStatus) > 100
+ ? 100
+ : Number(wo.completionStatus || 0),
+ }));
+ return { ...item, processRouteStatus };
+ } catch (error) {
+ console.error(`鑾峰彇宸ュ崟 ${item.npsNo} 杩涘害澶辫触:`, error);
+ return { ...item, processRouteStatus: [] };
+ }
+ }
+ return { ...item, processRouteStatus: [] };
+ });
+
+ const updatedRecords = await Promise.all(processPromises);
+
+ loading.value = false;
+ if (page.current === 1) {
+ tableData.value = updatedRecords;
+ } else {
+ tableData.value = [...tableData.value, ...updatedRecords];
+ }
+
+ if (updatedRecords.length < page.size) {
+ loadStatus.value = "nomore";
+ } else {
+ loadStatus.value = "loadmore";
+ }
+ page.total = res.data.total || 0;
+ })
+ .catch(() => {
+ loading.value = false;
+ loadStatus.value = "loadmore";
+ uni.showToast({
+ title: "鍔犺浇澶辫触",
+ icon: "error",
+ });
+ });
+ };
+
+ // 璺宠浆宸ヨ壓璺嚎 (BOM)
+ const goProcessRoute = item => {
+ getOrderProcessRouteMain(item.id)
+ .then(res => {
+ const data = res.data || {};
+ if (!data.id) {
+ uni.showToast({ title: "鏈壘鍒板伐鑹鸿矾绾�", icon: "none" });
+ return;
+ }
+ uni.navigateTo({
+ url: `/pages/productionManagement/processRoute/items?id=${
+ data.id
+ }&bomId=${data.orderBomId}&processRouteCode=${
+ data.processRouteCode || ""
+ }&productName=${encodeURIComponent(
+ item.productName || ""
+ )}&model=${encodeURIComponent(item.model || "")}&orderId=${
+ item.id
+ }&type=order`,
+ });
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇璺嚎澶辫触", icon: "none" });
+ });
+ };
+
+ // 璺宠浆鏉ユ簮
+ const goSource = item => {
+ uni.navigateTo({
+ url: `/pages/productionManagement/productionOrder/source?id=${
+ item.id
+ }&productName=${encodeURIComponent(
+ item.productName
+ )}&model=${encodeURIComponent(item.model)}&quantity=${item.quantity}`,
+ });
+ };
+
+ // 璺宠浆棰嗘枡璇︽儏
+ const goPickingDetail = item => {
+ uni.navigateTo({
+ url: `/pages/productionManagement/productionOrder/pickingDetail?id=${item.id}&npsNo=${item.npsNo}`,
+ });
+ };
+
+ // 璺宠浆鐢熶骇杩芥函
+ const goTraceability = item => {
+ uni.navigateTo({
+ url: `/pages/productionManagement/productionTraceability/index?npsNo=${item.npsNo}`,
+ });
+ };
+
+ // 椤甸潰鏄剧ず鏃跺姞杞芥暟鎹�
+ onShow(() => {
+ handleQuery();
+ });
</script>
<style scoped lang="scss">
-@import '@/styles/sales-common.scss';
+ @import "@/styles/sales-common.scss";
-// 鐢熶骇璁㈠崟椤甸潰鏍峰紡
-.production-order {
- min-height: 100vh;
- background: #f8f9fa;
- position: relative;
-}
+ .production-order {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
-// 閲嶅啓閮ㄥ垎鏍峰紡浠ラ�傞厤鐢熶骇璁㈠崟
-.ledger-item {
- .detail-value.highlight {
- color: #ff6b35;
- font-weight: 600;
- }
-}
+ .list-container {
+ flex: 1;
+ height: 0;
+ }
-// 閫傞厤 uView 缁勪欢鏍峰紡
-:deep(.up-input) {
- background: transparent;
-}
+ .ledger-item {
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
-:deep(.up-datetime-picker) {
- width: 100%;
-}
+ .item-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 12rpx;
+
+ .item-left {
+ display: flex;
+ align-items: center;
+
+ .document-icon {
+ width: 44rpx;
+ height: 44rpx;
+ background: #3c9cff;
+ border-radius: 10rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-right: 20rpx;
+ }
+
+ .item-id {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+ }
+
+ .item-details {
+ padding: 16rpx 0;
+
+ .detail-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 16rpx;
+
+ .detail-label {
+ font-size: 26rpx;
+ color: #999;
+ min-width: 140rpx;
+ }
+
+ .detail-value {
+ font-size: 26rpx;
+ color: #333;
+ text-align: right;
+
+ &.font-bold {
+ font-weight: bold;
+ }
+ }
+
+ .progress-box {
+ flex: 1;
+ margin-left: 40rpx;
+
+ .progress-text {
+ font-size: 22rpx;
+ color: #999;
+ margin-top: 4rpx;
+ display: block;
+ text-align: right;
+ }
+ }
+
+ &.process-row {
+ flex-direction: column;
+ margin: 20rpx 0;
+
+ .process-scroll {
+ width: 100%;
+ margin-top: 16rpx;
+
+ .process-container {
+ display: flex;
+ align-items: flex-start;
+ padding: 10rpx 0;
+ min-height: 120rpx;
+
+ .process-item {
+ display: flex;
+ align-items: center;
+
+ .process-node {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100rpx;
+
+ .node-circle {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ border: 2rpx solid #3c9cff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #fff;
+ margin-bottom: 8rpx;
+
+ .node-percentage {
+ font-size: 18rpx;
+ color: #3c9cff;
+ font-weight: bold;
+ }
+
+ &.is-complete {
+ border-color: #52c41a;
+ background: #f6ffed;
+
+ .node-percentage {
+ color: #52c41a;
+ }
+ }
+ }
+
+ .node-name {
+ font-size: 20rpx;
+ color: #666;
+ text-align: center;
+ width: 120rpx;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+
+ .node-line {
+ width: 40rpx;
+ height: 2rpx;
+ background: #e8e8e8;
+ margin: -30rpx 0 0 0;
+ }
+ }
+
+ .no-process {
+ font-size: 24rpx;
+ color: #ccc;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .item-footer {
+ padding-top: 20rpx;
+ border-top: 1rpx solid #f0f0f0;
+ display: flex;
+ justify-content: flex-end;
+
+ .action-btns {
+ display: flex;
+ gap: 20rpx;
+ }
+ }
+ }
+
+ .no-data {
+ padding-top: 200rpx;
+ }
</style>
diff --git a/src/pages/productionManagement/productionOrder/pickingDetail.vue b/src/pages/productionManagement/productionOrder/pickingDetail.vue
new file mode 100644
index 0000000..7aad62d
--- /dev/null
+++ b/src/pages/productionManagement/productionOrder/pickingDetail.vue
@@ -0,0 +1,350 @@
+<template>
+ <view class="picking-detail">
+ <PageHeader title="棰嗘枡璇︽儏"
+ @back="goBack" />
+ <scroll-view scroll-y
+ class="detail-list"
+ v-if="detailList.length > 0">
+ <view v-for="(item, index) in detailList"
+ :key="index"
+ class="material-card">
+ <view class="card-header">
+ <text class="material-name">{{ item.materialName || item.productName || '-' }}</text>
+ <up-tag :text="item.operationName || '-'"
+ type="info"
+ size="mini"
+ plain />
+ </view>
+ <view class="card-content">
+ <view class="info-grid">
+ <view class="info-item">
+ <text class="label">瑙勬牸鍨嬪彿</text>
+ <text class="value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">鍗曚綅</text>
+ <text class="value">{{ item.unit || '-' }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">闇�棰嗘暟閲�</text>
+ <text class="value highlight">{{ item.qtyRequired || item.demandedQuantity || 0 }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">宸查鏁伴噺</text>
+ <text class="value success">{{ item.qtyPicked || item.pickQuantity || 0 }}</text>
+ </view>
+ <view class="info-item">
+ <text class="label">琛ユ枡鏁伴噺</text>
+ <view class="value link"
+ @click="showSupplementDetail(item)">
+ {{ item.qtySupplement || item.feedingQty || 0 }}
+ </view>
+ </view>
+ <view class="info-item">
+ <text class="label">閫�鏂欐暟閲�</text>
+ <text class="value">{{ item.returnQty || 0 }}</text>
+ </view>
+ </view>
+ <view class="remark-row"
+ v-if="item.remark">
+ <text class="label">澶囨敞锛�</text>
+ <text class="value">{{ item.remark }}</text>
+ </view>
+ </view>
+ </view>
+ </scroll-view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤棰嗘枡璇︽儏"></up-empty>
+ </view>
+ <!-- 琛ユ枡璁板綍寮圭獥 -->
+ <up-popup :show="showPopup"
+ mode="bottom"
+ @close="showPopup = false"
+ round="10">
+ <view class="popup-content">
+ <view class="popup-header">
+ <text class="title">琛ユ枡璁板綍</text>
+ <up-icon name="close"
+ size="20"
+ @click="showPopup = false"></up-icon>
+ </view>
+ <scroll-view scroll-y
+ class="record-list">
+ <view v-if="supplementRecords.length > 0">
+ <view v-for="(record, rIndex) in supplementRecords"
+ :key="rIndex"
+ class="record-item">
+ <view class="record-row">
+ <text class="record-label">琛ユ枡鏁伴噺锛�</text>
+ <text class="record-value highlight">{{ record.pickQuantity || 0 }}</text>
+ </view>
+ <view class="record-row">
+ <text class="record-label">琛ユ枡浜猴細</text>
+ <text class="record-value">{{ record.supplementUserName || '-' }}</text>
+ </view>
+ <view class="record-row">
+ <text class="record-label">琛ユ枡鏃ユ湡锛�</text>
+ <text class="record-value">{{ record.supplementTime || '-' }}</text>
+ </view>
+ <view class="record-row">
+ <text class="record-label">琛ユ枡鍘熷洜锛�</text>
+ <text class="record-value">{{ record.feedingReason || '-' }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-record">
+ <text>鏆傛棤琛ユ枡璁板綍</text>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import {
+ listMaterialPickingDetail,
+ listMaterialSupplementRecord,
+ } from "@/api/productionManagement/productionOrder.js";
+ import PageHeader from "@/components/PageHeader.vue";
+
+ const npsNo = ref("");
+ const productionOrderId = ref("");
+ const detailList = ref([]);
+ const loading = ref(false);
+
+ // 寮圭獥鐩稿叧
+ const showPopup = ref(false);
+ const supplementRecords = ref([]);
+ const recordLoading = ref(false);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const calculatePending = item => {
+ const required = Number(item.qtyRequired || item.demandedQuantity || 0);
+ const picked = Number(item.qtyPicked || item.pickQuantity || 0);
+ return Math.max(0, required - picked);
+ };
+
+ onLoad(options => {
+ if (options.id) {
+ productionOrderId.value = options.id;
+ npsNo.value = options.npsNo || "";
+ fetchDetail(options.id);
+ }
+ });
+
+ const fetchDetail = id => {
+ loading.value = true;
+ listMaterialPickingDetail(id)
+ .then(res => {
+ detailList.value = res.data?.records || res.data || [];
+ loading.value = false;
+ })
+ .catch(() => {
+ loading.value = false;
+ uni.showToast({
+ title: "鑾峰彇璇︽儏澶辫触",
+ icon: "error",
+ });
+ });
+ };
+
+ const showSupplementDetail = item => {
+ const qty = Number(item.qtySupplement || item.feedingQty || 0);
+ if (qty <= 0) return;
+
+ showPopup.value = true;
+ recordLoading.value = true;
+ supplementRecords.value = [];
+
+ listMaterialSupplementRecord({
+ pickId: item.id,
+ productionOrderId: productionOrderId.value,
+ })
+ .then(res => {
+ supplementRecords.value = res.data || [];
+ recordLoading.value = false;
+ })
+ .catch(() => {
+ recordLoading.value = false;
+ uni.showToast({
+ title: "鑾峰彇琛ユ枡璁板綍澶辫触",
+ icon: "error",
+ });
+ });
+ };
+</script>
+
+<style scoped lang="scss">
+ .picking-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .detail-list {
+ flex: 1;
+ height: 0;
+ padding: 20rpx;
+ }
+
+ .material-card {
+ background: #fff;
+ margin-bottom: 24rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 20rpx;
+ border-bottom: 1rpx solid #f5f5f5;
+ margin-bottom: 20rpx;
+
+ .material-name {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .info-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20rpx;
+
+ .info-item {
+ display: flex;
+ flex-direction: column;
+
+ .label {
+ font-size: 24rpx;
+ color: #999;
+ margin-bottom: 4rpx;
+ }
+
+ .value {
+ font-size: 26rpx;
+ color: #333;
+
+ &.highlight {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+ &.success {
+ color: #67c23a;
+ font-weight: bold;
+ }
+ &.warning {
+ color: #e6a23c;
+ font-weight: bold;
+ }
+ &.link {
+ color: #3c9cff;
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+
+ .remark-row {
+ margin-top: 20rpx;
+ padding-top: 16rpx;
+ border-top: 1rpx dashed #eee;
+ display: flex;
+
+ .label {
+ font-size: 24rpx;
+ color: #999;
+ }
+ .value {
+ font-size: 24rpx;
+ color: #666;
+ flex: 1;
+ }
+ }
+ }
+
+ .no-data {
+ padding-top: 200rpx;
+ }
+
+ /* 寮圭獥鏍峰紡 */
+ .popup-content {
+ background: #fff;
+ padding: 30rpx;
+ max-height: 70vh;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 30rpx;
+ border-bottom: 1rpx solid #eee;
+
+ .title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .record-list {
+ flex: 1;
+ height: 0;
+ padding-top: 20rpx;
+ }
+
+ .record-item {
+ padding: 24rpx;
+ background: #f9f9f9;
+ border-radius: 12rpx;
+ margin-bottom: 20rpx;
+
+ .record-row {
+ display: flex;
+ margin-bottom: 10rpx;
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .record-label {
+ font-size: 26rpx;
+ color: #999;
+ width: 140rpx;
+ }
+
+ .record-value {
+ font-size: 26rpx;
+ color: #333;
+ flex: 1;
+
+ &.highlight {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+ }
+ }
+ }
+
+ .no-record {
+ padding: 100rpx 0;
+ text-align: center;
+ color: #999;
+ font-size: 28rpx;
+ }
+</style>
diff --git a/src/pages/productionManagement/productionOrder/source.vue b/src/pages/productionManagement/productionOrder/source.vue
new file mode 100644
index 0000000..bd06845
--- /dev/null
+++ b/src/pages/productionManagement/productionOrder/source.vue
@@ -0,0 +1,166 @@
+<template>
+ <view class="production-order-source">
+ <PageHeader title="鏉ユ簮鏁版嵁" @back="goBack" />
+
+ <view class="summary-card" v-if="summary">
+ <view class="summary-item">
+ <text class="label">浜у搧鍚嶇О</text>
+ <up-tag :text="summary.productName || '-'" type="primary" size="mini" />
+ </view>
+ <view class="summary-item">
+ <text class="label">瑙勬牸鍨嬪彿</text>
+ <text class="value">{{ summary.model || '-' }}</text>
+ </view>
+ <view class="summary-item">
+ <text class="label">闇�姹傛暟閲�</text>
+ <text class="value highlight">{{ summary.quantity || 0 }}</text>
+ </view>
+ </view>
+
+ <scroll-view scroll-y class="source-list" v-if="sourceList.length > 0">
+ <view v-for="(item, index) in sourceList" :key="index" class="source-card">
+ <view class="card-header">
+ <text class="plan-no">璁″垝鍙�: {{ item.mpsNo || '-' }}</text>
+ <up-tag :text="item.source || '鏈煡'" :type="item.source === '閿�鍞�' ? 'primary' : 'warning'" size="mini" />
+ </view>
+ <view class="card-content">
+ <view class="info-row">
+ <text class="info-label">鍚堝悓鍙�</text>
+ <text class="info-value">{{ item.salesContractNo || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">瀹㈡埛鍚嶇О</text>
+ <text class="info-value">{{ item.customerName || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">椤圭洰鍚嶇О</text>
+ <text class="info-value">{{ item.projectName || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">闇�姹傛暟閲�</text>
+ <text class="info-value">{{ item.qtyRequired || 0 }} {{ item.unit || '' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="info-label">闇�姹傛棩鏈�</text>
+ <text class="info-value">{{ formatDate(item.requiredDate) }}</text>
+ </view>
+ </view>
+ </view>
+ </scroll-view>
+
+ <view v-else class="no-data">
+ <up-empty mode="data" text="鏆傛棤鏉ユ簮鏁版嵁"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from "vue";
+import { onLoad } from '@dcloudio/uni-app';
+import dayjs from "dayjs";
+import { getProductOrderSource } from "@/api/productionManagement/productionOrder.js";
+import PageHeader from "@/components/PageHeader.vue";
+
+const summary = ref(null);
+const sourceList = ref([]);
+const loading = ref(false);
+
+const goBack = () => {
+ uni.navigateBack();
+};
+
+const formatDate = (date) => {
+ return date ? dayjs(date).format('YYYY-MM-DD') : '-';
+};
+
+onLoad((options) => {
+ if (options.id) {
+ summary.value = {
+ productName: decodeURIComponent(options.productName || ''),
+ model: decodeURIComponent(options.model || ''),
+ quantity: options.quantity || 0
+ };
+ fetchSource(options.id);
+ }
+});
+
+const fetchSource = (id) => {
+ loading.value = true;
+ getProductOrderSource(id).then(res => {
+ sourceList.value = res.data || [];
+ loading.value = false;
+ }).catch(() => {
+ loading.value = false;
+ uni.showToast({
+ title: '鑾峰彇鏉ユ簮澶辫触',
+ icon: 'error'
+ });
+ });
+};
+</script>
+
+<style scoped lang="scss">
+.production-order-source {
+ min-height: 100vh;
+ background: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+}
+
+.summary-card {
+ background: #fff;
+ margin: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.05);
+
+ .summary-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12rpx;
+ &:last-child { margin-bottom: 0; }
+
+ .label { font-size: 26rpx; color: #999; }
+ .value { font-size: 26rpx; color: #333; }
+ .highlight { color: #f56c6c; font-weight: bold; }
+ }
+}
+
+.source-list {
+ flex: 1;
+ height: 0;
+ padding: 0 20rpx;
+}
+
+.source-card {
+ background: #fff;
+ margin-bottom: 20rpx;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.03);
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 16rpx;
+ border-bottom: 1rpx solid #f9f9f9;
+ margin-bottom: 16rpx;
+
+ .plan-no { font-size: 28rpx; font-weight: bold; color: #333; }
+ }
+
+ .info-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 12rpx;
+ &:last-child { margin-bottom: 0; }
+
+ .info-label { font-size: 24rpx; color: #999; }
+ .info-value { font-size: 24rpx; color: #666; }
+ }
+}
+
+.no-data { padding-top: 100rpx; }
+</style>
diff --git a/src/pages/productionManagement/productionReport/index.vue b/src/pages/productionManagement/productionReport/index.vue
index dcd0fd7..ac92038 100644
--- a/src/pages/productionManagement/productionReport/index.vue
+++ b/src/pages/productionManagement/productionReport/index.vue
@@ -18,7 +18,7 @@
placeholder="鑷姩濉厖"
disabled />
</u-form-item>
- <u-form-item label="鏈鐢熶骇鏁伴噺"
+ <u-form-item label="鐢熶骇鍚堟牸鏁伴噺"
prop="quantity"
required>
<u-input v-model="form.quantity"
@@ -40,6 +40,67 @@
@click="openProducerPicker"
suffix-icon="arrow-down" />
</u-form-item>
+ <!-- 宸ユ椂 -->
+ <u-form-item label="宸ユ椂"
+ v-if="form.type == 0"
+ prop="workHour">
+ <u-input v-model="form.workHour"
+ placeholder="璇疯緭鍏ュ伐鏃�"
+ type="number" />
+ <text class="param-unit">h</text>
+ </u-form-item>
+ </view>
+ <!-- 鍔ㄦ�佸弬鏁板尯鍩� -->
+ <view class="form-section"
+ v-if="params.length > 0">
+ <view class="section-title">宸ュ簭鍙傛暟</view>
+ <u-form-item v-for="param in params"
+ :key="param.id"
+ :label="param.paramName"
+ :label-width="110"
+ :required="param.required === '1'">
+ <!-- 鏁板瓧绫诲瀷 -->
+ <template v-if="param.paramType == '1'">
+ <u-input v-model="form.paramGroups[param.id]"
+ type="number"
+ :placeholder="'璇疯緭鍏�' + param.paramName"
+ :key="param.id" />
+ <text v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</text>
+ </template>
+ <!-- 鏂囨湰绫诲瀷 -->
+ <template v-else-if="param.paramType == '2'">
+ <u-input v-model="form.paramGroups[param.id]"
+ :placeholder="'璇疯緭鍏�' + param.paramName"
+ :key="param.id" />
+ <text v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</text>
+ </template>
+ <!-- 閫夋嫨绫诲瀷 -->
+ <template v-else-if="param.paramType == '3'">
+ <u-input v-model="form.paramGroups[param.id]"
+ readonly
+ :placeholder="'璇烽�夋嫨' + param.paramName"
+ @click="openParamSelect(param)"
+ suffix-icon="arrow-down" />
+ </template>
+ <!-- 鏃ユ湡绫诲瀷 -->
+ <template v-else-if="param.paramType == '4'">
+ <u-input v-model="form.paramGroups[param.id]"
+ readonly
+ :placeholder="'璇烽�夋嫨' + param.paramName"
+ @click="openDateParamPicker(param)"
+ suffix-icon="arrow-down" />
+ <text v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</text>
+ </template>
+ <!-- 榛樿鏂囨湰 -->
+ <template v-else>
+ <u-input v-model="form.paramGroups[param.id]"
+ :placeholder="'璇疯緭鍏�' + param.paramName"
+ :key="param.id" />
+ </template>
+ </u-form-item>
</view>
<!-- 浣跨敤FooterButtons缁勪欢 -->
<FooterButtons @cancel="goBack"
@@ -54,6 +115,18 @@
title="閫夋嫨鐢熶骇浜�"
@select="onProducerConfirm"
@close="showProducerPicker = false" />
+ <!-- 鍙傛暟閫夋嫨鍣� -->
+ <up-action-sheet :show="showParamSelect"
+ :actions="paramOptions"
+ :title="currentParam?.paramName || '閫夋嫨'"
+ @select="onParamConfirm"
+ @close="showParamSelect = false" />
+ <!-- 鏃ユ湡閫夋嫨鍣� -->
+ <up-datetime-picker :show="showDatePicker"
+ v-model="datePickerValue"
+ :mode="datePickerMode"
+ @confirm="onDateConfirm"
+ @cancel="showDatePicker = false" />
</view>
</template>
@@ -71,34 +144,50 @@
import { addProductMain } from "@/api/productionManagement/productionReporting";
import { getInfo } from "@/api/login";
import { userListNoPageByTenantId } from "@/api/system/user";
+ import {
+ findProcessParamListOrder,
+ listMaterialPickingDetail,
+ } from "@/api/productionManagement/productionOrder.js";
+ import { getDicts } from "@/api/system/dict/data";
+ import { formatDateToYMD, parseTime } from "@/utils/ruoyi";
- // 琛ㄥ崟寮曠敤
const formRef = ref();
- // 琛ㄥ崟鏁版嵁
let form = ref({
planQuantity: "",
quantity: "",
scrapQty: "",
userName: "",
workOrderId: "",
- productProcessRouteItemId: "",
userId: "",
schedulingUserId: "",
+ reportWork: "",
+ productionOrderRoutingOperationId: "",
+ productionOrderId: "",
+ workHour: 0,
+ type: null,
+ paramGroups: {},
});
- // 鐢熶骇浜洪�夋嫨鍣ㄧ姸鎬�
const showProducerPicker = ref(false);
const producerList = ref([]);
- // 鎵撳紑鐢熶骇浜洪�夋嫨鍣�
+ const params = ref([]);
+ const dictOptions = ref({});
+ const showParamSelect = ref(false);
+ const currentParam = ref(null);
+ const paramOptions = ref([]);
+
+ const showDatePicker = ref(false);
+ const datePickerValue = ref(Date.now());
+ const datePickerMode = ref("date");
+ const currentDateParam = ref(null);
+
const openProducerPicker = async () => {
if (producerList.value.length === 0) {
- // 濡傛灉鍒楄〃涓虹┖锛屽厛鍔犺浇鐢ㄦ埛鍒楄〃
try {
const res = await userListNoPageByTenantId();
const users = res.data || [];
- // 杞崲涓� action-sheet 闇�瑕佺殑鏍煎紡
producerList.value = users.map(user => ({
name: user.nickName || user.userName,
value: user.userId,
@@ -112,102 +201,265 @@
showProducerPicker.value = true;
};
- // 鐢熶骇浜洪�夋嫨纭
const onProducerConfirm = e => {
form.value.schedulingUserId = e.value;
form.value.userName = e.name;
- form.value.userId = e.value; // 鍚屾椂鏇存柊 userId
+ form.value.userId = e.value;
showProducerPicker.value = false;
};
- // 鎻愪氦鐘舵��
+ const openParamSelect = async param => {
+ currentParam.value = param;
+ if (param.paramType == "3" && param.paramFormat) {
+ const options = await getDictOptions(param.paramFormat);
+ paramOptions.value = options.map(opt => ({
+ name: opt.dictLabel,
+ value: opt.dictLabel,
+ }));
+ }
+ showParamSelect.value = true;
+ };
+
+ const onParamConfirm = e => {
+ if (currentParam.value) {
+ form.value.paramGroups[currentParam.value.id] = e.value;
+ }
+ showParamSelect.value = false;
+ };
+
+ const openDateParamPicker = param => {
+ currentDateParam.value = param;
+ const currentValue = form.value.paramGroups[param.id];
+ datePickerValue.value = currentValue
+ ? new Date(currentValue).getTime()
+ : Date.now();
+ // 鍙傜収 PC 绔�昏緫锛氬鏋滄牸寮忔槸 yyyy-MM-dd 鍒欎负 date 妯″紡锛屽惁鍒欎负 datetime 妯″紡
+ datePickerMode.value =
+ param.paramFormat === "yyyy-MM-dd" ? "date" : "datetime";
+ showDatePicker.value = true;
+ };
+
+ const onDateConfirm = e => {
+ if (currentDateParam.value) {
+ const format =
+ currentDateParam.value.paramFormat === "yyyy-MM-dd"
+ ? "{y}-{m}-{d}"
+ : "{y}-{m}-{d} {h}:{i}:{s}";
+ form.value.paramGroups[currentDateParam.value.id] = parseTime(
+ e.value,
+ format
+ );
+ }
+ showDatePicker.value = false;
+ };
+
+ const getDictOptions = async dictType => {
+ if (!dictType) return [];
+ if (dictOptions.value[dictType]) return dictOptions.value[dictType];
+ try {
+ const res = await getDicts(dictType);
+ if (res.code === 200) {
+ dictOptions.value[dictType] = res.data;
+ return res.data;
+ }
+ return [];
+ } catch (error) {
+ console.error("鑾峰彇瀛楀吀鏁版嵁澶辫触:", error);
+ return [];
+ }
+ };
+
+ const loadParams = (productionOrderRoutingOperationId, productionOrderId) => {
+ findProcessParamListOrder({
+ productionOrderRoutingOperationId,
+ productionOrderId,
+ })
+ .then(res => {
+ if (res.code === 200) {
+ console.log(res.data, "res.data========");
+
+ const paramList = res.data || [];
+ params.value = paramList;
+ form.value.paramGroups = {};
+ paramList.forEach(param => {
+ if (!form.value.paramGroups[param.id]) {
+ form.value.paramGroups[param.id] = "";
+ }
+ if (param.paramType == "3" && param.paramFormat) {
+ getDictOptions(param.paramFormat);
+ }
+ });
+ }
+ })
+ .catch(err => {
+ console.error("鑾峰彇宸ュ簭鍙傛暟澶辫触:", err);
+ });
+ };
+
const submitting = ref(false);
- // 杩斿洖涓婁竴椤�
const goBack = () => {
uni.navigateBack();
};
- // 鎻愪氦琛ㄥ崟
+
const submitForm = async () => {
submitting.value = true;
- // 鏍¢獙琛ㄥ崟
+
if (!form.value.quantity) {
submitting.value = false;
- showToast("璇疯緭鍏ユ湰娆$敓浜ф暟閲�");
+ showToast("璇疯緭鍏ョ敓浜у悎鏍兼暟閲�");
return;
}
+
if (!form.value.schedulingUserId) {
submitting.value = false;
showToast("璇烽�夋嫨鐢熶骇浜�");
return;
}
- // 杞崲涓烘暟瀛楄繘琛屾瘮杈�
+
const quantity = Number(form.value.quantity) || 0;
const scrapQty = Number(form.value.scrapQty) || 0;
const planQuantity = Number(form.value.planQuantity);
- // 楠岃瘉鐢熶骇鏁伴噺鍜屾姤搴熸暟閲忕殑鍜屼笉鑳借秴杩囧緟鐢熶骇鏁伴噺
- if (quantity + scrapQty > planQuantity) {
+
+ if (quantity < 0) {
submitting.value = false;
- showToast("鐢熶骇鏁伴噺鍜屾姤搴熸暟閲忕殑鍜屼笉鑳借秴杩囧緟鐢熶骇鏁伴噺");
+ showToast("鐢熶骇鍚堟牸鏁伴噺蹇呴』澶т簬绛変簬0");
return;
}
- if (quantity > planQuantity) {
+
+ // if (quantity + scrapQty > planQuantity) {
+ // submitting.value = false;
+ // showToast("鐢熶骇鏁伴噺鍜屾姤搴熸暟閲忕殑鍜屼笉鑳借秴杩囧緟鐢熶骇鏁伴噺");
+ // return;
+ // }
+
+ if (scrapQty < 0) {
submitting.value = false;
- showToast("鏈鐢熶骇鏁伴噺涓嶈兘澶т簬寰呯敓浜ф暟閲�");
+ showToast("鎶ュ簾鏁伴噺涓嶈兘灏忎簬0");
return;
}
- // 鍑嗗鎻愪氦鏁版嵁锛岀‘淇濇暟閲忓瓧娈典负鏁板瓧绫诲瀷
+
+ // if (scrapQty > quantity) {
+ // submitting.value = false;
+ // showToast("鎶ュ簾鏁伴噺涓嶈兘澶т簬鏈鐢熶骇鏁伴噺");
+ // return;
+ // }
+
+ const productionOperationParamList = params.value.map(param => ({
+ ...param,
+ inputValue: form.value.paramGroups[param.id] ?? "",
+ }));
+
const submitData = {
- ...form.value,
- quantity: Number(form.value.quantity),
- scrapQty: Number(form.value.scrapQty) || 0,
- planQuantity: Number(form.value.planQuantity) || 0,
+ quantity: quantity,
+ scrapQty: scrapQty,
+ userId: form.value.userId,
+ userName: form.value.userName,
+ productionOperationTaskId: form.value.workOrderId,
+ reportWork: form.value.reportWork,
+ productionOrderRoutingOperationId:
+ form.value.productionOrderRoutingOperationId,
+ productionOrderId: form.value.productionOrderId,
+ workHour: form.value.workHour,
+ productionOperationParamList: productionOperationParamList,
};
+
console.log(submitData, "submitData");
- addProductMain(submitData).then(res => {
- if (res.code === 200) {
- showToast("鎶ュ伐鎴愬姛");
+ addProductMain(submitData)
+ .then(res => {
+ if (res.code === 200) {
+ showToast("鎶ュ伐鎴愬姛");
+ submitting.value = false;
+ setTimeout(() => {
+ goBack();
+ }, 1000);
+ } else {
+ showToast(res.msg || "鎶ュ伐澶辫触");
+ submitting.value = false;
+ }
+ })
+ .catch(() => {
+ showToast("鎶ュ伐澶辫触");
submitting.value = false;
- setTimeout(() => {
- goBack();
- }, 1000);
- } else {
- showToast(res.msg || "鎶ュ伐澶辫触");
- submitting.value = false;
- }
- });
+ });
};
- // 椤甸潰鍔犺浇鏃跺垵濮嬪寲鏁版嵁
- onLoad(options => {
+ onLoad(async options => {
console.log(options, "options");
- // 濡傛灉娌℃湁 orderRow 鍙傛暟锛岃鏄庢槸浠庨椤电洿鎺ヨ烦杞紝闇�瑕佺敤鎴锋墜鍔ㄩ�夋嫨璁㈠崟
if (!options.orderRow) {
console.log("浠庨椤佃烦杞紝鏃犺鍗曟暟鎹�");
getInfo().then(res => {
- // 榛樿浣跨敤褰撳墠鐧诲綍鐢ㄦ埛
form.value.userId = res.user.userId;
- form.value.userName = res.user.userName;
+ form.value.userName = res.user.nickName || res.user.userName;
form.value.schedulingUserId = res.user.userId;
});
return;
}
try {
- const orderRow = JSON.parse(options.orderRow);
+ const orderRow = JSON.parse(decodeURIComponent(options.orderRow));
console.log("鏋勯�犵殑orderRow:", orderRow);
- console.log(orderRow, "orderRow======########");
- // 纭繚 planQuantity 杞崲涓哄瓧绗︿覆锛屼互渚垮湪 u-input 涓纭樉绀�
- form.value.planQuantity = orderRow.planQuantity != null ? String(orderRow.planQuantity) : "";
- form.value.productProcessRouteItemId = orderRow.productProcessRouteItemId || "";
+
+ // 鍙傜収 PC 绔�昏緫锛氭湭棰嗘枡鏃犳硶鎶ュ伐
+ if (orderRow.productionOrderId) {
+ try {
+ const res = await listMaterialPickingDetail(orderRow.productionOrderId);
+ const records = Array.isArray(res.data)
+ ? res.data
+ : res.data?.records || [];
+ if (res.code === 200 && records.length === 0) {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "鏈鏂欐棤娉曟姤宸�",
+ showCancel: false,
+ success: () => {
+ goBack();
+ },
+ });
+ return;
+ }
+ } catch (error) {
+ console.error("鏌ヨ棰嗘枡璇︽儏澶辫触:", error);
+ }
+ }
+
+ const planQuantity = Number(orderRow.planQuantity || 0);
+ const completeQuantity = Number(orderRow.completeQuantity || 0);
+ form.value.planQuantity = String(
+ Math.max(0, planQuantity - completeQuantity)
+ );
form.value.workOrderId = orderRow.id || "";
+ form.value.reportWork = orderRow.reportWork || "";
+ form.value.productionOrderRoutingOperationId =
+ orderRow.productionOrderRoutingOperationId || "";
+ form.value.productionOrderId = orderRow.productionOrderId || "";
+ form.value.type = orderRow.type;
+
+ if (orderRow.type == 0) {
+ form.value.workHour = orderRow.workHour || 0;
+ } else {
+ form.value.workHour = 0;
+ }
+
getInfo().then(res => {
- // 榛樿浣跨敤褰撳墠鐧诲綍鐢ㄦ埛锛屼絾鍏佽鐢ㄦ埛淇敼
form.value.userId = res.user.userId;
- form.value.userName = res.user.userName;
+ form.value.userName = res.user.nickName || res.user.userName;
form.value.schedulingUserId = res.user.userId;
});
- // 浣跨敤 nextTick 纭繚 DOM 鏇存柊
+ console.log(orderRow, "orderRow=====");
+
+ if (
+ orderRow.productionOrderRoutingOperationId &&
+ orderRow.productionOrderId
+ ) {
+ nextTick(() => {
+ loadParams(
+ orderRow.productionOrderRoutingOperationId,
+ orderRow.productionOrderId
+ );
+ });
+ }
+
nextTick(() => {
console.log("form.value after assignment:", form.value);
});
@@ -215,13 +467,30 @@
console.error("璁㈠崟瑙f瀽澶辫触:", error);
showToast("璁㈠崟瑙f瀽澶辫触");
goBack();
- return;
}
});
</script>
<style scoped lang="scss">
@import "@/static/scss/form-common.scss";
+
+ .form-section {
+ background: #fff;
+ margin-bottom: 12px;
+ padding: 0 16px;
+ }
+
+ .section-title {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #303133;
+ padding: 24rpx 0 16rpx;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .param-unit {
+ margin-left: 8rpx;
+ color: #909399;
+ font-size: 24rpx;
+ }
</style>
-
-
diff --git a/src/pages/productionManagement/productionReporting/ledger.vue b/src/pages/productionManagement/productionReporting/ledger.vue
new file mode 100644
index 0000000..fb5d1dd
--- /dev/null
+++ b/src/pages/productionManagement/productionReporting/ledger.vue
@@ -0,0 +1,424 @@
+<template>
+ <view class="reporting-ledger">
+ <PageHeader title="鎶ュ伐鍙拌处"
+ @back="goBack" />
+ <!-- 鎼滅储鍖哄煙 - 鍙傝�冮噰璐彴璐︽牱寮� -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ュ伐鍗曞彿鎼滅储"
+ v-model="searchForm.keyword"
+ @change="handleQuery"
+ clearable />
+ </view>
+ <view class="filter-button"
+ @click="handleQuery">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <!-- 鍒楄〃鍖哄煙 -->
+ <scroll-view scroll-y
+ class="list-container"
+ v-if="tableData.length > 0"
+ @scrolltolower="loadMore">
+ <view class="ledger-list">
+ <view v-for="(item, index) in tableData"
+ :key="item.id || index">
+ <view class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.productNo || '-' }}</text>
+ </view>
+ <view class="item-tag">
+ <text class="create-time">{{ formatDate(item.createTime) }}</text>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ伐浜哄憳</text>
+ <text class="detail-value highlight">{{ item.nickName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ユ椂(h)</text>
+ <text class="detail-value">{{ item.workHour || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵�灞炲伐搴�</text>
+ <view class="detail-value">
+ <up-tag :text="item.process || '-'"
+ type="primary"
+ size="mini"
+ plain />
+ </view>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ崟缂栧彿</text>
+ <text class="detail-value">{{ item.workOrderNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閿�鍞悎鍚屽彿</text>
+ <text class="detail-value">{{ item.salesContractNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value font-bold">{{ item.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.productModelName || '-' }}</text>
+ </view>
+ <view class="quantity-section">
+ <view class="qty-item">
+ <text class="qty-label">浜у嚭鏁伴噺</text>
+ <text class="qty-value success">{{ item.quantity || 0 }} {{ item.unit || '' }}</text>
+ </view>
+ <view class="qty-item">
+ <text class="qty-label">鎶ュ簾鏁伴噺</text>
+ <text class="qty-value error">{{ item.scrapQty || 0 }}</text>
+ </view>
+ </view>
+ <view class="item-footer">
+ <view class="action-buttons">
+ <up-button type="primary"
+ size="small"
+ plain
+ text="鏌ョ湅鎶曞叆"
+ @click="handleShowInput(item)"></up-button>
+ <up-button type="info"
+ size="small"
+ plain
+ text="鍙傛暟璇︽儏"
+ @click="handleShowParams(item)"></up-button>
+ <!-- <up-button type="error"
+ size="small"
+ plain
+ text="鍒犻櫎"
+ @click="handleDelete(item)"></up-button> -->
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus"
+ v-if="tableData.length >= page.size" />
+ </scroll-view>
+ <view v-else
+ class="empty-state">
+ <up-empty mode="data"
+ text="鏆傛棤鎶ュ伐鍙拌处鏁版嵁"></up-empty>
+ </view>
+ <!-- 鎶曞叆璇︽儏寮圭獥 -->
+ <up-modal :show="inputVisible"
+ title="鎶曞叆璇︽儏"
+ @confirm="inputVisible = false">
+ <view class="modal-content scroll-view">
+ <view v-if="inputList.length > 0">
+ <view v-for="(input, idx) in inputList"
+ :key="idx"
+ class="detail-item">
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ伐鍗曞彿</text>
+ <text class="detail-value">{{ input.productNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曞叆浜у搧鍚嶇О</text>
+ <text class="detail-value font-bold">{{ input.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曞叆浜у搧鍨嬪彿</text>
+ <text class="detail-value">{{ input.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曞叆鏁伴噺</text>
+ <text class="detail-value highlight">{{ input.quantity || 0 }} {{ input.unit || '' }}</text>
+ </view>
+ <up-divider></up-divider>
+ </view>
+ </view>
+ <up-empty v-else
+ mode="data"
+ text="鏆傛棤鎶曞叆鏁版嵁" />
+ </view>
+ </up-modal>
+ <!-- 鍙傛暟璇︽儏寮圭獥 -->
+ <up-modal :show="paramsVisible"
+ title="鍙傛暟璇︽儏"
+ @confirm="paramsVisible = false">
+ <view class="modal-content">
+ <view v-if="currentParams.length > 0">
+ <view v-for="(param, idx) in currentParams"
+ :key="idx"
+ class="detail-row">
+ <text class="detail-label">{{ param.paramName }}</text>
+ <text class="detail-value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? '(' + param.unit + ')' : '' }}</text>
+ </view>
+ </view>
+ <up-empty v-else
+ mode="data"
+ text="鏆傛棤鍙傛暟鏁版嵁" />
+ </view>
+ </up-modal>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted } from "vue";
+ import dayjs from "dayjs";
+ import {
+ productionProductMainListPage,
+ productionReportDelete,
+ productionProductInputListPage,
+ } from "@/api/productionManagement/productionProductMain.js";
+ import PageHeader from "@/components/PageHeader.vue";
+ import modal from "@/plugins/modal";
+
+ const tableData = ref([]);
+ const loading = ref(false);
+ const loadStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ const searchForm = reactive({
+ keyword: "",
+ });
+
+ // 鎶曞叆璇︽儏鐩稿叧
+ const inputVisible = ref(false);
+ const inputList = ref([]);
+
+ // 鍙傛暟璇︽儏鐩稿叧
+ const paramsVisible = ref(false);
+ const currentParams = ref([]);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const formatDate = date => {
+ return date ? dayjs(date).format("YYYY-MM-DD HH:mm") : "-";
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ tableData.value = [];
+ getList();
+ };
+
+ const loadMore = () => {
+ if (loadStatus.value === "nomore" || loading.value) return;
+ page.current++;
+ getList();
+ };
+
+ const getList = () => {
+ loading.value = true;
+ loadStatus.value = "loading";
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ workOrderNo: searchForm.keyword,
+ };
+
+ productionProductMainListPage(params)
+ .then(res => {
+ loading.value = false;
+ const records = res.data.records || [];
+ if (page.current === 1) {
+ tableData.value = records;
+ } else {
+ tableData.value = [...tableData.value, ...records];
+ }
+
+ if (records.length < page.size) {
+ loadStatus.value = "nomore";
+ } else {
+ loadStatus.value = "loadmore";
+ }
+ page.total = res.data.total || 0;
+ })
+ .catch(() => {
+ loading.value = false;
+ loadStatus.value = "loadmore";
+ modal.msgError("鍔犺浇澶辫触");
+ });
+ };
+
+ const handleShowInput = item => {
+ modal.loading("鍔犺浇涓�...");
+ productionProductInputListPage({
+ productMainId: item.id,
+ current: 1,
+ size: 100,
+ })
+ .then(res => {
+ modal.closeLoading();
+ inputList.value = res.data.records || [];
+ inputVisible.value = true;
+ })
+ .catch(() => {
+ modal.closeLoading();
+ modal.msgError("鍔犺浇鎶曞叆鏁版嵁澶辫触");
+ });
+ };
+
+ const handleShowParams = item => {
+ currentParams.value = item.productionOperationParamList || [];
+ paramsVisible.value = true;
+ };
+
+ const handleDelete = item => {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "纭畾瑕佸垹闄よ鎶ュ伐璁板綍鍚楋紵",
+ success: res => {
+ if (res.confirm) {
+ productionReportDelete({ id: item.id }).then(res => {
+ if (res.code === 200) {
+ modal.msgSuccess("鍒犻櫎鎴愬姛");
+ handleQuery();
+ } else {
+ modal.msgError(res.msg || "鍒犻櫎澶辫触");
+ }
+ });
+ }
+ },
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .reporting-ledger {
+ min-height: 100vh;
+ background-color: #f8f9fa;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .list-container {
+ flex: 1;
+ height: 0;
+ }
+
+ .ledger-item {
+ .item-header {
+ .item-tag {
+ .create-time {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+ }
+
+ .item-details {
+ .quantity-section {
+ display: flex;
+ background-color: #f9f9f9;
+ border-radius: 8rpx;
+ padding: 20rpx;
+ margin: 20rpx 0;
+
+ .qty-item {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &:first-child {
+ border-right: 1rpx solid #eee;
+ }
+
+ .qty-label {
+ font-size: 24rpx;
+ color: #999;
+ margin-bottom: 8rpx;
+ }
+
+ .qty-value {
+ font-size: 32rpx;
+ font-weight: bold;
+
+ &.success {
+ color: #52c41a;
+ }
+
+ &.error {
+ color: #f56c6c;
+ }
+ }
+ }
+ }
+
+ .item-footer {
+ padding-top: 20rpx;
+ border-top: 1rpx solid #f0f0f0;
+
+ .action-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 20rpx;
+ }
+ }
+ }
+ }
+
+ .modal-content {
+ width: 100%;
+ padding: 20rpx 0;
+ max-height: 60vh;
+ overflow-y: auto;
+
+ .detail-item {
+ padding-bottom: 20rpx;
+ }
+
+ .detail-row {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16rpx;
+
+ .detail-label {
+ font-size: 26rpx;
+ color: #999;
+ }
+
+ .detail-value {
+ font-size: 26rpx;
+ color: #333;
+
+ &.font-bold {
+ font-weight: bold;
+ }
+
+ &.highlight {
+ color: #3c9cff;
+ }
+ }
+ }
+ }
+
+ .empty-state {
+ padding-top: 200rpx;
+ }
+</style>
diff --git a/src/pages/productionManagement/productionScheduling/index.vue b/src/pages/productionManagement/productionScheduling/index.vue
new file mode 100644
index 0000000..5cc2c0c
--- /dev/null
+++ b/src/pages/productionManagement/productionScheduling/index.vue
@@ -0,0 +1,241 @@
+<template>
+ <view class="production-scheduling">
+ <!-- 閫氱敤椤甸潰澶撮儴 -->
+ <PageHeader title="鐢熶骇鎺掍骇"
+ @back="goBack" />
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input class="search-text"
+ placeholder="璇疯緭鍏ュ伐鍗曠紪鍙�"
+ v-model="searchForm.workOrderNo"
+ @confirm="handleQuery"
+ clearable />
+ </view>
+ <view class="filter-button"
+ @click="handleQuery">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <!-- 鍒楄〃 -->
+ <scroll-view scroll-y
+ class="ledger-list"
+ v-if="tableData.length > 0"
+ @scrolltolower="loadMore">
+ <view v-for="(item, index) in tableData"
+ :key="item.id || index"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.workOrderNo }}</text>
+ </view>
+ <view class="item-right">
+ <up-tag :text="item.workOrderType"
+ size="mini"
+ type="primary"
+ plain></up-tag>
+ </view>
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鐢熶骇璁㈠崟鍙�</text>
+ <text class="detail-value">{{ item.npsNo || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value">{{ item.productName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ item.model || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ item.unit || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸ュ簭鍚嶇О</text>
+ <text class="detail-value">{{ item.operationName || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">闇�姹傛暟閲�</text>
+ <text class="detail-value">{{ item.planQuantity || 0 }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹屾垚鏁伴噺</text>
+ <text class="detail-value">{{ item.completeQuantity || 0 }}</text>
+ </view>
+ <view class="progress-section">
+ <text class="detail-label">瀹屾垚杩涘害</text>
+ <view class="progress-bar">
+ <up-line-progress :percentage="toProgressPercentage(item.completionStatus)"
+ :activeColor="progressColor(item.completionStatus)"
+ :showText="true"></up-line-progress>
+ </view>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁″垝寮�濮�</text>
+ <text class="detail-value">{{ item.planStartTime || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁″垝缁撴潫</text>
+ <text class="detail-value">{{ item.planEndTime || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹為檯寮�濮�</text>
+ <text class="detail-value">{{ item.actualStartTime || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹為檯缁撴潫</text>
+ <text class="detail-value">{{ item.actualEndTime || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎸囧畾鎶ュ伐浜�</text>
+ <view class="detail-value tags-box">
+ <template v-if="item.userNames">
+ <up-tag v-for="(name, idx) in item.userNames.split(',')"
+ :key="idx"
+ :text="name"
+ size="mini"
+ type="info"
+ plain
+ class="user-tag"></up-tag>
+ </template>
+ <text v-else>-</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <up-loadmore :status="loadStatus" />
+ </scroll-view>
+ <view v-else-if="!loading"
+ class="no-data">
+ <up-empty mode="data"
+ text="鏆傛棤鏁版嵁"></up-empty>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, toRefs, getCurrentInstance } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
+ import PageHeader from "@/components/PageHeader.vue";
+
+ const { proxy } = getCurrentInstance();
+
+ const loading = ref(false);
+ const tableData = ref([]);
+ const loadStatus = ref("loadmore");
+
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ const data = reactive({
+ searchForm: {
+ workOrderNo: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ tableData.value = [];
+ getList();
+ };
+
+ const getList = () => {
+ if (loading.value) return;
+ loading.value = true;
+
+ const params = {
+ ...searchForm.value,
+ ...page,
+ };
+
+ productWorkOrderPage(params)
+ .then(res => {
+ loading.value = false;
+ const records = res.data.records || [];
+ tableData.value =
+ page.current === 1 ? records : [...tableData.value, ...records];
+ page.total = res.data.total;
+
+ if (tableData.value.length >= page.total) {
+ loadStatus.value = "nomore";
+ } else {
+ loadStatus.value = "loadmore";
+ }
+ })
+ .catch(() => {
+ loading.value = false;
+ uni.showToast({ title: "鍔犺浇澶辫触", icon: "error" });
+ });
+ };
+
+ const loadMore = () => {
+ if (loadStatus.value === "nomore" || loading.value) return;
+ page.current++;
+ getList();
+ };
+
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
+
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
+
+ onShow(() => {
+ handleQuery();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .production-scheduling {
+ padding-bottom: 20rpx;
+ }
+ .progress-bar {
+ margin-top: 20rpx;
+ margin-bottom: 20rpx;
+ }
+
+ .tags-box {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8rpx;
+ justify-content: flex-end;
+ }
+
+ .user-tag {
+ margin-bottom: 4rpx;
+ }
+</style>
diff --git a/src/pages/productionManagement/productionTraceability/index.vue b/src/pages/productionManagement/productionTraceability/index.vue
new file mode 100644
index 0000000..d187543
--- /dev/null
+++ b/src/pages/productionManagement/productionTraceability/index.vue
@@ -0,0 +1,1032 @@
+<template>
+ <view class="production-traceability">
+ <PageHeader title="鐢熶骇杩芥函"
+ @back="goBack" />
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-bar"
+ @click="openNpsNoSelector">
+ <view class="search-input">
+ <text v-if="!selectedNpsNo"
+ class="placeholder">璇烽�夋嫨鐢熶骇璁㈠崟鍙�</text>
+ <text v-else
+ class="value">{{ selectedNpsNoLabel }}</text>
+ </view>
+ <view class="search-button">
+ <up-icon name="arrow-down"
+ size="20"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+ <!-- 鍐呭鍖哄煙 -->
+ <view class="content-container"
+ v-if="rowData.productionOrderDto">
+ <!-- 鍩虹淇℃伅 -->
+ <view class="info-card">
+ <view class="card-title">鍩虹淇℃伅</view>
+ <view class="base-info">
+ <view class="info-row">
+ <text class="label">鐢熶骇璁㈠崟鍙凤細</text>
+ <text class="value">{{ rowData.productionOrderDto?.npsNo || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">浜у搧鍚嶇О锛�</text>
+ <text class="value">{{ rowData.productionOrderDto?.productName || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">瑙勬牸鍨嬪彿锛�</text>
+ <text class="value">{{ rowData.productionOrderDto?.model || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">璁″垝鏁伴噺锛�</text>
+ <text class="value">{{ rowData.productionOrderDto?.quantity || 0 }} {{ rowData.productionOrderDto?.unit }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">褰撳墠鐘舵�侊細</text>
+ <view class="value">
+ <up-tag :text="getStatusText(rowData.productionOrderDto?.status)"
+ :type="getStatusType(rowData.productionOrderDto?.status)"
+ size="mini" />
+ </view>
+ </view>
+ <view class="info-row">
+ <text class="label">瀹㈡埛鍚嶇О锛�</text>
+ <text class="value">{{ rowData.productionOrderDto?.customerName || '-' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">寮�濮嬫棩鏈燂細</text>
+ <text class="value">{{ formatDate(rowData.productionOrderDto?.startTime) }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">瀹屾垚杩涘害锛�</text>
+ <view class="value progress-box">
+ <up-line-progress :percentage="formatProgress(rowData.productionOrderDto?.completionStatus)"
+ :activeColor="progressColor(formatProgress(rowData.productionOrderDto?.completionStatus))"
+ :showText="true" />
+ </view>
+ </view>
+ </view>
+ </view>
+ <!-- 宸ュ崟淇℃伅 -->
+ <view class="work-order-section"
+ v-if="rowData.productionRecords && rowData.productionRecords.length > 0">
+ <view class="section-title">宸ュ崟淇℃伅</view>
+ <view v-for="(item, index) in rowData.productionRecords"
+ :key="index"
+ class="work-order-card">
+ <view class="card-header">
+ <text class="work-order-no">{{ item.workOrder.workOrderNo }}</text>
+ <text class="progress-tag"
+ :style="{ color: progressColor(item.workOrder.completionStatus) }">{{ item.workOrder.completionStatus || 0 }}%</text>
+ </view>
+ <view class="card-content">
+ <view class="content-row">
+ <text class="label">浜у搧/瑙勬牸锛�</text>
+ <text class="value">{{ item.workOrder.productName }} / {{ item.workOrder.model }}</text>
+ </view>
+ <view class="content-row">
+ <text class="label">褰撳墠宸ュ簭锛�</text>
+ <text class="value">{{ item.workOrder.operationName || '-' }}</text>
+ </view>
+ <view class="content-row">
+ <text class="label">闇�姹�/瀹屾垚锛�</text>
+ <text class="value">{{ item.workOrder.planQuantity }} / {{ item.workOrder.completeQuantity }}</text>
+ </view>
+ <view class="content-row">
+ <text class="label">鎶ュ簾鏁伴噺锛�</text>
+ <text class="value error-text">{{ item.workOrder.scrapQty || 0 }}</text>
+ </view>
+ </view>
+ <view class="card-footer">
+ <up-button type="primary"
+ size="small"
+ plain
+ text="鎶ュ伐璁板綍"
+ @click="handleShowReports(item)"></up-button>
+ <up-button type="success"
+ size="small"
+ plain
+ text="璐ㄦ淇℃伅"
+ @click="handleShowQuality(item)"></up-button>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data-minor">
+ <up-empty mode="data"
+ text="鏆傛棤宸ュ崟淇℃伅"
+ icon-size="40"></up-empty>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <up-empty mode="search"
+ text="璇烽�夋嫨鐢熶骇璁㈠崟鍙锋煡鐪嬭拷婧俊鎭�"></up-empty>
+ </view>
+ <!-- 鐢熶骇璁㈠崟鍙烽�夋嫨寮圭獥 -->
+ <up-popup :show="showNpsNoSelector"
+ mode="bottom"
+ @close="showNpsNoSelector = false"
+ round="10">
+ <view class="selector-popup">
+ <view class="popup-header">
+ <text class="popup-title">閫夋嫨鐢熶骇璁㈠崟鍙�</text>
+ <up-icon name="close"
+ size="20"
+ @click="showNpsNoSelector = false"></up-icon>
+ </view>
+ <view class="search-box">
+ <up-search placeholder="杈撳叆鍏抽敭瀛楁悳绱�"
+ v-model="npsNoQuery"
+ :show-action="false"
+ @change="handleNpsNoSearch"
+ @search="handleNpsNoSearch"
+ :loading="npsNoLoading"></up-search>
+ </view>
+ <scroll-view scroll-y
+ class="options-list">
+ <view v-for="item in npsNoOptions"
+ :key="item.id"
+ class="option-item"
+ @click="onSelectNpsNo(item)">
+ <view class="option-main">
+ <text class="nps-no">{{ item.npsNo }}</text>
+ <text class="product-info">{{ item.productName }} / {{ item.model }}</text>
+ </view>
+ <up-icon v-if="selectedNpsNo === item.id"
+ name="checkbox-mark"
+ color="#3c9cff"
+ size="20"></up-icon>
+ </view>
+ <view v-if="npsNoOptions.length === 0"
+ class="no-options">
+ <text>{{ npsNoLoading ? '鍔犺浇涓�...' : '鏆傛棤閫夐」' }}</text>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ <!-- 鎶ュ伐璇︽儏寮圭獥 -->
+ <up-popup :show="reportPopupVisible"
+ mode="bottom"
+ @close="reportPopupVisible = false"
+ round="10">
+ <view class="popup-content">
+ <view class="popup-header">
+ <text class="popup-title">鐢熶骇鎶ュ伐璇︽儏</text>
+ <up-icon name="close"
+ size="20"
+ @click="reportPopupVisible = false"></up-icon>
+ </view>
+ <scroll-view scroll-y
+ class="popup-scroll">
+ <view class="detail-info">
+ <view class="info-row"><text class="label">宸ュ崟鍙凤細</text><text class="value">{{ detailData.workOrder.workOrderNo }}</text></view>
+ <view class="info-row"><text class="label">璁″垝/瀹屾垚锛�</text><text class="value">{{ detailData.workOrder.planQuantity }} / {{ detailData.workOrder.completeQuantity }}</text></view>
+ <view class="info-row"><text class="label">瀹為檯鏃堕棿锛�</text><text class="value">{{ formatDate(detailData.workOrder.actualStartTime) }} 鑷� {{ formatDate(detailData.workOrder.actualEndTime) }}</text></view>
+ </view>
+ <view class="list-title">鎶ュ伐鏄庣粏</view>
+ <view v-for="(report, idx) in detailData.reports"
+ :key="idx"
+ class="detail-item">
+ <view class="item-main">
+ <view class="item-row"><text class="label">鎶ュ伐鍗曞彿锛�</text><text class="value">{{ report.productNo }}</text></view>
+ <view class="item-row"><text class="label">浜у嚭鏁伴噺锛�</text><text class="value">{{ report.quantity || 0 }}</text></view>
+ <view class="item-row"><text class="label">鎶ュ簾鏁伴噺锛�</text><text class="value error-text">{{ report.scrapQty || 0 }}</text></view>
+ <view class="item-row"><text class="label">宸ユ椂(h)锛�</text><text class="value">{{ report.workHour || 0 }}</text></view>
+ <view class="item-row"><text class="label">鍒涘缓浜猴細</text><text class="value">{{ report.userName }}</text></view>
+ <view class="item-row"><text class="label">鍒涘缓鏃堕棿锛�</text><text class="value">{{ formatDate(report.createTime, '{y}-{m}-{d} {h}:{i}') }}</text></view>
+ </view>
+ <view class="item-actions">
+ <text class="action-link"
+ @click="showParams(report.productionOperationParamList)">鍙傛暟璇︽儏</text>
+ <text class="action-link green"
+ @click="handleShowInput(report.id)">鎶曞叆璇︽儏</text>
+ </view>
+ </view>
+ <view v-if="!detailData.reports || detailData.reports.length === 0"
+ class="no-data-minor">鏆傛棤鎶ュ伐鏄庣粏</view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ <!-- 鎶曞叆璇︽儏寮圭獥 -->
+ <up-popup :show="inputPopupVisible"
+ mode="bottom"
+ @close="inputPopupVisible = false"
+ round="10">
+ <view class="popup-content">
+ <view class="popup-header">
+ <text class="popup-title">鎶曞叆淇℃伅璇︽儏</text>
+ <up-icon name="close"
+ size="20"
+ @click="inputPopupVisible = false"></up-icon>
+ </view>
+ <scroll-view scroll-y
+ class="popup-scroll">
+ <view class="input-list-popup">
+ <view v-for="(item, idx) in inputListData"
+ :key="idx"
+ class="quality-record">
+ <view class="record-title">鎶曞叆璁板綍 {{ idx + 1 }}</view>
+ <view class="info-grid">
+ <view class="info-item"><text class="label">鎶ュ伐鍗曞彿</text><text class="value">{{ item.productNo || '-' }}</text></view>
+ <view class="info-item"><text class="label">鎶曞叆鏁伴噺</text><text class="value">{{ item.quantity || 0 }} {{ item.unit || '' }}</text></view>
+ <view class="info-item full-width"><text class="label">鎶曞叆浜у搧鍚嶇О</text><text class="value">{{ item.productName || '-' }}</text></view>
+ <view class="info-item full-width"><text class="label">鎶曞叆浜у搧鍨嬪彿</text><text class="value">{{ item.model || '-' }}</text></view>
+ </view>
+ </view>
+ <view v-if="!inputListData || inputListData.length === 0"
+ class="no-data-minor">{{ inputLoading ? '鍔犺浇涓�...' : '鏆傛棤鎶曞叆璁板綍' }}</view>
+ </view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ <!-- 璐ㄦ璇︽儏寮圭獥 -->
+ <up-popup :show="qualityPopupVisible"
+ mode="bottom"
+ @close="qualityPopupVisible = false"
+ round="10">
+ <view class="popup-content">
+ <view class="popup-header">
+ <text class="popup-title">璐ㄦ璇︽儏</text>
+ <up-icon name="close"
+ size="20"
+ @click="qualityPopupVisible = false"></up-icon>
+ </view>
+ <scroll-view scroll-y
+ class="popup-scroll">
+ <view v-for="(record, idx) in qualityRecords"
+ :key="idx"
+ class="quality-record">
+ <view class="record-title">妫�娴嬭褰� {{ idx + 1 }}</view>
+ <view class="info-grid">
+ <view class="info-item"><text class="label">妫�娴嬫棩鏈�</text><text class="value">{{ formatDate(record.createTime) }}</text></view>
+ <view class="info-item"><text class="label">妫�娴嬬粨鏋�</text><up-tag style="width:100rpx"
+ :text="record.checkResult || '寰呮娴�'"
+ :type="record.checkResult === '鍚堟牸' ? 'success' : 'error'"
+ size="mini" /></view>
+ <view class="info-item"><text class="label">妫�楠屽憳</text><text class="value">{{ record.userName }}</text></view>
+ <view class="info-item"><text class="label">鏁伴噺</text><text class="value">{{ record.quantity }} {{ record.unit }}</text></view>
+ <view class="info-item"><text class="label">鎶ュ伐鍗曞彿</text><text class="value">{{ record.reportNo || '-' }}</text></view>
+ <view class="info-item"><text class="label">浜у搧鍚嶇О</text><text class="value">{{ record.productName || '-' }}</text></view>
+ <view class="info-item"><text class="label">瑙勬牸鍨嬪彿</text><text class="value">{{ record.model || '-' }}</text></view>
+ <view class="info-item"><text class="label">妫�娴嬪崟浣�</text><text class="value">{{ record.checkCompany || '-' }}</text></view>
+ </view>
+ <view class="params-table">
+ <view class="table-header">
+ <text class="col">鎸囨爣</text>
+ <text class="col">鍗曚綅</text>
+ <text class="col">鏍囧噯鍊�</text>
+ <text class="col">鍐呮帶鍊�</text>
+ <text class="col">瀹為檯鍊�</text>
+ </view>
+ <view v-for="(param, pIdx) in record.inspectParamList"
+ :key="pIdx"
+ class="table-row">
+ <text class="col">{{ param.parameterItem }}</text>
+ <text class="col">{{ param.unit || '-' }}</text>
+ <text class="col">{{ param.standardValue }}</text>
+ <text class="col">{{ param.controlValue || '-' }}</text>
+ <text class="col"
+ :class="{ 'error-text': param.testValue != param.standardValue }">{{ param.testValue }}</text>
+ </view>
+ </view>
+ </view>
+ <view v-if="!qualityRecords || qualityRecords.length === 0"
+ class="no-data-minor">鏆傛棤璐ㄦ璁板綍</view>
+ </scroll-view>
+ </view>
+ </up-popup>
+ <!-- 鍙傛暟璇︽儏寮圭獥 -->
+ <up-modal :show="paramModalVisible"
+ title="鍙傛暟璇︽儏"
+ @confirm="paramModalVisible = false">
+ <view class="modal-content">
+ <view v-for="(param, idx) in currentParams"
+ :key="idx"
+ class="param-row">
+ <text class="label">{{ param.paramName }}锛�</text>
+ <text class="value">{{ param.inputValue }} {{ param.unit && param.unit !== '/' ? param.unit : '' }}</text>
+ </view>
+ <view v-if="!currentParams || currentParams.length === 0"
+ class="no-data-minor">鏆傛棤鍙傛暟鏁版嵁</view>
+ </view>
+ </up-modal>
+ </view>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import {
+ getOrderDetail,
+ productOrderListPage,
+ } from "@/api/productionManagement/productionOrder";
+ import { productionProductInputListPage } from "@/api/productionManagement/productionProductMain";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { parseTime } from "@/utils/ruoyi";
+
+ // 閫夋嫨鍣ㄧ浉鍏�
+ const showNpsNoSelector = ref(false);
+ const npsNoQuery = ref("");
+ const npsNoOptions = ref([]);
+ const npsNoLoading = ref(false);
+ const selectedNpsNo = ref(null);
+ const selectedNpsNoLabel = ref("");
+
+ const rowData = reactive({
+ productionOrderDto: null,
+ productionRecords: [],
+ });
+
+ // 鎶ュ伐璇︽儏
+ const reportPopupVisible = ref(false);
+ const detailData = ref({ workOrder: {}, reports: [] });
+
+ // 鎶曞叆璇︽儏
+ const inputPopupVisible = ref(false);
+ const inputListData = ref([]);
+ const inputLoading = ref(false);
+
+ // 璐ㄦ璇︽儏
+ const qualityPopupVisible = ref(false);
+ const qualityRecords = ref([]);
+
+ // 鍙傛暟璇︽儏
+ const paramModalVisible = ref(false);
+ const currentParams = ref([]);
+
+ const goBack = () => {
+ uni.navigateBack();
+ };
+
+ const openNpsNoSelector = () => {
+ showNpsNoSelector.value = true;
+ if (npsNoOptions.value.length === 0) {
+ handleNpsNoSearch();
+ }
+ };
+
+ const handleNpsNoSearch = async () => {
+ npsNoLoading.value = true;
+ try {
+ const res = await productOrderListPage({
+ npsNo: npsNoQuery.value || "",
+ pageNum: 1,
+ pageSize: 50,
+ });
+ npsNoOptions.value = res.data?.records || res.rows || [];
+ } catch (error) {
+ console.error(error);
+ } finally {
+ npsNoLoading.value = false;
+ }
+ };
+
+ const onSelectNpsNo = async item => {
+ selectedNpsNo.value = item.id;
+ selectedNpsNoLabel.value = item.npsNo;
+ showNpsNoSelector.value = false;
+
+ uni.showLoading({ title: "鍔犺浇涓�..." });
+ try {
+ const res = await getOrderDetail(item.npsNo);
+ if (res.code === 200 && res.data) {
+ const { productionOrder, workOrderList } = res.data;
+ rowData.productionOrderDto = productionOrder || item;
+ rowData.productionRecords = workOrderList || [];
+ } else {
+ rowData.productionOrderDto = item;
+ rowData.productionRecords = [];
+ }
+ } catch (error) {
+ console.error(error);
+ rowData.productionOrderDto = item;
+ rowData.productionRecords = [];
+ uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "none" });
+ } finally {
+ uni.hideLoading();
+ }
+ };
+
+ onLoad(async options => {
+ if (options.npsNo) {
+ uni.showLoading({ title: "鍔犺浇涓�..." });
+ try {
+ const res = await productOrderListPage({
+ npsNo: options.npsNo,
+ pageNum: 1,
+ pageSize: 10,
+ });
+ const records = res.data?.records || res.rows || [];
+ if (records.length > 0) {
+ onSelectNpsNo(records[0]);
+ } else {
+ uni.showToast({ title: "鏈壘鍒扮浉鍏宠鍗�", icon: "none" });
+ }
+ } catch (error) {
+ console.error(error);
+ } finally {
+ uni.hideLoading();
+ }
+ }
+ });
+
+ const getStatusText = status => {
+ const statusMap = { 1: "寰呭紑濮�", 2: "杩涜涓�", 3: "宸插畬鎴�", 5: "宸茬粨鏉�" };
+ return statusMap[status] || "宸插彇娑�";
+ };
+
+ const getStatusType = status => {
+ const typeMap = { 1: "primary", 2: "warning", 3: "success", 5: "error" };
+ return typeMap[status] || "info";
+ };
+
+ const formatDate = (date, pattern = "{y}-{m}-{d}") => {
+ return parseTime(date, pattern) || "-";
+ };
+
+ const formatProgress = val => {
+ const p = parseFloat(val || 0);
+ return p >= 100 ? 100 : p;
+ };
+
+ const progressColor = percentage => {
+ if (percentage < 30) return "#f56c6c";
+ if (percentage < 70) return "#e6a23c";
+ return "#67c23a";
+ };
+
+ const handleShowReports = row => {
+ detailData.value = {
+ workOrder: row.workOrder || {},
+ reports: (row.reportList || []).map(r => ({
+ ...r.reportMain,
+ ...(r.reportOutputList ? r.reportOutputList[0] : {}),
+ id: r.reportMain.id,
+ productionOperationParamList: r.reportParamList || [],
+ })),
+ };
+ reportPopupVisible.value = true;
+ };
+
+ const handleShowInput = async reportId => {
+ inputPopupVisible.value = true;
+ inputLoading.value = true;
+ inputListData.value = [];
+ try {
+ const res = await productionProductInputListPage({
+ productMainId: reportId,
+ pageNum: 1,
+ pageSize: 100,
+ });
+ inputListData.value = res.data?.records || res.rows || [];
+ } catch (error) {
+ console.error(error);
+ uni.showToast({ title: "鑾峰彇鎶曞叆淇℃伅澶辫触", icon: "none" });
+ } finally {
+ inputLoading.value = false;
+ }
+ };
+
+ const handleShowQuality = row => {
+ const inspects = row.inspectList || [];
+ qualityRecords.value = inspects.map(i => ({
+ ...i.inspect,
+ reportNo: i.reportNo,
+ productName: row.workOrder?.productName || "-",
+ model: row.workOrder?.model || "-",
+ userName: i.reportMain?.userName || "-",
+ inspectParamList: i.inspectParamList || [],
+ }));
+ qualityPopupVisible.value = true;
+ };
+
+ const showParams = params => {
+ currentParams.value = params || [];
+ paramModalVisible.value = true;
+ };
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/procurement-common.scss";
+
+ .production-traceability {
+ min-height: 100vh;
+ background-color: #f5f7fa;
+ }
+
+ .search-section {
+ background-color: #fff;
+ padding: 20rpx 24rpx;
+ margin-bottom: 20rpx;
+ }
+
+ .search-bar {
+ display: flex;
+ align-items: center;
+ background-color: #f2f2f2;
+ border-radius: 8rpx;
+ padding: 0 20rpx;
+ height: 80rpx;
+
+ .search-input {
+ flex: 1;
+ display: flex;
+ align-items: center;
+
+ .placeholder {
+ font-size: 28rpx;
+ color: #999;
+ }
+
+ .value {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 500;
+ }
+ }
+
+ .search-button {
+ padding: 0 10rpx;
+ }
+ }
+
+ .selector-popup {
+ background: #fff;
+ padding: 30rpx;
+ max-height: 70vh;
+ display: flex;
+ flex-direction: column;
+
+ .popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+
+ .popup-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ }
+ }
+
+ .search-box {
+ margin-bottom: 20rpx;
+ }
+
+ .options-list {
+ flex: 1;
+ overflow: hidden;
+ }
+
+ .option-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24rpx 0;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .option-main {
+ flex: 1;
+ .nps-no {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+ margin-bottom: 4rpx;
+ }
+ .product-info {
+ font-size: 24rpx;
+ color: #999;
+ }
+ }
+ }
+
+ .no-options {
+ text-align: center;
+ padding: 40rpx;
+ color: #999;
+ font-size: 26rpx;
+ }
+ }
+
+ .content-container {
+ padding: 0 24rpx 40rpx;
+ }
+
+ .info-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ .card-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 24rpx;
+ padding-left: 16rpx;
+ border-left: 8rpx solid #3c9cff;
+ }
+ }
+
+ .info-grid {
+ display: flex;
+ flex-wrap: wrap;
+
+ .info-item {
+ width: 50%;
+ margin-bottom: 20rpx;
+ display: flex;
+ flex-direction: column;
+
+ &.full-width {
+ width: 100%;
+ }
+
+ .label {
+ font-size: 24rpx;
+ color: #999;
+ margin-bottom: 8rpx;
+ }
+
+ .value {
+ font-size: 28rpx;
+ color: #333;
+ word-break: break-all;
+ }
+ }
+ }
+
+ .progress-container {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+
+ up-line-progress {
+ flex: 1;
+ }
+
+ .progress-text {
+ font-size: 24rpx;
+ color: #666;
+ min-width: 60rpx;
+ }
+ }
+
+ .section-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ margin: 32rpx 0 20rpx;
+ }
+
+ .work-order-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+ padding-bottom: 16rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+
+ .work-order-no {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #3c9cff;
+ }
+
+ .progress-tag {
+ font-size: 28rpx;
+ font-weight: bold;
+ }
+ }
+
+ .card-content {
+ .content-row {
+ margin-bottom: 12rpx;
+ font-size: 26rpx;
+
+ .label {
+ color: #999;
+ }
+
+ .value {
+ color: #333;
+ }
+ }
+ }
+
+ .card-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 20rpx;
+ margin-top: 20rpx;
+ }
+ }
+
+ .base-info {
+ background: #fff;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ margin-bottom: 30rpx;
+
+ .info-row {
+ margin-bottom: 16rpx;
+ font-size: 28rpx;
+ display: flex;
+ align-items: center;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .label {
+ color: #999;
+ min-width: 180rpx;
+ }
+ .value {
+ color: #333;
+ flex: 1;
+ font-weight: 500;
+
+ &.progress-box {
+ flex: 1;
+ }
+ }
+ }
+ }
+
+ .info-grid {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 10rpx 0;
+
+ .info-item {
+ width: 50%;
+ margin-bottom: 20rpx;
+ display: flex;
+ flex-direction: column;
+
+ &.full-width {
+ width: 100%;
+ }
+
+ .label {
+ font-size: 24rpx;
+ color: #999;
+ margin-bottom: 4rpx;
+ }
+ .value {
+ font-size: 28rpx;
+ color: #333;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .popup-content {
+ background: #fff;
+ padding: 30rpx;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ border-radius: 20rpx 20rpx 0 0;
+
+ .popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30rpx;
+
+ .popup-title {
+ font-size: 34rpx;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .popup-scroll {
+ flex: 1;
+ overflow: hidden;
+ }
+ }
+
+ .detail-info {
+ background: #f8f9fa;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ margin-bottom: 30rpx;
+ flex-direction: column;
+
+ .info-row {
+ margin-bottom: 12rpx;
+ font-size: 28rpx;
+ display: flex;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .label {
+ color: #999;
+ min-width: 140rpx;
+ }
+ .value {
+ color: #333;
+ flex: 1;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .list-title {
+ font-size: 30rpx;
+ font-weight: bold;
+ margin-bottom: 20rpx;
+ color: #333;
+ display: flex;
+ align-items: center;
+
+ &::before {
+ content: "";
+ width: 6rpx;
+ height: 28rpx;
+ background: #3c9cff;
+ margin-right: 12rpx;
+ border-radius: 4rpx;
+ }
+ }
+
+ .detail-item {
+ background: #fff;
+ border: 1rpx solid #f0f0f0;
+ border-radius: 12rpx;
+ padding: 24rpx;
+ margin-bottom: 20rpx;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .item-main {
+ flex: 1;
+ .item-row {
+ font-size: 26rpx;
+ margin-bottom: 8rpx;
+ display: flex;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .label {
+ color: #999;
+ min-width: 130rpx;
+ }
+ .value {
+ color: #333;
+ flex: 1;
+ }
+ }
+ }
+
+ .item-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+ padding-left: 20rpx;
+ border-left: 1rpx solid #f0f0f0;
+
+ .action-link {
+ font-size: 26rpx;
+ color: #3c9cff;
+ white-space: nowrap;
+
+ &.green {
+ color: #52c41a;
+ }
+ }
+ }
+ }
+
+ .quality-record {
+ background: #fff;
+ border: 1rpx solid #f0f0f0;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ margin-bottom: 30rpx;
+
+ .record-title {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #3c9cff;
+ margin-bottom: 24rpx;
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+
+ .params-table {
+ margin-top: 24rpx;
+ border: 1rpx solid #f0f0f0;
+ border-radius: 12rpx;
+ overflow: hidden;
+
+ .table-header {
+ display: flex;
+ background: #f8f9fa;
+ padding: 20rpx 16rpx;
+ font-size: 26rpx;
+ font-weight: bold;
+ color: #666;
+ }
+
+ .table-row {
+ display: flex;
+ padding: 20rpx 16rpx;
+ font-size: 26rpx;
+ border-top: 1rpx solid #f0f0f0;
+ color: #333;
+
+ &:nth-child(even) {
+ background: #fafafa;
+ }
+ }
+
+ .col {
+ flex: 1;
+ text-align: center;
+ word-break: break-all;
+ }
+ }
+
+ .modal-content {
+ padding: 30rpx;
+ .param-row {
+ margin-bottom: 20rpx;
+ font-size: 28rpx;
+ display: flex;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .label {
+ color: #666;
+ min-width: 160rpx;
+ }
+ .value {
+ color: #333;
+ font-weight: 500;
+ flex: 1;
+ }
+ }
+ }
+
+ .input-list-popup {
+ .input-item {
+ background: #fff;
+ border: 1rpx solid #f0f0f0;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ margin-bottom: 20rpx;
+
+ .input-row {
+ display: flex;
+ font-size: 26rpx;
+ margin-bottom: 8rpx;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ .label {
+ color: #999;
+ min-width: 160rpx;
+ }
+ .value {
+ color: #333;
+ flex: 1;
+ }
+ }
+ }
+ }
+
+ .error-text {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+
+ .no-data-minor {
+ text-align: center;
+ padding: 60rpx 40rpx;
+ color: #999;
+ font-size: 28rpx;
+ }
+</style>
diff --git a/src/pages/qualityManagement/finalInspection/add.vue b/src/pages/qualityManagement/finalInspection/add.vue
index 9b605c7..94321dd 100644
--- a/src/pages/qualityManagement/finalInspection/add.vue
+++ b/src/pages/qualityManagement/finalInspection/add.vue
@@ -51,6 +51,7 @@
</up-form-item>
<up-form-item label="鎸囨爣閫夋嫨"
prop="testStandardId"
+ required
border-bottom>
<up-input v-model="testStandardDisplay"
placeholder="璇烽�夋嫨鎸囨爣"
@@ -442,7 +443,7 @@
{ required: true, message: "璇烽�夋嫨浜у搧鍨嬪彿", trigger: "change" },
],
testStandardId: [
- { required: false, message: "璇烽�夋嫨鎸囨爣", trigger: "change" },
+ { required: true, message: "璇烽�夋嫨鎸囨爣", trigger: "change" },
],
unit: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
@@ -602,9 +603,9 @@
// 鑾峰彇宸ュ簭鍒楄〃
const getprocessList = () => {
- list().then(res => {
- processList.value = res.data;
- });
+ // list().then(res => {
+ // processList.value = res.data;
+ // });
};
// 鑾峰彇浜у搧閫夐」
@@ -691,6 +692,10 @@
showToast("璇烽�夋嫨浜у搧");
return;
}
+ if (!form.value.testStandardId) {
+ showToast("璇烽�夋嫨鎸囨爣");
+ return;
+ }
if (!form.value.checkResult) {
showToast("璇烽�夋嫨妫�娴嬬粨鏋�");
return;
@@ -699,7 +704,7 @@
loading.value = true;
form.value.inspectType = 2;
- if (isEdit.value) {
+ if (!isEdit.value) {
tableData.value.forEach(item => {
delete item.id;
});
diff --git a/src/pages/qualityManagement/finalInspection/detail.vue b/src/pages/qualityManagement/finalInspection/detail.vue
index 0e5bba6..d274a28 100644
--- a/src/pages/qualityManagement/finalInspection/detail.vue
+++ b/src/pages/qualityManagement/finalInspection/detail.vue
@@ -15,7 +15,8 @@
</view>
<text class="header-title">{{ detailData.productName || '-' }}</text>
<view class="status-tags">
- <u-tag :type="getTagType(detailData.checkResult)"
+ <u-tag v-if="detailData.checkResult"
+ :type="getTagType(detailData.checkResult)"
size="small"
class="status-tag">
{{ detailData.checkResult || '-' }}
diff --git a/src/pages/qualityManagement/finalInspection/index.vue b/src/pages/qualityManagement/finalInspection/index.vue
index 180d08d..0e860ff 100644
--- a/src/pages/qualityManagement/finalInspection/index.vue
+++ b/src/pages/qualityManagement/finalInspection/index.vue
@@ -76,7 +76,8 @@
</view>
</view>
<view class="status-tags">
- <u-tag :type="getTagType(item.checkResult)"
+ <u-tag v-if="item.checkResult"
+ :type="getTagType(item.checkResult)"
size="mini"
class="status-tag">
{{ item.checkResult }}
@@ -117,29 +118,29 @@
</view>
<!-- 鎿嶄綔鎸夐挳 -->
<view class="action-buttons">
- <!-- <u-button type="primary"
+ <u-button type="primary"
size="small"
class="action-btn"
:disabled="item.inspectState"
@click.stop="startInspection(item)">
缂栬緫
- </u-button> -->
+ </u-button>
<u-button type="info"
size="small"
class="action-btn"
@click.stop="viewDetail(item)">
璇︽儏
</u-button>
- <!-- <u-button type="success"
+ <u-button type="success"
size="small"
class="action-btn"
:disabled="item.inspectState"
@click.stop="submitInspection(item)">
鎻愪氦
- </u-button> -->
+ </u-button>
</view>
<view class="action-buttons">
- <!-- <u-button type="info"
+ <u-button type="info"
size="small"
class="action-btn"
@click.stop="viewFileList(item)">
@@ -151,7 +152,7 @@
:disabled="item.inspectState || item.checkName !== ''"
@click.stop="assignInspector(item)">
鍒嗛厤妫�楠屽憳
- </u-button> -->
+ </u-button>
</view>
</view>
</view>
@@ -163,12 +164,12 @@
</view>
<!-- 鍒嗛〉缁勪欢 -->
<!-- 娴姩鏂板鎸夐挳 -->
- <!-- <view class="fab-button"
+ <view class="fab-button"
@click="addInspection">
<up-icon name="plus"
size="24"
color="#ffffff"></up-icon>
- </view> -->
+ </view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
<up-popup v-model:show="showDate"
mode="date"
diff --git a/src/pages/qualityManagement/materialInspection/add.vue b/src/pages/qualityManagement/materialInspection/add.vue
index 63a94ef..e04391f 100644
--- a/src/pages/qualityManagement/materialInspection/add.vue
+++ b/src/pages/qualityManagement/materialInspection/add.vue
@@ -51,6 +51,7 @@
</up-form-item>
<up-form-item label="鎸囨爣閫夋嫨"
prop="testStandardId"
+ required
border-bottom>
<up-input v-model="testStandardDisplay"
placeholder="璇烽�夋嫨鎸囨爣"
@@ -114,9 +115,10 @@
border-bottom>
<up-input v-model="form.checkTime"
placeholder="璇烽�夋嫨妫�娴嬫棩鏈�"
- readonly />
+ readonly
+ @click="showDatePicker" />
<!-- <template #right>
- <up-icon name="calendar"
+ <up-icon name="arrow-right"
@click="showDatePicker"></up-icon>
</template> -->
</up-form-item>
@@ -191,11 +193,15 @@
</up-button>
</view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
- <up-popup v-model:show="showDate"
- mode="date"
- :start-year="2020"
- :end-year="2030"
- @confirm="confirmDate" />
+ <up-popup :show="showDate"
+ mode="bottom"
+ @close="showDate = false">
+ <up-datetime-picker :show="true"
+ v-model="pickerValue"
+ @confirm="confirmDate"
+ @cancel="showDate = false"
+ mode="date" />
+ </up-popup>
<!-- 渚涘簲鍟嗛�夋嫨 -->
<up-action-sheet :show="showSupplierSheet"
:actions="supplierOptions"
@@ -337,6 +343,7 @@
const loading = ref(false);
// 鏃ユ湡閫夋嫨鍣�
const showDate = ref(false);
+ const pickerValue = ref(Date.now());
// 渚涘簲鍟嗛�夋嫨
const showSupplierSheet = ref(false);
// 浜у搧閫夋嫨
@@ -448,7 +455,7 @@
{ required: true, message: "璇烽�夋嫨浜у搧鍨嬪彿", trigger: "change" },
],
testStandardId: [
- { required: false, message: "璇烽�夋嫨鎸囨爣", trigger: "change" },
+ { required: true, message: "璇烽�夋嫨鎸囨爣", trigger: "change" },
],
unit: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
@@ -483,6 +490,7 @@
// 纭鏃ユ湡閫夋嫨
const confirmDate = e => {
form.value.checkTime = dayjs(e.value).format("YYYY-MM-DD");
+ showDate.value = false;
};
// 閫夋嫨渚涘簲鍟�
@@ -697,6 +705,10 @@
showToast("璇烽�夋嫨浜у搧");
return;
}
+ if (!form.value.testStandardId) {
+ showToast("璇烽�夋嫨鎸囨爣");
+ return;
+ }
if (!form.value.checkResult) {
showToast("璇烽�夋嫨妫�娴嬬粨鏋�");
return;
@@ -705,7 +717,7 @@
loading.value = true;
form.value.inspectType = 0;
- if (isEdit.value) {
+ if (!isEdit.value) {
tableData.value.forEach(item => {
delete item.id;
});
diff --git a/src/pages/qualityManagement/materialInspection/detail.vue b/src/pages/qualityManagement/materialInspection/detail.vue
index e45b9c0..12ff7a3 100644
--- a/src/pages/qualityManagement/materialInspection/detail.vue
+++ b/src/pages/qualityManagement/materialInspection/detail.vue
@@ -15,7 +15,8 @@
</view>
<text class="header-title">{{ detailData.productName || '-' }}</text>
<view class="status-tags">
- <u-tag :type="getTagType(detailData.checkResult)"
+ <u-tag v-if="detailData.checkResult"
+ :type="getTagType(detailData.checkResult)"
size="small"
class="status-tag">
{{ detailData.checkResult || '-' }}
diff --git a/src/pages/qualityManagement/materialInspection/index.vue b/src/pages/qualityManagement/materialInspection/index.vue
index 83ab257..eb4a140 100644
--- a/src/pages/qualityManagement/materialInspection/index.vue
+++ b/src/pages/qualityManagement/materialInspection/index.vue
@@ -76,7 +76,8 @@
</view>
</view>
<view class="status-tags">
- <u-tag :type="getTagType(item.checkResult)"
+ <u-tag v-if="item.checkResult"
+ :type="getTagType(item.checkResult)"
size="mini"
class="status-tag">
{{ item.checkResult }}
@@ -117,29 +118,29 @@
</view>
<!-- 鎿嶄綔鎸夐挳 -->
<view class="action-buttons">
- <!-- <u-button type="primary"
+ <u-button type="primary"
size="small"
class="action-btn"
:disabled="item.inspectState"
@click.stop="startInspection(item)">
缂栬緫
- </u-button> -->
+ </u-button>
<u-button type="info"
size="small"
class="action-btn"
@click.stop="viewDetail(item)">
璇︽儏
</u-button>
- <!-- <u-button type="success"
+ <u-button type="success"
size="small"
class="action-btn"
:disabled="item.inspectState"
@click.stop="submitInspection(item)">
鎻愪氦
- </u-button> -->
+ </u-button>
</view>
<view class="action-buttons">
- <!-- <u-button type="info"
+ <u-button type="info"
size="small"
class="action-btn"
@click.stop="viewFileList(item)">
@@ -151,7 +152,7 @@
:disabled="item.inspectState || item.checkName !== ''"
@click.stop="assignInspector(item)">
鍒嗛厤妫�楠屽憳
- </u-button> -->
+ </u-button>
</view>
</view>
</view>
@@ -163,12 +164,12 @@
</view>
<!-- 鍒嗛〉缁勪欢 -->
<!-- 娴姩鏂板鎸夐挳 -->
- <!-- <view class="fab-button"
+ <view class="fab-button"
@click="addInspection">
<up-icon name="plus"
size="24"
color="#ffffff"></up-icon>
- </view> -->
+ </view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
<up-popup v-model:show="showDate"
mode="date"
diff --git a/src/pages/qualityManagement/processInspection/add.vue b/src/pages/qualityManagement/processInspection/add.vue
index f146fe1..2f6a6d0 100644
--- a/src/pages/qualityManagement/processInspection/add.vue
+++ b/src/pages/qualityManagement/processInspection/add.vue
@@ -51,6 +51,7 @@
</up-form-item>
<up-form-item label="鎸囨爣閫夋嫨"
prop="testStandardId"
+ required
border-bottom>
<up-input v-model="testStandardDisplay"
placeholder="璇烽�夋嫨鎸囨爣"
@@ -184,11 +185,15 @@
</up-button>
</view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
- <up-popup v-model:show="showDate"
- mode="date"
- :start-year="2020"
- :end-year="2030"
- @confirm="confirmDate" />
+ <up-popup :show="showDate"
+ mode="bottom"
+ @close="showDate = false">
+ <up-datetime-picker :show="true"
+ v-model="pickerValue"
+ @confirm="confirmDate"
+ @cancel="showDate = false"
+ mode="date" />
+ </up-popup>
<!-- 宸ュ簭閫夋嫨 -->
<up-action-sheet :show="showprocessSheet"
:actions="processOptions"
@@ -313,8 +318,8 @@
qualityInspectParamInfo,
qualityInspectDetailByProductId,
getQualityTestStandardParamByTestStandardId,
- list,
} from "@/api/qualityManagement/materialInspection.js";
+ import { getProcessList } from "@/api/productionManagement/processManagement.js";
import { userListNoPage } from "@/api/system/user.js";
// 鏄剧ず鎻愮ず淇℃伅
@@ -442,7 +447,7 @@
{ required: true, message: "璇烽�夋嫨浜у搧鍨嬪彿", trigger: "change" },
],
testStandardId: [
- { required: false, message: "璇烽�夋嫨鎸囨爣", trigger: "change" },
+ { required: true, message: "璇烽�夋嫨鎸囨爣", trigger: "change" },
],
unit: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
@@ -602,8 +607,8 @@
// 鑾峰彇宸ュ簭鍒楄〃
const getprocessList = () => {
- list().then(res => {
- processList.value = res.data;
+ getProcessList({ size: -1, current: -1 }).then(res => {
+ processList.value = res.data?.records || res.data || [];
});
};
@@ -691,6 +696,10 @@
showToast("璇烽�夋嫨浜у搧");
return;
}
+ if (!form.value.testStandardId) {
+ showToast("璇烽�夋嫨鎸囨爣");
+ return;
+ }
if (!form.value.checkResult) {
showToast("璇烽�夋嫨妫�娴嬬粨鏋�");
return;
@@ -699,7 +708,7 @@
loading.value = true;
form.value.inspectType = 1;
- if (isEdit.value) {
+ if (!isEdit.value) {
tableData.value.forEach(item => {
delete item.id;
});
diff --git a/src/pages/qualityManagement/processInspection/detail.vue b/src/pages/qualityManagement/processInspection/detail.vue
index 721bb2a..798aeee 100644
--- a/src/pages/qualityManagement/processInspection/detail.vue
+++ b/src/pages/qualityManagement/processInspection/detail.vue
@@ -15,10 +15,11 @@
</view>
<text class="header-title">{{ detailData.productName || '-' }}</text>
<view class="status-tags">
- <u-tag :type="getTagType(detailData.checkResult)"
+ <u-tag v-if="detailData.checkResult"
+ :type="getTagType(detailData.checkResult)"
size="small"
class="status-tag">
- {{ detailData.checkResult || '-' }}
+ {{ detailData.checkResult }}
</u-tag>
<u-tag :type="getStateTagType(detailData.inspectState)"
size="small"
diff --git a/src/pages/qualityManagement/processInspection/index.vue b/src/pages/qualityManagement/processInspection/index.vue
index 14e66ef..40358ef 100644
--- a/src/pages/qualityManagement/processInspection/index.vue
+++ b/src/pages/qualityManagement/processInspection/index.vue
@@ -76,7 +76,8 @@
</view>
</view>
<view class="status-tags">
- <u-tag :type="getTagType(item.checkResult)"
+ <u-tag v-if="item.checkResult"
+ :type="getTagType(item.checkResult)"
size="mini"
class="status-tag">
{{ item.checkResult }}
@@ -117,29 +118,29 @@
</view>
<!-- 鎿嶄綔鎸夐挳 -->
<view class="action-buttons">
- <!-- <u-button type="primary"
+ <u-button type="primary"
size="small"
class="action-btn"
:disabled="item.inspectState"
@click.stop="startInspection(item)">
缂栬緫
- </u-button> -->
+ </u-button>
<u-button type="info"
size="small"
class="action-btn"
@click.stop="viewDetail(item)">
璇︽儏
</u-button>
- <!-- <u-button type="success"
+ <u-button type="success"
size="small"
class="action-btn"
:disabled="item.inspectState"
@click.stop="submitInspection(item)">
鎻愪氦
- </u-button> -->
+ </u-button>
</view>
<view class="action-buttons">
- <!-- <u-button type="info"
+ <u-button type="info"
size="small"
class="action-btn"
@click.stop="viewFileList(item)">
@@ -151,7 +152,7 @@
:disabled="item.inspectState || item.checkName !== ''"
@click.stop="assignInspector(item)">
鍒嗛厤妫�楠屽憳
- </u-button> -->
+ </u-button>
</view>
</view>
</view>
@@ -163,12 +164,12 @@
</view>
<!-- 鍒嗛〉缁勪欢 -->
<!-- 娴姩鏂板鎸夐挳 -->
- <!-- <view class="fab-button"
+ <view class="fab-button"
@click="addInspection">
<up-icon name="plus"
size="24"
color="#ffffff"></up-icon>
- </view> -->
+ </view>
<!-- 鏃ユ湡閫夋嫨鍣� -->
<up-popup v-model:show="showDate"
mode="date"
diff --git a/src/pages/sales/salesAccount/goOut.vue b/src/pages/sales/salesAccount/goOut.vue
index 9980e5f..42321c9 100644
--- a/src/pages/sales/salesAccount/goOut.vue
+++ b/src/pages/sales/salesAccount/goOut.vue
@@ -1,657 +1,451 @@
<template>
- <view class="account-detail">
+ <view class="shipment-page">
<PageHeader title="鍙戣揣"
@back="goBack" />
- <!-- 琛ㄥ崟鍖哄煙 -->
- <u-form ref="formRef"
- @submit="submitForm"
- :rules="rules"
- :model="form"
- label-width="140rpx">
- <u-form-item prop="typeValue"
- label="鍙戣揣绫诲瀷"
- required>
- <u-input v-model="typeValue"
- readonly
- placeholder="璇烽�夋嫨鍙戣揣鏂瑰紡"
- @click="showPicker = true" />
- <template #right>
- <up-icon name="arrow-right"
- @click="showPicker = true"></up-icon>
- </template>
- </u-form-item>
- </u-form>
- <!-- 閫夋嫨鍣ㄥ脊绐� -->
- <up-action-sheet :show="showPicker"
- :actions="productOptions"
- title="鍙戣揣鏂瑰紡"
- @select="onConfirm"
- @close="showPicker = false" />
- <!-- 瀹℃牳娴佺▼鍖哄煙 -->
- <view class="approval-process">
- <view class="approval-header">
- <text class="approval-title">瀹℃牳娴佺▼</text>
- <text class="approval-desc">姣忎釜姝ラ鍙兘閫夋嫨涓�涓鎵逛汉</text>
- </view>
- <view class="approval-steps">
- <view v-for="(step, stepIndex) in approverNodes"
- :key="stepIndex"
- class="approval-step">
- <view class="step-dot"></view>
- <view class="step-title">
- <text>瀹℃壒浜�</text>
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100"
+ input-align="right"
+ error-message-align="right">
+ <!-- 鍩烘湰淇℃伅 -->
+ <u-cell-group title="鍩烘湰淇℃伅"
+ class="form-section">
+ <up-form-item label="鍙戣揣鏂瑰紡"
+ prop="type"
+ required>
+ <up-input v-model="form.type"
+ readonly
+ placeholder="璇烽�夋嫨鍙戣揣鏂瑰紡"
+ @click="showTypePicker = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showTypePicker = true"></up-icon>
+ </template>
+ </up-form-item>
+ <block v-if="form.type === '璐ц溅'">
+ <up-form-item label="鍙戣揣杞︾墝"
+ prop="shippingCarNumber"
+ required>
+ <up-input v-model="form.shippingCarNumber"
+ placeholder="璇疯緭鍏ュ彂璐ц溅鐗屽彿"
+ clearable />
+ </up-form-item>
+ </block>
+ <block v-if="form.type === '蹇��'">
+ <up-form-item label="蹇�掑叕鍙�"
+ prop="expressCompany"
+ required>
+ <up-input v-model="form.expressCompany"
+ placeholder="璇疯緭鍏ュ揩閫掑叕鍙�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="蹇�掑崟鍙�"
+ prop="expressNumber"
+ required>
+ <up-input v-model="form.expressNumber"
+ placeholder="璇疯緭鍏ュ揩閫掑崟鍙�"
+ clearable />
+ </up-form-item>
+ </block>
+ </u-cell-group>
+ <!-- 鎵规閫夋嫨 -->
+ <u-cell-group title="鎵规閫夋嫨"
+ class="form-section">
+ <view class="section-header-info">
+ <text class="subtitle">寰呭彂璐ф暟閲�: {{ goOutData.noQuantity || 0 }}</text>
</view>
- <view class="approver-container">
- <view v-if="step.nickName"
- class="approver-item">
- <view class="approver-avatar">
- <text class="avatar-text">{{ step.nickName.charAt(0) }}</text>
- <view class="status-dot"></view>
+ <view v-if="batchList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤鍙敤搴撳瓨鎵规</text>
+ </view>
+ <view v-else
+ class="batch-list">
+ <view v-for="(item, index) in batchList"
+ :key="index"
+ class="batch-card">
+ <view class="batch-header">
+ <text class="batch-no">鎵瑰彿: {{ item.batchNo }}</text>
+ <text class="batch-qty">搴撳瓨: {{ getAvailableQty(item) }}</text>
</view>
- <view class="approver-info">
- <text class="approver-name">{{ step.nickName }}</text>
+ <up-divider></up-divider>
+ <view class="batch-body">
+ <up-form-item label="鍙戣揣鏁伴噺">
+ <up-input v-model="item.deliveryQuantity"
+ type="digit"
+ placeholder="0.00"
+ input-align="right"
+ @blur="onBatchQtyChange(item)" />
+ </up-form-item>
</view>
- <view class="delete-approver-btn"
- @click="removeApprover(stepIndex)">脳</view>
- </view>
- <view v-else
- class="add-approver-btn"
- @click="addApprover(stepIndex)">
- <view class="add-circle">+</view>
- <text class="add-label">閫夋嫨瀹℃壒浜�</text>
</view>
</view>
- <view class="step-line"
- v-if="stepIndex < approverNodes.length - 1"></view>
- <view class="delete-step-btn"
- v-if="approverNodes.length > 1"
- @click="removeApprovalStep(stepIndex)">鍒犻櫎鑺傜偣</view>
- </view>
- </view>
- <view class="add-step-btn">
- <u-button icon="plus"
- plain
- type="primary"
- style="width: 100%"
- @click="addApprovalStep">鏂板鑺傜偣</u-button>
- </view>
+ </u-cell-group>
+ <!-- 鍙戣揣鍥剧墖 -->
+ <u-cell-group title="鍙戣揣鍥剧墖"
+ class="form-section">
+ <view class="upload-container">
+ <up-upload :fileList="fileList"
+ @afterRead="afterRead"
+ @delete="deleteFile"
+ multiple
+ :maxCount="9"
+ width="160rpx"
+ height="160rpx" />
+ </view>
+ </u-cell-group>
+ </up-form>
</view>
<!-- 搴曢儴鎸夐挳 -->
- <view class="footer-btns">
- <u-button class="cancel-btn"
- @click="goBack">鍙栨秷</u-button>
- <u-button class="save-btn"
- @click="submitForm">鍙戣揣</u-button>
- </view>
+ <FooterButtons confirmText="纭鍙戣揣"
+ @cancel="goBack"
+ @confirm="submitForm" />
+ <!-- 鍙戣揣鏂瑰紡閫夋嫨鍣� -->
+ <up-action-sheet :show="showTypePicker"
+ :actions="typeActions"
+ title="鍙戣揣鏂瑰紡"
+ @select="onTypeSelect"
+ @close="showTypePicker = false" />
</view>
</template>
<script setup>
- import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
+ import config from "@/config";
+ import { ref, onMounted, reactive } from "vue";
import PageHeader from "@/components/PageHeader.vue";
+ import FooterButtons from "@/components/FooterButtons.vue";
import { addShippingInfo } from "@/api/salesManagement/salesLedger";
- const showToast = message => {
- uni.showToast({
- title: message,
- icon: "none",
- });
- };
- import { userListNoPageByTenantId } from "@/api/system/user";
+ import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory";
+ import { getToken } from "@/utils/auth";
- const data = reactive({
- form: {
- approveTime: "",
- approveId: "",
- approveUser: "",
- approveUserName: "",
- approveDeptName: "",
- approveDeptId: "",
- approveReason: "",
- checkResult: "",
- tempFileIds: [],
- approverList: [], // 鏂板瀛楁锛屽瓨鍌ㄦ墍鏈夎妭鐐圭殑瀹℃壒浜篿d
- startDate: "",
- endDate: "",
- location: "",
- price: "",
- },
- rules: {
- typeValue: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
- },
- });
- const { form, rules } = toRefs(data);
- const showPicker = ref(false);
- const productOptions = ref([
- {
- value: "璐ц溅",
- name: "璐ц溅",
- },
- {
- value: "蹇��",
- name: "蹇��",
- },
- ]);
- const operationType = ref("");
- const currentApproveStatus = ref("");
- const approverNodes = ref([]);
- const userList = ref([]);
- const formRef = ref(null);
- const approveType = ref(0);
const goOutData = ref({});
+ const batchList = ref([]);
+ const fileList = ref([]);
+ const showTypePicker = ref(false);
+ const typeActions = [
+ { name: "璐ц溅", value: "璐ц溅" },
+ { name: "蹇��", value: "蹇��" },
+ ];
+
+ const form = reactive({
+ type: "璐ц溅",
+ shippingCarNumber: "",
+ expressCompany: "",
+ expressNumber: "",
+ });
+
+ const rules = {
+ type: [{ required: true, message: "璇烽�夋嫨鍙戣揣鏂瑰紡", trigger: "change" }],
+ shippingCarNumber: [
+ {
+ required: true,
+ validator: (rule, value, callback) => {
+ if (form.type === "璐ц溅" && !value) {
+ return false;
+ }
+ return true;
+ },
+ message: "璇疯緭鍏ヨ溅鐗屽彿",
+ trigger: "blur",
+ },
+ ],
+ expressCompany: [
+ {
+ required: true,
+ validator: (rule, value, callback) => {
+ if (form.type === "蹇��" && !value) {
+ return false;
+ }
+ return true;
+ },
+ message: "璇疯緭鍏ュ揩閫掑叕鍙�",
+ trigger: "blur",
+ },
+ ],
+ expressNumber: [
+ {
+ required: true,
+ validator: (rule, value, callback) => {
+ if (form.type === "蹇��" && !value) {
+ return false;
+ }
+ return true;
+ },
+ message: "璇疯緭鍏ュ揩閫掑崟鍙�",
+ trigger: "blur",
+ },
+ ],
+ };
+
+ const formRef = ref(null);
+
onMounted(async () => {
- try {
- userListNoPageByTenantId().then(res => {
- userList.value = res.data;
- });
- // 浠庢湰鍦板瓨鍌ㄨ幏鍙栧彂璐ц鎯�
- goOutData.value = JSON.parse(uni.getStorageSync("goOutData"));
- console.log(goOutData.value, "goOutData.value");
-
- // 鍒濆鍖栧鎵规祦绋嬭妭鐐癸紝榛樿涓�涓妭鐐�
- approverNodes.value = [{ id: 1, userId: null }];
-
- // 鐩戝惉鑱旂郴浜洪�夋嫨浜嬩欢
- uni.$on("selectContact", handleSelectContact);
- } catch (error) {
- console.error("鑾峰彇澶辫触:", error);
+ const storedData = uni.getStorageSync("goOutData");
+ goOutData.value = JSON.parse(storedData || "{}");
+ if (goOutData.value.productModelId) {
+ loadBatches(goOutData.value.productModelId);
}
});
- onUnmounted(() => {
- // 绉婚櫎浜嬩欢鐩戝惉
- uni.$off("selectContact", handleSelectContact);
- });
- const typeValue = ref("璐ц溅");
- const onConfirm = item => {
- // 璁剧疆閫変腑鐨勯儴闂�
- typeValue.value = item.name;
- showPicker.value = false;
+ const loadBatches = async modelId => {
+ if (!modelId) return;
+ try {
+ const res = await getStockInventoryByModelId(modelId);
+ const rawList = Array.isArray(res?.data)
+ ? res.data
+ : res?.data?.records || res?.data?.rows || res || [];
+ const seenIds = new Set();
+ batchList.value = rawList
+ .filter(item => {
+ if (!item?.id || !item?.batchNo || seenIds.has(item.id)) {
+ return false;
+ }
+ seenIds.add(item.id);
+ return true;
+ })
+ .map(item => ({
+ ...item,
+ deliveryQuantity: "",
+ }));
+ } catch (e) {
+ console.error("鍔犺浇鎵规澶辫触", e);
+ }
};
- const goBack = () => {
- // 娓呴櫎鏈湴瀛樺偍鐨勬暟鎹�
- uni.removeStorageSync("operationType");
- uni.removeStorageSync("invoiceLedgerEditRow");
- uni.removeStorageSync("approveType");
- uni.navigateBack();
+ const getAvailableQty = item => {
+ const quantity =
+ item?.qualitity ??
+ item?.quantity ??
+ item?.unLockedQuantity ??
+ item?.qualifiedUnLockedQuantity ??
+ item?.qualifiedQuantity ??
+ item?.stockQuantity;
+ return quantity ?? 0;
};
- const submitForm = () => {
- // 妫�鏌ユ瘡涓鎵规楠ゆ槸鍚﹂兘鏈夊鎵逛汉
- const hasEmptyStep = approverNodes.value.some(step => !step.nickName);
- if (hasEmptyStep) {
- showToast("璇蜂负姣忎釜瀹℃壒姝ラ閫夋嫨瀹℃壒浜�");
+ const onBatchQtyChange = item => {
+ const val = parseFloat(item.deliveryQuantity);
+ if (isNaN(val) || val <= 0) {
+ item.deliveryQuantity = "";
return;
}
- formRef.value
- .validate()
- .then(valid => {
- if (valid) {
- // 琛ㄥ崟鏍¢獙閫氳繃锛屽彲浠ユ彁浜ゆ暟鎹�
- // 鏀堕泦鎵�鏈夎妭鐐圭殑瀹℃壒浜篿d
- console.log("approverNodes---", approverNodes.value);
- const approveUserIds = approverNodes.value
- .map(node => node.userId)
- .join(",");
- const params = {
- salesLedgerId: goOutData.value.salesLedgerId,
- salesLedgerProductId: goOutData.value.id,
- type: typeValue.value,
- approveUserIds,
- };
- console.log(params, "params");
- addShippingInfo(params).then(res => {
- showToast("鍙戣揣鎴愬姛");
- setTimeout(() => {
- goBack();
- }, 500);
- });
- }
- })
- .catch(error => {
- console.error("琛ㄥ崟鏍¢獙澶辫触:", error);
- // 灏濊瘯鑾峰彇鍏蜂綋鐨勯敊璇瓧娈�
- if (error && error.errors) {
- const firstError = error.errors[0];
- if (firstError) {
- uni.showToast({
- title: firstError.message || "琛ㄥ崟鏍¢獙澶辫触锛岃妫�鏌ュ繀濉」",
- icon: "none",
- });
- return;
+ const available = getAvailableQty(item);
+ if (val > available) {
+ uni.showToast({ title: "涓嶈兘瓒呰繃搴撳瓨鏁伴噺", icon: "none" });
+ item.deliveryQuantity = available.toString();
+ }
+
+ const totalToShip = Number(goOutData.value.noQuantity || 0);
+ const otherBatchesTotal = batchList.value.reduce((sum, b) => {
+ if (b.id === item.id) return sum;
+ return sum + Number(b.deliveryQuantity || 0);
+ }, 0);
+
+ if (val + otherBatchesTotal > totalToShip) {
+ uni.showToast({ title: "鎬绘暟涓嶈兘瓒呰繃寰呭彂璐ф暟閲�", icon: "none" });
+ item.deliveryQuantity = (totalToShip - otherBatchesTotal).toString();
+ }
+ };
+
+ const onTypeSelect = item => {
+ form.type = item.name;
+ showTypePicker.value = false;
+ };
+
+ const afterRead = async event => {
+ const { file } = event;
+ const lists = [].concat(file);
+ const token = getToken();
+
+ for (let i = 0; i < lists.length; i++) {
+ const item = lists[i];
+ const uploadIndex = fileList.value.length;
+ fileList.value.push({
+ ...item,
+ status: "uploading",
+ message: "涓婁紶涓�",
+ });
+
+ uni.uploadFile({
+ url: config.baseUrl + "/common/upload",
+ filePath: item.url,
+ name: "files",
+ header: {
+ Authorization: "Bearer " + token,
+ },
+ success: res => {
+ try {
+ const data = JSON.parse(res.data);
+ if (data.code === 200) {
+ const fileData = Array.isArray(data.data)
+ ? data.data[0]
+ : data.data || data;
+ fileList.value[uploadIndex].status = "success";
+ fileList.value[uploadIndex].message = "";
+ fileList.value[uploadIndex].url = fileData.url;
+ fileList.value[uploadIndex].storageBlobDTO = fileData;
+ } else {
+ fileList.value[uploadIndex].status = "failed";
+ fileList.value[uploadIndex].message = data.msg || "涓婁紶澶辫触";
+ }
+ } catch (e) {
+ fileList.value[uploadIndex].status = "failed";
+ fileList.value[uploadIndex].message = "瑙f瀽澶辫触";
}
- }
- // 鏄剧ず閫氱敤閿欒淇℃伅
- uni.showToast({
- title: "琛ㄥ崟鏍¢獙澶辫触锛岃妫�鏌ュ繀濉」",
- icon: "none",
- });
+ },
+ fail: () => {
+ fileList.value[uploadIndex].status = "failed";
+ fileList.value[uploadIndex].message = "缃戠粶寮傚父";
+ },
});
+ }
};
- // 澶勭悊鑱旂郴浜洪�夋嫨缁撴灉
- const handleSelectContact = data => {
- const { stepIndex, contact } = data;
- // 灏嗛�変腑鐨勮仈绯讳汉璁剧疆涓哄搴斿鎵规楠ょ殑瀹℃壒浜�
- approverNodes.value[stepIndex].userId = contact.userId;
- approverNodes.value[stepIndex].nickName = contact.nickName;
+ const deleteFile = event => {
+ fileList.value.splice(event.index, 1);
};
- const addApprover = stepIndex => {
- // 璺宠浆鍒拌仈绯讳汉閫夋嫨椤甸潰
- uni.setStorageSync("stepIndex", stepIndex);
- uni.navigateTo({
- url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
- });
- };
+ const goBack = () => uni.navigateBack();
- const addApprovalStep = () => {
- // 娣诲姞鏂扮殑瀹℃壒姝ラ
- approverNodes.value.push({ userId: null, nickName: null });
- };
+ const submitForm = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
- const removeApprover = stepIndex => {
- // 绉婚櫎瀹℃壒浜�
- approverNodes.value[stepIndex].userId = null;
- approverNodes.value[stepIndex].nickName = null;
- };
+ const selectedBatches = batchList.value.filter(
+ b => parseFloat(b.deliveryQuantity) > 0
+ );
+ if (selectedBatches.length === 0) {
+ uni.showToast({ title: "璇疯嚦灏戝~鍐欎竴涓壒娆$殑鍙戣揣鏁伴噺", icon: "none" });
+ return;
+ }
- const removeApprovalStep = stepIndex => {
- // 纭繚鑷冲皯淇濈暀涓�涓鎵规楠�
- if (approverNodes.value.length > 1) {
- approverNodes.value.splice(stepIndex, 1);
- } else {
- uni.showToast({
- title: "鑷冲皯闇�瑕佷竴涓鎵规楠�",
- icon: "none",
- });
+ // Check if any file is still uploading
+ if (fileList.value.some(f => f.status === "uploading")) {
+ uni.showToast({ title: "璇风瓑寰呭浘鐗囦笂浼犲畬鎴�", icon: "none" });
+ return;
+ }
+
+ const payload = {
+ salesLedgerId: goOutData.value.salesLedgerId,
+ salesLedgerProductId: goOutData.value.id,
+ type: form.type,
+ shippingCarNumber: form.type === "璐ц溅" ? form.shippingCarNumber : "",
+ expressCompany: form.type === "蹇��" ? form.expressCompany : "",
+ expressNumber: form.type === "蹇��" ? form.expressNumber : "",
+ storageBlobDTOs: fileList.value
+ .filter(f => f.status === "success")
+ .map(f => f.storageBlobDTO),
+ batchNo: selectedBatches.map(b => b.id),
+ batchNoDetailList: selectedBatches.map(b => ({
+ stockInventoryId: b.id,
+ batchNo: b.batchNo,
+ quantity: parseFloat(b.deliveryQuantity),
+ productModelId: goOutData.value.productModelId,
+ })),
+ };
+
+ try {
+ uni.showLoading({ title: "鎻愪氦涓�..." });
+ const res = await addShippingInfo(payload);
+ uni.hideLoading();
+ uni.showToast({ title: "鍙戣揣鎴愬姛" });
+ setTimeout(() => goBack(), 500);
+ } catch (e) {
+ uni.hideLoading();
+ uni.showToast({ title: "鍙戣揣澶辫触", icon: "none" });
}
};
</script>
<style scoped lang="scss">
@import "@/static/scss/form-common.scss";
-
- .approval-process {
- background: #fff;
- margin: 16px;
- border-radius: 16px;
- padding: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
+ .shipment-page {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
}
- .approval-header {
- margin-bottom: 16px;
+ .form-container {
+ padding: 12px 12px 0;
}
- .approval-title {
- font-size: 16px;
- font-weight: 600;
- color: #333;
- display: block;
- margin-bottom: 4px;
- }
-
- .approval-desc {
- font-size: 12px;
- color: #999;
- }
-
- /* 鏍峰紡澧炲己涓衡�滅畝娲佸皬鍦嗗湀椋庢牸鈥� */
- .approval-steps {
- padding-left: 22px;
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- left: 11px;
- top: 40px;
- bottom: 40px;
- width: 2px;
- background: linear-gradient(
- to bottom,
- #e6f7ff 0%,
- #bae7ff 50%,
- #91d5ff 100%
- );
- border-radius: 1px;
- }
- }
-
- .approval-step {
- position: relative;
- margin-bottom: 24px;
-
- &::before {
- content: "";
- position: absolute;
- left: -18px;
- top: 14px; // 浠� 8px 璋冩暣涓� 14px锛屼笌鏂囧瓧涓績瀵归綈
- width: 12px;
- height: 12px;
- background: #fff;
- border: 3px solid #006cfb;
- border-radius: 50%;
- z-index: 2;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- }
- }
-
- .step-title {
- top: 12px;
+ .form-section {
margin-bottom: 12px;
- position: relative;
- margin-left: 6px;
- }
-
- .step-title text {
- font-size: 14px;
- color: #666;
- background: #f0f0f0;
- padding: 4px 12px;
border-radius: 12px;
- position: relative;
- line-height: 1.4; // 纭繚鏂囧瓧琛岄珮涓�鑷�
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
}
- .approver-item {
+ .section-header-info {
+ padding: 10px 18px;
+ background: #f8fbff;
display: flex;
- align-items: center;
- background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
- border-radius: 16px;
- padding: 16px;
+ justify-content: flex-end;
+
+ .subtitle {
+ font-size: 13px;
+ color: #7a8599;
+ }
+ }
+
+ .batch-list {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
gap: 12px;
- position: relative;
- border: 1px solid #e6f7ff;
- box-shadow: 0 4px 12px rgba(0, 108, 251, 0.08);
- transition: all 0.3s ease;
}
- .approver-avatar {
- width: 48px;
- height: 48px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
- }
-
- .avatar-text {
- color: #fff;
- font-size: 18px;
- font-weight: 600;
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
- }
-
- .approver-info {
- flex: 1;
- position: relative;
- }
-
- .approver-name {
- display: block;
- font-size: 16px;
- color: #333;
- font-weight: 500;
- position: relative;
- }
-
- .approver-dept {
- font-size: 12px;
- color: #999;
- background: rgba(0, 108, 251, 0.05);
- padding: 2px 8px;
- border-radius: 8px;
- display: inline-block;
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- left: 4px;
- top: 50%;
- transform: translateY(-50%);
- width: 2px;
- height: 2px;
- background: #006cfb;
- border-radius: 50%;
- }
- }
-
- .delete-approver-btn {
- font-size: 16px;
- color: #ff4d4f;
- background: linear-gradient(
- 135deg,
- rgba(255, 77, 79, 0.1) 0%,
- rgba(255, 77, 79, 0.05) 100%
- );
- width: 28px;
- height: 28px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- position: relative;
- }
-
- .add-approver-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(135deg, #f0f8ff 0%, #e6f7ff 100%);
- border: 2px dashed #006cfb;
- border-radius: 16px;
- padding: 20px;
- color: #006cfb;
- font-size: 14px;
- position: relative;
- transition: all 0.3s ease;
-
- &::before {
- content: "";
- position: absolute;
- left: 50%;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 32px;
- height: 32px;
- border: 2px solid #006cfb;
- border-radius: 50%;
- opacity: 0;
- transition: all 0.3s ease;
- }
- }
-
- .delete-step-btn {
- color: #ff4d4f;
- font-size: 12px;
- background: linear-gradient(
- 135deg,
- rgba(255, 77, 79, 0.1) 0%,
- rgba(255, 77, 79, 0.05) 100%
- );
- padding: 6px 12px;
- border-radius: 12px;
- display: inline-block;
- position: relative;
- transition: all 0.3s ease;
-
- &::before {
- content: "";
- position: absolute;
- left: 6px;
- top: 50%;
- transform: translateY(-50%);
- width: 4px;
- height: 4px;
- background: #ff4d4f;
- border-radius: 50%;
- }
- }
-
- .step-line {
- display: none; // 闅愯棌鍘熸潵鐨勭嚎鏉★紝浣跨敤浼厓绱犱唬鏇�
- }
-
- .add-step-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .footer-btns {
- position: fixed;
- left: 0;
- right: 0;
- bottom: 0;
+ .batch-card {
background: #fff;
+ border-radius: 12px;
+ padding: 0 12px 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ border: 1px solid #f0f3f7;
+ }
+
+ .batch-header {
+ padding: 12px 0;
display: flex;
- justify-content: space-around;
+ justify-content: space-between;
align-items: center;
- padding: 0.75rem 0;
- box-shadow: 0 -0.125rem 0.5rem rgba(0, 0, 0, 0.05);
- z-index: 1000;
- }
- .cancel-btn {
- font-weight: 400;
- font-size: 1rem;
- color: #ffffff;
- width: 6.375rem;
- background: #c7c9cc;
- box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
- border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
- }
-
- .save-btn {
- font-weight: 400;
- font-size: 1rem;
- color: #ffffff;
- width: 14rem;
- background: linear-gradient(140deg, #00baff 0%, #006cfb 100%);
- box-shadow: 0 0.25rem 0.625rem 0 rgba(3, 88, 185, 0.2);
- border-radius: 2.5rem 2.5rem 2.5rem 2.5rem;
- }
-
- // 鍔ㄧ敾瀹氫箟
- @keyframes pulse {
- 0% {
- transform: scale(1);
- opacity: 1;
+ .batch-no {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
}
- 50% {
- transform: scale(1.2);
- opacity: 0.7;
- }
- 100% {
- transform: scale(1);
- opacity: 1;
+
+ .batch-qty {
+ font-size: 13px;
+ color: #2979ff;
+ background: #eef6ff;
+ padding: 2px 8px;
+ border-radius: 4px;
}
}
- @keyframes rotate {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(360deg);
- }
- }
-
- @keyframes ripple {
- 0% {
- transform: translate(-50%, -50%) scale(0.8);
- opacity: 1;
- }
- 100% {
- transform: translate(-50%, -50%) scale(1.6);
- opacity: 0;
- }
- }
-
- /* 濡傛灉宸叉湁 .step-line锛岃繖閲屾洿绮惧噯瀹氫綅鍒板乏渚т笌灏忓渾鐐瑰榻� */
- .step-line {
- position: absolute;
- left: 4px;
- top: 48px;
- width: 2px;
- height: calc(100% - 48px);
- background: #e5e7eb;
- }
-
- .approver-container {
- display: flex;
- align-items: center;
- background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
- border-radius: 16px;
- gap: 12px;
- padding: 10px 0;
- background: transparent;
- border: none;
- box-shadow: none;
- }
-
- .approver-item {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 8px 10px;
- background: transparent;
- border: none;
- box-shadow: none;
- border-radius: 0;
- }
-
- .approver-avatar {
- position: relative;
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: #f3f4f6;
- border: 2px solid #e5e7eb;
- display: flex;
- align-items: center;
- justify-content: center;
- animation: none; /* 绂佺敤鏃嬭浆绛夊姩鐢伙紝鍥炲綊绠�娲� */
- }
-
- .avatar-text {
- font-size: 14px;
- color: #374151;
- font-weight: 600;
- }
-
- .add-approver-btn {
- display: flex;
- align-items: center;
- gap: 8px;
- background: transparent;
- border: none;
- box-shadow: none;
- padding: 0;
- }
-
- .add-approver-btn .add-circle {
- width: 40px;
- height: 40px;
- border: 2px dashed #a0aec0;
- border-radius: 50%;
- color: #6b7280;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 22px;
- line-height: 1;
- }
-
- .add-approver-btn .add-label {
- color: #3b82f6;
+ .empty-text {
+ padding: 30px 12px;
+ text-align: center;
+ color: #999;
font-size: 14px;
}
-</style>
\ No newline at end of file
+
+ .upload-container {
+ padding: 12px 18px;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+
+ :deep(.u-form-item) {
+ padding: 0 18px !important;
+ }
+</style>
diff --git a/src/pages/sales/salesQuotation/detail.vue b/src/pages/sales/salesQuotation/detail.vue
index a273974..45c3fd6 100644
--- a/src/pages/sales/salesQuotation/detail.vue
+++ b/src/pages/sales/salesQuotation/detail.vue
@@ -1,7 +1,7 @@
<template>
<view class="customer-detail-page">
- <PageHeader title="鎶ヤ环璇︽儏" @back="goBack" />
-
+ <PageHeader title="鎶ヤ环璇︽儏"
+ @back="goBack" />
<view class="detail-content">
<view class="section">
<view class="section-title">鍩虹淇℃伅</view>
@@ -44,24 +44,13 @@
</view>
</view>
</view>
-
- <view class="section">
- <view class="section-title">瀹℃壒鑺傜偣</view>
- <view v-if="approverNames.length" class="info-list">
- <view v-for="(name, index) in approverNames" :key="index" class="info-item">
- <text class="info-label">瀹℃壒鑺傜偣 {{ index + 1 }}</text>
- <text class="info-value">{{ name }}</text>
- </view>
- </view>
- <view v-else class="empty-box">
- <text>鏆傛棤瀹℃壒鑺傜偣</text>
- </view>
- </view>
-
<view class="section">
<view class="section-title">浜у搧鏄庣粏</view>
- <view v-if="detailData.products && detailData.products.length > 0" class="product-list">
- <view v-for="(item, index) in detailData.products" :key="index" class="product-card">
+ <view v-if="detailData.products && detailData.products.length > 0"
+ class="product-list">
+ <view v-for="(item, index) in detailData.products"
+ :key="index"
+ class="product-card">
<view class="product-head">浜у搧 {{ index + 1 }}</view>
<view class="info-item">
<text class="info-label">浜у搧鍚嶇О</text>
@@ -89,13 +78,16 @@
</view>
</view>
</view>
- <view v-else class="empty-box">
+ <view v-else
+ class="empty-box">
<text>鏆傛棤浜у搧鏄庣粏</text>
</view>
</view>
</view>
-
- <FooterButtons cancelText="杩斿洖" confirmText="缂栬緫" @cancel="goBack" @confirm="goEdit" />
+ <FooterButtons cancelText="杩斿洖"
+ confirmText="缂栬緫"
+ @cancel="goBack"
+ @confirm="goEdit" />
</view>
</template>
@@ -108,29 +100,27 @@
const quotationId = ref("");
const detailData = ref({});
- const approverNames = computed(() => {
- const approverText = detailData.value.approveUserNames || detailData.value.approverNames || detailData.value.approveUserIds || "";
- if (Array.isArray(approverText)) return approverText.filter(Boolean);
- return String(approverText)
- .split(",")
- .map(item => item.trim())
- .filter(Boolean);
- });
-
const goBack = () => {
uni.navigateBack();
};
const goEdit = () => {
if (!quotationId.value) return;
- uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}` });
+ uni.navigateTo({
+ url: `/pages/sales/salesQuotation/edit?id=${quotationId.value}`,
+ });
};
const formatAmount = amount => `楼${Number(amount || 0).toFixed(2)}`;
const loadDetailFromStorage = () => {
const cachedData = uni.getStorageSync("salesQuotationDetail");
- detailData.value = cachedData || {};
+ if (cachedData && (cachedData.id === quotationId.value || cachedData.id === Number(quotationId.value))) {
+ detailData.value = cachedData;
+ } else {
+ detailData.value = cachedData || {};
+ console.warn("鏈壘鍒板搴旂殑鎶ヤ环鍗曠紦瀛樻暟鎹�");
+ }
};
onLoad(options => {
diff --git a/src/pages/sales/salesQuotation/edit.vue b/src/pages/sales/salesQuotation/edit.vue
index 940a8d9..bfcb930 100644
--- a/src/pages/sales/salesQuotation/edit.vue
+++ b/src/pages/sales/salesQuotation/edit.vue
@@ -1,173 +1,196 @@
<template>
<view class="account-detail">
- <PageHeader :title="pageTitle" @back="goBack" />
-
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
<view class="form-container">
- <up-form
- ref="formRef"
- :model="form"
- :rules="rules"
- label-width="110"
- input-align="right"
- error-message-align="right"
- >
- <u-cell-group title="鍩虹淇℃伅" class="form-section">
- <up-form-item label="瀹㈡埛鍚嶇О" prop="customer" required>
- <up-input v-model="form.customer" placeholder="璇烽�夋嫨瀹㈡埛" readonly @click="showCustomerSheet = true" />
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="110"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="鍩虹淇℃伅"
+ class="form-section">
+ <up-form-item label="瀹㈡埛鍚嶇О"
+ prop="customer"
+ required>
+ <up-input v-model="form.customer"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ readonly
+ @click="showCustomerSheet = true" />
<template #right>
- <up-icon name="arrow-right" @click="showCustomerSheet = true"></up-icon>
+ <up-icon name="arrow-right"
+ @click="showCustomerSheet = true"></up-icon>
</template>
</up-form-item>
- <up-form-item label="涓氬姟鍛�" prop="salesperson" required>
- <up-input
- v-model="form.salesperson"
- placeholder="璇烽�夋嫨涓氬姟鍛�"
- readonly
- @click="showSalespersonSheet = true"
- />
+ <up-form-item label="涓氬姟鍛�"
+ prop="salesperson"
+ required>
+ <up-input v-model="form.salesperson"
+ placeholder="璇烽�夋嫨涓氬姟鍛�"
+ readonly
+ @click="showSalespersonSheet = true" />
<template #right>
- <up-icon name="arrow-right" @click="showSalespersonSheet = true"></up-icon>
+ <up-icon name="arrow-right"
+ @click="showSalespersonSheet = true"></up-icon>
</template>
</up-form-item>
- <up-form-item label="鎶ヤ环鏃ユ湡" prop="quotationDate" required>
- <up-input
- v-model="form.quotationDate"
- placeholder="璇烽�夋嫨鎶ヤ环鏃ユ湡"
- readonly
- @click="showQuotationDatePicker = true"
- />
+ <up-form-item label="鎶ヤ环鏃ユ湡"
+ prop="quotationDate"
+ required>
+ <up-input v-model="form.quotationDate"
+ placeholder="璇烽�夋嫨鎶ヤ环鏃ユ湡"
+ readonly
+ @click="showQuotationDatePicker = true" />
<template #right>
- <up-icon name="arrow-right" @click="showQuotationDatePicker = true"></up-icon>
+ <up-icon name="arrow-right"
+ @click="showQuotationDatePicker = true"></up-icon>
</template>
</up-form-item>
- <up-form-item label="鏈夋晥鏈熻嚦" prop="validDate" required>
- <up-input
- v-model="form.validDate"
- placeholder="璇烽�夋嫨鏈夋晥鏈�"
- readonly
- @click="showValidDatePicker = true"
- />
+ <up-form-item label="鏈夋晥鏈熻嚦"
+ prop="validDate"
+ required>
+ <up-input v-model="form.validDate"
+ placeholder="璇烽�夋嫨鏈夋晥鏈�"
+ readonly
+ @click="showValidDatePicker = true" />
<template #right>
- <up-icon name="arrow-right" @click="showValidDatePicker = true"></up-icon>
+ <up-icon name="arrow-right"
+ @click="showValidDatePicker = true"></up-icon>
</template>
</up-form-item>
- <up-form-item label="浠樻鏂瑰紡" prop="paymentMethod" required>
- <up-input v-model="form.paymentMethod" placeholder="璇疯緭鍏ヤ粯娆炬柟寮�" clearable />
+ <up-form-item label="浠樻鏂瑰紡"
+ prop="paymentMethod"
+ required>
+ <up-input v-model="form.paymentMethod"
+ placeholder="璇疯緭鍏ヤ粯娆炬柟寮�"
+ clearable />
</up-form-item>
- <up-form-item label="澶囨敞" prop="remark">
- <up-textarea v-model="form.remark" placeholder="璇疯緭鍏ュ娉�" auto-height />
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ auto-height />
</up-form-item>
</u-cell-group>
-
- <u-cell-group title="瀹℃壒鑺傜偣" class="form-section">
+ <u-cell-group title="浜у搧淇℃伅"
+ class="form-section">
<view class="section-tools">
- <up-button type="primary" size="small" text="鏂板鑺傜偣" @click="addApproverNode" />
+ <up-button type="primary"
+ size="small"
+ text="鏂板浜у搧"
+ @click="addProduct" />
</view>
- <view v-if="salespersonList.length === 0" class="empty-text">
- <text>鏆傛棤鍙�夊鎵逛汉锛岃妫�鏌ョ敤鎴锋暟鎹�</text>
- </view>
- <view class="node-list">
- <view v-for="(node, index) in approverNodes" :key="node.id" class="node-card">
- <view class="node-top">
- <text class="node-title">瀹℃壒鑺傜偣 {{ index + 1 }}</text>
- <up-icon
- v-if="approverNodes.length > 1"
- name="trash"
- color="#ee0a24"
- size="18"
- @click="removeApproverNode(index)"
- ></up-icon>
- </view>
- <view class="picker-field" @click="openApproverPicker(index)">
- <up-input :model-value="node.nickName || ''" placeholder="璇烽�夋嫨瀹℃壒浜�" readonly disabled />
- <up-icon name="arrow-right" color="#909399" size="16"></up-icon>
- </view>
- </view>
- </view>
- </u-cell-group>
-
- <u-cell-group title="浜у搧淇℃伅" class="form-section">
- <view class="section-tools">
- <up-button type="primary" size="small" text="鏂板浜у搧" @click="addProduct" />
- </view>
- <view v-if="form.products.length === 0" class="empty-text">
+ <view v-if="form.products.length === 0"
+ class="empty-text">
<text>鏆傛棤浜у搧锛岃鍏堟坊鍔犱骇鍝�</text>
</view>
- <view v-else class="product-list">
- <view v-for="(product, index) in form.products" :key="product.uid" class="product-card">
+ <view v-else
+ class="product-list">
+ <view v-for="(product, index) in form.products"
+ :key="product.uid"
+ class="product-card">
<view class="product-header">
<text class="product-title">浜у搧 {{ index + 1 }}</text>
- <up-icon name="trash" color="#ee0a24" size="18" @click="removeProduct(index)"></up-icon>
+ <up-icon name="trash"
+ color="#ee0a24"
+ size="18"
+ @click="removeProduct(index)"></up-icon>
</view>
<up-divider></up-divider>
<view class="product-body">
<up-form-item label="浜у搧鍚嶇О">
- <up-input
- v-model="product.product"
- placeholder="璇烽�夋嫨浜у搧"
- readonly
- @click="openProductPicker(index)"
- />
+ <up-input v-model="product.product"
+ placeholder="璇烽�夋嫨浜у搧"
+ readonly
+ @click="openProductPicker(index)" />
<template #right>
- <up-icon name="arrow-right" @click="openProductPicker(index)"></up-icon>
+ <up-icon name="arrow-right"
+ @click="openProductPicker(index)"></up-icon>
</template>
</up-form-item>
<up-form-item label="瑙勬牸鍨嬪彿">
- <up-input
- v-model="product.specification"
- placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
- readonly
- @click="openModelPicker(index)"
- />
+ <up-input v-model="product.ProductModel"
+ placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
+ readonly
+ @click="openModelPicker(index)" />
<template #right>
- <up-icon name="arrow-right" @click="openModelPicker(index)"></up-icon>
+ <up-icon name="arrow-right"
+ @click="openModelPicker(index)"></up-icon>
</template>
</up-form-item>
<up-form-item label="鍗曚綅">
- <up-input v-model="product.unit" placeholder="璇疯緭鍏ュ崟浣�" clearable />
+ <up-input v-model="product.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable />
</up-form-item>
<up-form-item label="鏁伴噺">
- <up-input
- v-model="product.quantity"
- type="number"
- placeholder="璇疯緭鍏ユ暟閲�"
- clearable
- @blur="calculateAmount(product)"
- />
+ <up-input v-model="product.quantity"
+ type="number"
+ placeholder="璇疯緭鍏ユ暟閲�"
+ clearable
+ @blur="calculateAmount(product)" />
</up-form-item>
<up-form-item label="鍗曚环">
- <up-input
- v-model="product.unitPrice"
- type="number"
- placeholder="璇疯緭鍏ュ崟浠�"
- clearable
- @blur="calculateAmount(product)"
- />
+ <up-input v-model="product.unitPrice"
+ type="number"
+ placeholder="璇疯緭鍏ュ崟浠�"
+ clearable
+ @blur="calculateAmount(product)" />
</up-form-item>
<up-form-item label="閲戦">
- <up-input :model-value="formatAmount(product.amount)" disabled placeholder="鑷姩璁$畻" />
+ <up-input :model-value="formatAmount(product.amount)"
+ disabled
+ placeholder="鑷姩璁$畻" />
</up-form-item>
</view>
</view>
</view>
</u-cell-group>
-
- <u-cell-group title="姹囨�讳俊鎭�" class="form-section">
+ <u-cell-group title="姹囨�讳俊鎭�"
+ class="form-section">
<up-form-item label="鎶ヤ环鎬婚">
- <up-input :model-value="formatAmount(totalAmount)" disabled placeholder="鑷姩姹囨��" />
+ <up-input :model-value="formatAmount(totalAmount)"
+ disabled
+ placeholder="鑷姩姹囨��" />
</up-form-item>
</u-cell-group>
</up-form>
</view>
-
- <FooterButtons :loading="loading" confirmText="淇濆瓨" @cancel="goBack" @confirm="handleSubmit" />
-
- <up-action-sheet :show="showCustomerSheet" title="閫夋嫨瀹㈡埛" :actions="customerActions" @select="onSelectCustomer" @close="showCustomerSheet = false" />
- <up-action-sheet :show="showSalespersonSheet" title="閫夋嫨涓氬姟鍛�" :actions="salespersonActions" @select="onSelectSalesperson" @close="showSalespersonSheet = false" />
- <up-action-sheet :show="showProductSheet" title="閫夋嫨浜у搧" :actions="productActions" @select="onSelectProduct" @close="showProductSheet = false" />
- <up-action-sheet :show="showModelSheet" title="閫夋嫨瑙勬牸鍨嬪彿" :actions="modelActions" @select="onSelectModel" @close="showModelSheet = false" />
- <up-datetime-picker :show="showQuotationDatePicker" v-model="quotationDateValue" mode="date" @confirm="onQuotationDateConfirm" @cancel="showQuotationDatePicker = false" />
- <up-datetime-picker :show="showValidDatePicker" v-model="validDateValue" mode="date" @confirm="onValidDateConfirm" @cancel="showValidDatePicker = false" />
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ <up-action-sheet :show="showCustomerSheet"
+ title="閫夋嫨瀹㈡埛"
+ :actions="customerActions"
+ @select="onSelectCustomer"
+ @close="showCustomerSheet = false" />
+ <up-action-sheet :show="showSalespersonSheet"
+ title="閫夋嫨涓氬姟鍛�"
+ :actions="salespersonActions"
+ @select="onSelectSalesperson"
+ @close="showSalespersonSheet = false" />
+ <up-action-sheet :show="showProductSheet"
+ title="閫夋嫨浜у搧"
+ :actions="productActions"
+ @select="onSelectProduct"
+ @close="showProductSheet = false" />
+ <up-action-sheet :show="showModelSheet"
+ title="閫夋嫨瑙勬牸鍨嬪彿"
+ :actions="modelActions"
+ @select="onSelectModel"
+ @close="showModelSheet = false" />
+ <up-datetime-picker :show="showQuotationDatePicker"
+ v-model="quotationDateValue"
+ mode="date"
+ @confirm="onQuotationDateConfirm"
+ @cancel="showQuotationDatePicker = false" />
+ <up-datetime-picker :show="showValidDatePicker"
+ v-model="validDateValue"
+ mode="date"
+ @confirm="onValidDateConfirm"
+ @cancel="showValidDatePicker = false" />
</view>
</template>
@@ -179,7 +202,11 @@
import { formatDateToYMD } from "@/utils/ruoyi";
import { modelList, productTreeList } from "@/api/basicData/product";
import { userListNoPageByTenantId } from "@/api/system/user";
- import { addQuotation, getCustomerList, getQuotationDetail, updateQuotation } from "@/api/salesManagement/salesQuotation";
+ import {
+ addQuotation,
+ getCustomerList,
+ updateQuotation,
+ } from "@/api/salesManagement/salesQuotation";
const formRef = ref();
const loading = ref(false);
@@ -199,47 +226,73 @@
const modelActions = ref([]);
let uidSeed = 1;
- let nextApproverId = 2;
const form = ref({
id: undefined,
quotationNo: "",
+ customerId: undefined,
customer: "",
salesperson: "",
quotationDate: "",
validDate: "",
paymentMethod: "",
- status: "寰呭鎵�",
+ status: "鑽夌",
remark: "",
- approveUserIds: "",
products: [],
+ subtotal: 0,
+ freight: 0,
+ otherFee: 0,
+ discountRate: 0,
+ discountAmount: 0,
totalAmount: 0,
});
-
- const approverNodes = ref([{ id: 1, userId: "", nickName: "" }]);
const rules = {
customer: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
salesperson: [{ required: true, message: "璇烽�夋嫨涓氬姟鍛�", trigger: "change" }],
- quotationDate: [{ required: true, message: "璇烽�夋嫨鎶ヤ环鏃ユ湡", trigger: "change" }],
+ quotationDate: [
+ { required: true, message: "璇烽�夋嫨鎶ヤ环鏃ユ湡", trigger: "change" },
+ ],
validDate: [{ required: true, message: "璇烽�夋嫨鏈夋晥鏈�", trigger: "change" }],
- paymentMethod: [{ required: true, message: "璇疯緭鍏ヤ粯娆炬柟寮�", trigger: "blur" }],
+ paymentMethod: [
+ { required: true, message: "璇疯緭鍏ヤ粯娆炬柟寮�", trigger: "blur" },
+ ],
};
const pageTitle = computed(() => (quotationId.value ? "缂栬緫鎶ヤ环" : "鏂板鎶ヤ环"));
const totalAmount = computed(() =>
- Number((form.value.products || []).reduce((sum, item) => sum + Number(item.amount || 0), 0).toFixed(2))
+ Number(
+ (form.value.products || [])
+ .reduce((sum, item) => sum + Number(item.amount || 0), 0)
+ .toFixed(2)
+ )
);
- const customerActions = computed(() => customerList.value.map(item => ({ name: item.customerName, value: item.customerName })));
- const salespersonActions = computed(() => salespersonList.value.map(item => ({ name: item.nickName, value: item.nickName })));
- const productActions = computed(() => productList.value.map(item => ({ name: item.label, value: item.value, label: item.label })));
+ const customerActions = computed(() =>
+ customerList.value.map(item => ({
+ name: item.customerName,
+ value: item.id,
+ }))
+ );
+ const salespersonActions = computed(() =>
+ salespersonList.value.map(item => ({
+ name: item.nickName,
+ value: item.nickName,
+ }))
+ );
+ const productActions = computed(() =>
+ productList.value.map(item => ({
+ name: item.label,
+ value: item.value,
+ label: item.label,
+ }))
+ );
const createEmptyProduct = () => ({
uid: `p_${uidSeed++}`,
productId: "",
product: "",
- specificationId: "",
- specification: "",
+ productModelId: "",
+ ProductModel: "",
unit: "",
quantity: 1,
unitPrice: 0,
@@ -254,7 +307,10 @@
if (item.children && item.children.length) {
walk(item.children);
} else {
- result.push({ label: item.label || item.productName || "", value: item.id || item.value });
+ result.push({
+ label: item.label || item.productName || "",
+ value: item.id || item.value,
+ });
}
});
};
@@ -266,18 +322,12 @@
const goBack = () => uni.navigateBack();
const calculateAmount = product => {
- product.amount = Number((Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2));
+ product.amount = Number(
+ (Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2)
+ );
form.value.totalAmount = totalAmount.value;
};
- const addApproverNode = () => approverNodes.value.push({ id: nextApproverId++, userId: "", nickName: "" });
- const removeApproverNode = index => approverNodes.value.splice(index, 1);
- const openApproverPicker = index => {
- uni.setStorageSync("stepIndex", index);
- uni.navigateTo({
- url: "/pages/cooperativeOffice/collaborativeApproval/contactSelect",
- });
- };
const addProduct = () => form.value.products.push(createEmptyProduct());
const removeProduct = index => {
form.value.products.splice(index, 1);
@@ -285,8 +335,14 @@
};
const fetchModelOptions = async (productId, product) => {
- const rows = await modelList({ id: productId }).catch(() => []);
- product.modelOptions = Array.isArray(rows) ? rows : [];
+ try {
+ const res = await modelList({ id: productId });
+ const rows = res?.data?.records || res?.data || res?.records || res || [];
+ product.modelOptions = Array.isArray(rows) ? rows : [];
+ } catch (error) {
+ console.error("鑾峰彇瑙勬牸鍨嬪彿澶辫触:", error);
+ product.modelOptions = [];
+ }
};
const openProductPicker = index => {
@@ -300,7 +356,11 @@
uni.showToast({ title: "璇峰厛閫夋嫨浜у搧", icon: "none" });
return;
}
- modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit }));
+ modelActions.value = (current.modelOptions || []).map(item => ({
+ name: item.model || item.specification,
+ value: item.id,
+ unit: item.unit,
+ }));
if (!modelActions.value.length) {
uni.showToast({ title: "鏆傛棤瑙勬牸鍨嬪彿", icon: "none" });
return;
@@ -309,27 +369,21 @@
};
const onSelectCustomer = action => {
- form.value.customer = action.value;
+ form.value.customerId = action.value;
+ form.value.customer = action.name;
showCustomerSheet.value = false;
};
const onSelectSalesperson = action => {
form.value.salesperson = action.value;
showSalespersonSheet.value = false;
};
- const onSelectApprover = data => {
- const { stepIndex, contact } = data || {};
- if (stepIndex === undefined || !contact) return;
- if (!approverNodes.value[stepIndex]) return;
- approverNodes.value[stepIndex].userId = contact.userId;
- approverNodes.value[stepIndex].nickName = contact.nickName;
- };
const onSelectProduct = action => {
const current = form.value.products[currentProductIndex.value];
if (!current) return;
current.productId = action.value;
current.product = action.label;
- current.specificationId = "";
- current.specification = "";
+ current.productModelId = "";
+ current.ProductModel = "";
current.unit = "";
current.modelOptions = [];
showProductSheet.value = false;
@@ -338,8 +392,8 @@
const onSelectModel = action => {
const current = form.value.products[currentProductIndex.value];
if (!current) return;
- current.specificationId = action.value;
- current.specification = action.name;
+ current.productModelId = action.value;
+ current.ProductModel = action.name;
current.unit = action.unit || current.unit;
showModelSheet.value = false;
};
@@ -353,70 +407,114 @@
};
const fetchBaseOptions = async () => {
- const [customers, users, productTree] = await Promise.all([
- getCustomerList({ current: -1, size: -1 }).catch(() => ({})),
- userListNoPageByTenantId().catch(() => ({})),
- productTreeList().catch(() => []),
- ]);
+ const [customers, users, productTree] = await Promise.all([
+ getCustomerList({ current: -1, size: -1 }).catch(() => ({})),
+ userListNoPageByTenantId().catch(() => ({})),
+ productTreeList().catch(() => []),
+ ]);
customerList.value = customers?.data?.records || customers?.records || [];
const userRows = users?.data || [];
salespersonList.value = Array.isArray(userRows) ? userRows : [];
- productList.value = flattenProductTree(Array.isArray(productTree) ? productTree : productTree?.data || []);
+ productList.value = flattenProductTree(
+ Array.isArray(productTree) ? productTree : productTree?.data || []
+ );
+ };
+
+ // 鏍规嵁鍚嶇О鍙嶆煡鑺傜偣 id锛屼究浜庝粎瀛樺悕绉版椂鐨勫弽鏄�
+ const findNodeIdByLabel = (nodes, label) => {
+ if (!label) return null;
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (node.label === label) return node.value;
+ if (node.children && node.children.length > 0) {
+ const found = findNodeIdByLabel(node.children, label);
+ if (found !== null && found !== undefined) return found;
+ }
+ }
+ return null;
};
const normalizeProductRows = async rows => {
- const normalized = await Promise.all((Array.isArray(rows) ? rows : []).map(async item => {
- const row = {
- uid: `p_${uidSeed++}`,
- productId: item.productId || "",
- product: item.product || item.productName || "",
- specificationId: item.specificationId || "",
- specification: item.specification || "",
- unit: item.unit || "",
- quantity: Number(item.quantity || 1),
- unitPrice: Number(item.unitPrice || 0),
- amount: Number(item.amount || 0),
- modelOptions: [],
- };
- if (row.productId) await fetchModelOptions(row.productId, row);
- return row;
- }));
+ const normalized = await Promise.all(
+ (Array.isArray(rows) ? rows : []).map(async item => {
+ const productName = item.product || item.productName || "";
+ // 浼樺厛鐢� productId锛涘鏋滃彧鏈夊悕绉帮紝灏濊瘯鍙嶆煡 id 浠ヤ究閫夋嫨鍣ㄥ弽鏄�
+ let resolvedProductId =
+ item.productId ||
+ findNodeIdByLabel(productList.value, productName) ||
+ "";
+
+ const row = {
+ uid: `p_${uidSeed++}`,
+ productId: resolvedProductId,
+ product: productName,
+ productModelId: item.productModelId || "",
+ ProductModel: item.ProductModel || item.specification || "",
+ unit: item.unit || "",
+ quantity: Number(item.quantity || 1),
+ unitPrice: Number(item.unitPrice || 0),
+ amount: Number(item.amount || 0),
+ modelOptions: [],
+ };
+
+ if (row.productId) {
+ await fetchModelOptions(row.productId, row);
+ // 濡傛灉娌℃湁 productModelId 浣嗘湁 ProductModel 鍚嶇О锛屽皾璇曚粠 modelOptions 涓尮閰� ID
+ if (!row.productModelId && row.ProductModel) {
+ const foundModel = row.modelOptions.find(
+ m =>
+ m.model === row.ProductModel ||
+ m.specification === row.ProductModel
+ );
+ if (foundModel) {
+ row.productModelId = foundModel.id;
+ // 缁熶竴浣跨敤 modelOptions 涓殑瀛楁
+ row.ProductModel =
+ foundModel.model || foundModel.specification || row.ProductModel;
+ row.unit = foundModel.unit || row.unit;
+ }
+ }
+ }
+ return row;
+ })
+ );
form.value.products = normalized;
};
const loadDetail = async () => {
if (!quotationId.value) return;
- uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
- try {
- const res = await getQuotationDetail({ id: quotationId.value });
- const data = res?.data || {};
+
+ // 鐩存帴浠庢湰鍦板瓨鍌ㄨ幏鍙栨暟鎹紝涓嶅啀璋冪敤璇︽儏鎺ュ彛
+ const cachedData = uni.getStorageSync("salesQuotationDetail");
+ if (
+ cachedData &&
+ (cachedData.id === quotationId.value ||
+ cachedData.id === Number(quotationId.value))
+ ) {
+ const data = cachedData;
form.value = {
...form.value,
id: data.id,
quotationNo: data.quotationNo || "",
+ customerId: data.customerId,
customer: data.customer || "",
salesperson: data.salesperson || "",
quotationDate: data.quotationDate || "",
validDate: data.validDate || "",
paymentMethod: data.paymentMethod || "",
- status: data.status || "寰呭鎵�",
+ status: data.status || "鑽夌",
remark: data.remark || "",
+ subtotal: data.subtotal || 0,
+ freight: data.freight || 0,
+ otherFee: data.otherFee || 0,
+ discountRate: data.discountRate || 0,
+ discountAmount: data.discountAmount || 0,
+ totalAmount: data.totalAmount || 0,
};
await normalizeProductRows(data.products || []);
- if (data.approveUserIds) {
- const ids = String(data.approveUserIds).split(",").map(item => item.trim()).filter(Boolean);
- approverNodes.value = ids.map((userId, index) => ({
- id: index + 1,
- userId,
- nickName: salespersonList.value.find(item => String(item.userId) === String(userId))?.nickName || "",
- }));
- nextApproverId = approverNodes.value.length + 1;
- }
form.value.totalAmount = totalAmount.value;
- } catch {
- uni.showToast({ title: "鑾峰彇璇︽儏澶辫触", icon: "error" });
- } finally {
- uni.hideLoading();
+ } else {
+ console.warn("鏈壘鍒扮紦瀛樼殑鎶ヤ环鍗曡鎯呮暟鎹�");
}
};
@@ -425,16 +523,16 @@
uni.showToast({ title: "璇疯嚦灏戞坊鍔犱竴涓骇鍝�", icon: "none" });
return false;
}
- const invalid = form.value.products.some(item => !item.productId || !item.specificationId || !item.unit || !Number(item.quantity) || !Number(item.unitPrice));
+ const invalid = form.value.products.some(
+ item =>
+ !item.productId ||
+ !item.productModelId ||
+ !item.unit ||
+ !Number(item.quantity) ||
+ !Number(item.unitPrice)
+ );
if (invalid) {
uni.showToast({ title: "璇峰畬鍠勪骇鍝佷俊鎭�", icon: "none" });
- return false;
- }
- return true;
- };
- const validateApprovers = () => {
- if (approverNodes.value.some(item => !item.userId)) {
- uni.showToast({ title: "璇烽�夋嫨瀹℃壒浜�", icon: "none" });
return false;
}
return true;
@@ -442,17 +540,20 @@
const handleSubmit = async () => {
const valid = await formRef.value.validate().catch(() => false);
- if (!valid || !validateApprovers() || !validateProducts()) return;
+ if (!valid || !validateProducts()) return;
loading.value = true;
+
+ // 鍚屾鏈�鏂扮殑鎬婚
+ form.value.totalAmount = totalAmount.value;
+ form.value.subtotal = totalAmount.value;
+
const payload = {
...form.value,
- approveUserIds: approverNodes.value.map(item => item.userId).join(","),
- totalAmount: totalAmount.value,
products: form.value.products.map(item => ({
productId: item.productId,
product: item.product,
- specificationId: item.specificationId,
- specification: item.specification,
+ productModelId: item.productModelId,
+ ProductModel: item.ProductModel,
quantity: Number(item.quantity || 0),
unit: item.unit,
unitPrice: Number(item.unitPrice || 0),
@@ -486,16 +587,12 @@
onMounted(async () => {
await fetchBaseOptions();
- uni.$on("selectContact", onSelectApprover);
if (quotationId.value) {
await loadDetail();
}
});
- onUnmounted(() => {
- uni.$off("selectContact", onSelectApprover);
- uni.removeStorageSync("stepIndex");
- });
+ onUnmounted(() => {});
</script>
<style scoped lang="scss">
@@ -547,7 +644,6 @@
padding: 12px 12px 0;
}
- .node-list,
.product-list {
padding: 12px;
display: flex;
@@ -555,31 +651,12 @@
gap: 12px;
}
- .node-card {
- background: #f8fbff;
- border-radius: 12px;
- padding: 12px;
- border: 1px solid #e6eef8;
- }
-
- .picker-field {
- display: flex;
- align-items: center;
- gap: 8px;
- }
-
- .picker-field :deep(.u-input) {
- flex: 1;
- }
-
- .node-top,
.product-header {
display: flex;
align-items: center;
justify-content: space-between;
}
- .node-title,
.product-title {
font-size: 14px;
font-weight: 600;
diff --git a/src/pages/sales/salesQuotation/index.vue b/src/pages/sales/salesQuotation/index.vue
index a6a5103..4ecf910 100644
--- a/src/pages/sales/salesQuotation/index.vue
+++ b/src/pages/sales/salesQuotation/index.vue
@@ -1,47 +1,50 @@
<template>
<view class="sales-account">
- <PageHeader title="閿�鍞姤浠�" @back="goBack" />
-
+ <PageHeader title="閿�鍞姤浠�"
+ @back="goBack" />
<view class="search-section">
<view class="search-bar">
<view class="search-input">
- <up-input
- class="search-text"
- v-model="quotationNo"
- placeholder="璇疯緭鍏ユ姤浠峰崟鍙锋悳绱�"
- clearable
- @change="getList"
- />
+ <up-input class="search-text"
+ v-model="quotationNo"
+ placeholder="璇疯緭鍏ユ姤浠峰崟鍙锋悳绱�"
+ clearable
+ @change="getList" />
</view>
- <view class="filter-button" @click="getList">
- <up-icon name="search" size="24" color="#999"></up-icon>
+ <view class="filter-button"
+ @click="getList">
+ <up-icon name="search"
+ size="24"
+ color="#999"></up-icon>
</view>
</view>
</view>
-
<view class="tabs-section">
- <up-tabs
- v-model="tabValue"
- :list="tabList"
- itemStyle="width: 20%;height: 80rpx;"
- @change="onTabChange"
- />
+ <up-tabs v-model="tabValue"
+ :list="tabList"
+ itemStyle="width: 20%;height: 80rpx;"
+ @change="onTabChange" />
</view>
-
- <view v-if="quotationList.length > 0" class="ledger-list">
- <view v-for="item in quotationList" :key="item.id" class="ledger-item">
+ <view v-if="quotationList.length > 0"
+ class="ledger-list">
+ <view v-for="item in quotationList"
+ :key="item.id"
+ class="ledger-item">
<view class="item-header">
<view class="item-left">
<view class="document-icon">
- <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff"></up-icon>
</view>
<text class="item-id">{{ item.quotationNo || "-" }}</text>
</view>
- <text class="item-index">{{ item.status || "-" }}</text>
+ <up-tag :text="item.status || '寰呭鎵�'"
+ :type="getStatusType(item.status)"
+ size="mini"
+ shape="circle" />
</view>
-
<up-divider></up-divider>
-
<view class="item-details">
<view class="detail-row">
<text class="detail-label">瀹㈡埛鍚嶇О</text>
@@ -60,43 +63,45 @@
<text class="detail-value">{{ item.validDate || "-" }}</text>
</view>
<view class="detail-row">
- <text class="detail-label">浠樻鏂瑰紡</text>
- <text class="detail-value">{{ item.paymentMethod || "-" }}</text>
- </view>
- <view class="detail-row">
<text class="detail-label">鎶ヤ环閲戦</text>
<text class="detail-value highlight">{{ formatAmount(item.totalAmount) }}</text>
</view>
- <view class="detail-row">
+ <view class="detail-row"
+ v-if="item.remark">
<text class="detail-label">澶囨敞</text>
- <text class="detail-value">{{ item.remark || "-" }}</text>
+ <text class="detail-value">{{ item.remark }}</text>
</view>
</view>
-
<view class="action-buttons">
- <up-button
- class="action-btn"
- size="small"
- type="primary"
- :disabled="!canEdit(item)"
- @click="goEdit(item)"
- >
- 缂栬緫
- </up-button>
- <up-button class="action-btn" size="small" @click="goDetail(item)">璇︽儏</up-button>
- <up-button class="action-btn" size="small" type="error" plain @click="handleDelete(item)">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ :disabled="!canEdit(item)"
+ @click="goEdit(item)">
+ 缂栬緫
+ </up-button>
+ <up-button class="action-btn"
+ size="small"
+ @click="goDetail(item)">璇︽儏</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ plain
+ @click="handleDelete(item)">
鍒犻櫎
</up-button>
</view>
</view>
</view>
-
- <view v-else class="no-data">
+ <view v-else
+ class="no-data">
<text>鏆傛棤閿�鍞姤浠锋暟鎹�</text>
</view>
-
- <view class="fab-button" @click="goAdd">
- <up-icon name="plus" size="28" color="#ffffff"></up-icon>
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
</view>
</view>
</template>
@@ -105,7 +110,10 @@
import { reactive, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
- import { deleteQuotation, getQuotationList } from "@/api/salesManagement/salesQuotation";
+ import {
+ deleteQuotation,
+ getQuotationList,
+ } from "@/api/salesManagement/salesQuotation";
const quotationNo = ref("");
const quotationList = ref([]);
@@ -129,11 +137,13 @@
};
const goAdd = () => {
+ uni.removeStorageSync("salesQuotationDetail");
uni.navigateTo({ url: "/pages/sales/salesQuotation/edit" });
};
const goEdit = item => {
if (!canEdit(item)) return;
+ uni.setStorageSync("salesQuotationDetail", item || {});
uni.navigateTo({ url: `/pages/sales/salesQuotation/edit?id=${item.id}` });
};
@@ -159,6 +169,16 @@
return `楼${num.toFixed(2)}`;
};
+ const getStatusType = status => {
+ const statusMap = {
+ 寰呭鎵�: "info",
+ 瀹℃牳涓�: "primary",
+ 閫氳繃: "success",
+ 鎷掔粷: "danger",
+ };
+ return statusMap[status] || "info";
+ };
+
const getList = () => {
uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
getQuotationList({
diff --git a/src/pages/works.vue b/src/pages/works.vue
index d049f52..41d6798 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -15,7 +15,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -37,7 +38,31 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
+ </view>
+ <text class="item-label">{{item.label}}</text>
+ </up-grid-item>
+ </up-grid>
+ </view>
+ </view>
+ <!-- 宸ヨ壓璁捐 -->
+ <view class="common-module design-module"
+ v-if="hasDesignItems">
+ <view class="module-header">
+ <view class="module-title-container">
+ <text class="module-title">宸ヨ壓璁捐</text>
+ </view>
+ </view>
+ <view class="module-content">
+ <up-grid :border="false"
+ col="4">
+ <up-grid-item v-for="(item, index) in designItems"
+ :key="index"
+ @click="handleCommonItemClick(item)">
+ <view class="icon-container">
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -59,7 +84,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -81,7 +107,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -103,7 +130,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -125,7 +153,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -147,7 +176,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -169,7 +199,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -213,7 +244,8 @@
:key="index"
@click="handleCommonItemClick(item)">
<view class="icon-container">
- <image :src="item.icon" class="item-icon"></image>
+ <image :src="item.icon"
+ class="item-icon"></image>
</view>
<text class="item-label">{{item.label}}</text>
</up-grid-item>
@@ -221,27 +253,28 @@
</view>
</view>
<!-- 璁惧绠$悊妯″潡 -->
-<!-- <view class="common-module equipment-module"-->
-<!-- v-if="hasEquipmentItems">-->
-<!-- <view class="module-header">-->
-<!-- <view class="module-title-container">-->
-<!-- <text class="module-title">璁惧绠$悊</text>-->
-<!-- </view>-->
-<!-- </view>-->
-<!-- <view class="module-content">-->
-<!-- <up-grid :border="false"-->
-<!-- col="4">-->
-<!-- <up-grid-item v-for="(item, index) in equipmentItems"-->
-<!-- :key="index"-->
-<!-- @click="handleCommonItemClick(item)">-->
-<!-- <view class="icon-container">-->
-<!-- <image :src="item.icon" class="item-icon"></image>-->
-<!-- </view>-->
-<!-- <text class="item-label">{{item.label}}</text>-->
-<!-- </up-grid-item>-->
-<!-- </up-grid>-->
-<!-- </view>-->
-<!-- </view>-->
+ <view class="common-module equipment-module"
+ v-if="hasEquipmentItems">
+ <view class="module-header">
+ <view class="module-title-container">
+ <text class="module-title">璁惧绠$悊</text>
+ </view>
+ </view>
+ <view class="module-content">
+ <up-grid :border="false"
+ col="4">
+ <up-grid-item v-for="(item, index) in equipmentItems"
+ :key="index"
+ @click="handleCommonItemClick(item)">
+ <view class="icon-container">
+ <image :src="item.icon"
+ class="item-icon"></image>
+ </view>
+ <text class="item-label">{{item.label}}</text>
+ </up-grid-item>
+ </up-grid>
+ </view>
+ </view>
<!-- 瀹夊叏鐢熶骇妯″潡 -->
<!-- <view class="common-module collaboration-module"-->
<!-- v-if="hasSafetyItems">-->
@@ -271,16 +304,14 @@
<script setup>
import { ref, onMounted, nextTick, reactive, computed } from "vue";
- import { onShow } from "@dcloudio/uni-app";
import { userLoginFacotryList } from "@/api/login";
import { getProductWorkOrderById } from "@/api/productionManagement/productionReporting";
- import { createVersionUpgradeChecker } from "@/utils/versionUpgrade";
import DownloadProgressMask from "@/components/DownloadProgressMask.vue";
+ import { OA_WORKBENCH_ITEMS } from "@/config/oaWorkbench.js";
import modal from "@/plugins/modal";
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
- const { triggerVersionCheck } = createVersionUpgradeChecker({ logPrefix: "[version-works]" });
const show = ref(false);
const factoryList = ref([]);
const factoryListTem = ref([]);
@@ -302,6 +333,25 @@
currentStatus.value = statusList[statusIndex];
}, 3000);
};
+ // 宸ヨ壓璁捐鍔熻兘鏁版嵁
+ const designItems = reactive([
+ {
+ icon: "/static/images/icon/jichucanshu.svg",
+ label: "鍩虹鍙傛暟",
+ },
+ {
+ icon: "/static/images/icon/gongxuguanli.svg",
+ label: "宸ュ簭绠$悊",
+ },
+ {
+ icon: "/static/images/icon/bom.svg",
+ label: "BOM",
+ },
+ {
+ icon: "/static/images/icon/gongyiluxian.svg",
+ label: "宸ヨ壓璺嚎",
+ },
+ ]);
// 钀ラ攢绠$悊鍔熻兘鏁版嵁
const marketingItems = reactive([
@@ -333,10 +383,14 @@
icon: "/static/images/icon/gongyingshangwanglai.svg",
label: "渚涘簲鍟嗗線鏉�",
},
- // {
- // icon: "/static/images/icon/caigouguanli.svg",
- // label: "閲囪喘閫�璐�",
- // },
+ {
+ icon: "/static/images/icon/caigouguanli.svg",
+ label: "閲囪喘閫�璐�",
+ },
+ {
+ icon: "/static/images/icon/gongyingshangdangan.svg",
+ label: "渚涘簲鍟嗘。妗�",
+ },
]);
// 璐㈠姟绠$悊鍔熻兘鏁版嵁
@@ -390,8 +444,12 @@
// 妗f绠$悊鍔熻兘鏁版嵁
const archiveManagementItems = reactive([
{
- icon: "/static/images/icon/gongyingshangdangan.svg",
- label: "渚涘簲鍟嗘。妗�",
+ icon: "/static/images/icon/jieyuedengji.svg",
+ label: "鍊熼槄鐧昏",
+ },
+ {
+ icon: "/static/images/icon/guihuandengji.svg",
+ label: "褰掕繕鐧昏",
},
]);
@@ -410,7 +468,7 @@
// 浠撳偍鐗╂祦鍔熻兘鏁版嵁
const warehouseLogisticsItems = reactive([
{
- icon: "/static/images/icon/xiaoshoutaizhang.svg",
+ icon: "/static/images/icon/kucunguanli.svg",
label: "搴撳瓨绠$悊",
},
]);
@@ -511,30 +569,39 @@
// 鐢熶骇绠℃帶鍔熻兘鏁版嵁
const productionItems = reactive([
- // {
- // icon: "/static/images/icon/shengchandingdan@2x.svg",
- // label: "鐢熶骇璁㈠崟",
- // },
- // {
- // icon: "/static/images/icon/shengchanpaigong@2x.svg",
- // label: "鐢熶骇娲惧伐",
- // },
- // {
- // icon: "/static/images/icon/shengchanpaichan@2x.svg",
- // label: "宸ュ簭鎺掍骇",
- // },
+ {
+ icon: "/static/images/icon/shengchanjihua.svg",
+ label: "涓荤敓浜ц鍒�",
+ },
+ {
+ icon: "/static/images/icon/shengchandingdan.svg",
+ label: "鐢熶骇璁㈠崟",
+ },
+ {
+ icon: "/static/images/icon/shengchanzhuisu.svg",
+ label: "鐢熶骇杩芥函",
+ },
+ {
+ icon: "/static/images/icon/shengchanshikuang.svg",
+ label: "宸ュ簭鐢熶骇瀹炲喌",
+ },
+ {
+ icon: "/static/images/icon/shengchanpaichan.svg",
+ label: "鐢熶骇鎺掍骇",
+ },
+
{
icon: "/static/images/icon/shengchanbaogong.svg",
label: "鐢熶骇鎶ュ伐",
},
{
- icon: "/static/images/icon/shengchanbaogong.svg",
- label: "鐢熶骇宸ュ崟",
+ icon: "/static/images/icon/baogongtaizhang.svg",
+ label: "鎶ュ伐鍙拌处",
},
- // {
- // icon: "/static/images/icon/shengchanhesuan@2x.svg",
- // label: "鐢熶骇鏍哥畻",
- // },
+ {
+ icon: "/static/images/icon/shengchanhesuan.svg",
+ label: "鐢熶骇鏍哥畻",
+ },
]);
// 璁惧绠$悊鍔熻兘鏁版嵁
@@ -563,6 +630,10 @@
// 澶勭悊甯哥敤鍔熻兘鐐瑰嚮
const handleCommonItemClick = item => {
+ if (item.path) {
+ uni.navigateTo({ url: item.path });
+ return;
+ }
// 鏍规嵁涓嶅悓鐨勫姛鑳介」杩涜璺宠浆
switch (item.label) {
case "瀹㈡埛妗f":
@@ -775,9 +846,9 @@
url: "/pages/productionManagement/productionDispatching/index",
});
break;
- case "宸ュ簭鎺掍骇":
+ case "宸ヨ壓璺嚎":
uni.navigateTo({
- url: "/pages/productionManagement/processScheduling/index",
+ url: "/pages/productionManagement/processRoute/index",
});
break;
case "鐢熶骇宸ュ崟":
@@ -785,12 +856,37 @@
url: "/pages/productionManagement/workOrder/index",
});
break;
+ case "涓荤敓浜ц鍒�":
+ uni.navigateTo({
+ url: "/pages/productionManagement/mainProductionPlan/index",
+ });
+ break;
+ case "鐢熶骇鎺掍骇":
+ uni.navigateTo({
+ url: "/pages/productionManagement/productionScheduling/index",
+ });
+ break;
case "鐢熶骇鎶ュ伐":
getcode();
+ break;
+ case "鎶ュ伐鍙拌处":
+ uni.navigateTo({
+ url: "/pages/productionManagement/productionReporting/ledger",
+ });
break;
case "鐢熶骇鏍哥畻":
uni.navigateTo({
url: "/pages/productionManagement/productionAccounting/index",
+ });
+ break;
+ case "鐢熶骇杩芥函":
+ uni.navigateTo({
+ url: "/pages/productionManagement/productionTraceability/index",
+ });
+ break;
+ case "宸ュ簭鐢熶骇瀹炲喌":
+ uni.navigateTo({
+ url: "/pages/productionManagement/processStatistics/index",
});
break;
case "璁惧鍙拌处":
@@ -923,6 +1019,31 @@
url: "/pages/customerService/afterSalesHandling/index",
});
break;
+ case "鍊熼槄鐧昏":
+ uni.navigateTo({
+ url: "/pages/fileManagement/borrow/index",
+ });
+ break;
+ case "褰掕繕鐧昏":
+ uni.navigateTo({
+ url: "/pages/fileManagement/return/index",
+ });
+ break;
+ case "鍩虹鍙傛暟":
+ uni.navigateTo({
+ url: "/pages/productionDesign/basicParameters/index",
+ });
+ break;
+ case "宸ュ簭绠$悊":
+ uni.navigateTo({
+ url: "/pages/productionDesign/processManagement/index",
+ });
+ break;
+ case "BOM":
+ uni.navigateTo({
+ url: "/pages/productionDesign/bom/index",
+ });
+ break;
default:
uni.showToast({
title: `鐐瑰嚮浜�${item.label}`,
@@ -954,7 +1075,7 @@
factoryList.value = [];
});
}
- const getcode = () => {
+ const getcode = async () => {
uni.scanCode({
success: async res => {
// 瑙f瀽浜岀淮鐮佸唴瀹�
@@ -979,14 +1100,12 @@
const workData = workRes.data;
console.log("宸ュ崟鏁版嵁:", workData);
- orderRow = JSON.stringify({
- id: workData.id || workOrderId,
- planQuantity: workData.planQuantity - workData.completeQuantity,
- productProcessRouteItemId:
- workData.productProcessRouteItemId ||
- workData.浜у搧宸ヨ壓璺嚎椤笽D ||
- "",
- });
+ if (workData.endOrder === true) {
+ modal.msgError("璇ヨ鍗曞凡缁撴潫锛屾棤娉曟姤宸�");
+ return;
+ }
+
+ orderRow = JSON.stringify(workData);
console.log("鏋勯�犵殑orderRow:", orderRow);
} else {
@@ -1110,10 +1229,13 @@
// 瀹氫箟鑿滃崟閰嶇疆鏄犲皠
const menuMapping = {
- collaboration: { target: collaborationItems, specialMapping: { "瑙勭珷鍒跺害": "瑙勭珷鍒跺害绠$悊" } },
- archiveManagement: { target: archiveManagementItems, specialMapping: { "渚涘簲鍟嗘。妗�": "渚涘簲鍟嗙鐞�" } },
+ collaboration: {
+ target: collaborationItems,
+ specialMapping: { 瑙勭珷鍒跺害: "瑙勭珷鍒跺害绠$悊" },
+ },
+ purchase: { specialMapping: { 渚涘簲鍟嗘。妗�: "渚涘簲鍟嗙鐞�" } },
};
- console.log(allowedMenuTitles)
+ console.log(allowedMenuTitles);
// 閫氱敤杩囨护鍑芥暟
const filterArray = (targetArray, specialMapping) => {
const filtered = targetArray.filter(item => {
@@ -1128,9 +1250,9 @@
// 杩囨护鍚勪釜妯″潡
filterArray(marketingItems);
- filterArray(purchaseItems);
+ filterArray(designItems);
+ filterArray(purchaseItems, menuMapping.purchase.specialMapping);
filterArray(financeManagementItems);
- filterArray(archiveManagementItems, menuMapping.archiveManagement.specialMapping);
filterArray(collaborationItems, menuMapping.collaboration.specialMapping);
filterArray(safetyItems);
filterArray(humanResourcesItems);
@@ -1138,24 +1260,35 @@
filterArray(qualityItems);
filterArray(productionItems);
filterArray(equipmentItems);
+ filterArray(archiveManagementItems);
+ filterArray(afterSalesServiceItems);
};
// 妫�鏌ユā鍧楁槸鍚︽湁鑿滃崟椤归渶瑕佹樉绀�
const hasMarketingItems = computed(() => marketingItems.length > 0);
+ const hasDesignItems = computed(() => designItems.length > 0);
const hasPurchaseItems = computed(() => purchaseItems.length > 0);
- const hasFinanceManagementItems = computed(() => financeManagementItems.length > 0);
- const hasArchiveManagementItems = computed(() => archiveManagementItems.length > 0);
- const hasAfterSalesServiceItems = computed(() => afterSalesServiceItems.length > 0);
+ const hasFinanceManagementItems = computed(
+ () => financeManagementItems.length > 0
+ );
+ const hasAfterSalesServiceItems = computed(
+ () => afterSalesServiceItems.length > 0
+ );
+ const hasOaItems = computed(() => oaItems.length > 0);
const hasCollaborationItems = computed(() => collaborationItems.length > 0);
const hasSafetyItems = computed(() => safetyItems.length > 0);
const hasQualityItems = computed(() => qualityItems.length > 0);
const hasHumanResourcesItems = computed(() => humanResourcesItems.length > 0);
- const hasWarehouseLogisticsItems = computed(() => warehouseLogisticsItems.length > 0);
+ const hasWarehouseLogisticsItems = computed(
+ () => warehouseLogisticsItems.length > 0
+ );
const hasProductionItems = computed(() => productionItems.length > 0);
const hasEquipmentItems = computed(() => equipmentItems.length > 0);
+ const hasArchiveManagementItems = computed(
+ () => archiveManagementItems.length > 0
+ );
onMounted(() => {
- triggerVersionCheck("onMounted");
// 姣忔杩涘叆棣栭〉閮藉己鍒跺埛鏂扮敤鎴蜂俊鎭拰璺敱鏉冮檺锛屼笉鍋氭湰鍦扮紦瀛樺垽鏂�
userStore.getInfo().then(() => {
userStore
@@ -1170,10 +1303,6 @@
getUserLoginFacotryList();
// 鍚姩閫氱煡鐘舵�佸畾鏃跺櫒
startStatusTimer();
- });
-
- onShow(() => {
- triggerVersionCheck("onShow");
});
</script>
@@ -1524,6 +1653,10 @@
--module-color: #4caf50;
}
+ .oa-module {
+ --module-color: #673ab7;
+ }
+
.production-module {
--module-color: #ff9800;
}
@@ -1766,6 +1899,10 @@
--module-color: #4caf50;
}
+ .oa-module {
+ --module-color: #673ab7;
+ }
+
.production-module {
--module-color: #ff9800;
}
diff --git a/src/static/images/icon/baogongtaizhang.svg b/src/static/images/icon/baogongtaizhang.svg
new file mode 100644
index 0000000..d8a822a
--- /dev/null
+++ b/src/static/images/icon/baogongtaizhang.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34784"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34784)"><path d="M22.914785281249998,14.7584486125C19.94918328125,14.5709972125,17.63954128125,12.1115937125,17.63853528125,9.140073812499999C17.63853528125,7.8441987125,18.08040928125,6.6541986125,18.81540928125,5.7021988625C16.85716058125,5.1570737325,16.802909881250002,5.6943240125,15.13253498125,6.5203237125C13.82265998125,6.7941990125,13.91541008125,5.0013237625,12.260785081249999,5.8281989124999996C13.102535281249999,7.6236989125,13.792035081249999,8.1040738125,13.94516038125,8.736698412500001C13.753535281249999,8.7576985125,13.60566088125,8.8758237125,13.60566088125,9.0201988125C13.60566088125,9.1636982125,13.753535281249999,9.282698912499999,13.94516138125,9.3028235125C13.93991188125,9.3308234125,13.93641088125,9.3570738125,13.92853638125,9.3894486125C13.799910981250001,9.8951988125,12.595911481249999,10.705448112500001,11.571286181249999,11.464948612499999L11.571286181249999,18.7930733125C11.571286181249999,19.3863233125,11.16791058125,19.865823312499998,10.670036281249999,19.865823312499998L7.51128649725,19.865823312499998C7.19803667125,22.8863223125,8.09316146125,25.6539483125,11.87228678125,25.7878223125C15.48691178125,25.9138223125,15.94628718125,25.9138223125,15.94628718125,25.9138223125C15.94628718125,25.9138223125,16.40566158125,25.9138223125,20.021161281250002,25.7878223125C26.02016028125,25.5751973125,24.75928728125,18.7221973125,22.914785281249998,14.7584486125ZM18.59141028125,17.703697312499997L18.59141028125,19.0109473125L16.719785681250002,19.0109473125L16.719785681250002,19.546446312500002L18.57128528125,19.546446312500002L18.57828428125,20.8598213125L16.71978378125,20.8545703125L16.71978378125,22.4269463125L15.25503448125,22.4199463125L15.25503448125,20.8545723125L13.40003438125,20.8545723125L13.40003438125,19.547323312499998L15.25503448125,19.547323312499998L15.25503448125,19.0118233125L13.40003438125,19.0118233125L13.40003438125,17.704573312500003L15.04503438125,17.697574312500002L12.55653478125,14.2518244125L14.20153378125,14.2518244125L16.04865928125,16.6685753125L17.693658281250002,14.2308264125L19.33778628125,14.2308264125L16.934161181249998,17.6975763125L18.59141028125,17.703697312499997Z" fill="#CFCFCF" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M22.914785281249998,14.7584486125C19.94918328125,14.5709972125,17.63954128125,12.1115937125,17.63853528125,9.140073812499999C17.63853528125,7.8441987125,18.08040928125,6.6541986125,18.81540928125,5.7021988625C16.85716058125,5.1570737325,16.802909881250002,5.6943240125,15.13253498125,6.5203237125C13.82265998125,6.7941990125,13.91541008125,5.0013237625,12.260785081249999,5.8281989124999996C13.102535281249999,7.6236989125,13.792035081249999,8.1040738125,13.94516038125,8.736698412500001C13.753535281249999,8.7576985125,13.60566088125,8.8758237125,13.60566088125,9.0201988125C13.60566088125,9.1636982125,13.753535281249999,9.282698912499999,13.94516138125,9.3028235125C13.93991188125,9.3308234125,13.93641088125,9.3570738125,13.92853638125,9.3894486125C13.799910981250001,9.8951988125,12.595911481249999,10.705448112500001,11.571286181249999,11.464948612499999L11.571286181249999,18.7930733125C11.571286181249999,19.3863233125,11.16791058125,19.865823312499998,10.670036281249999,19.865823312499998L7.51128649725,19.865823312499998C7.19803667125,22.8863223125,8.09316146125,25.6539483125,11.87228678125,25.7878223125C15.48691178125,25.9138223125,15.94628718125,25.9138223125,15.94628718125,25.9138223125C15.94628718125,25.9138223125,16.40566158125,25.9138223125,20.021161281250002,25.7878223125C26.02016028125,25.5751973125,24.75928728125,18.7221973125,22.914785281249998,14.7584486125ZM18.59141028125,17.703697312499997L18.59141028125,19.0109473125L16.719785681250002,19.0109473125L16.719785681250002,19.546446312500002L18.57128528125,19.546446312500002L18.57828428125,20.8598213125L16.71978378125,20.8545703125L16.71978378125,22.4269463125L15.25503448125,22.4199463125L15.25503448125,20.8545723125L13.40003438125,20.8545723125L13.40003438125,19.547323312499998L15.25503448125,19.547323312499998L15.25503448125,19.0118233125L13.40003438125,19.0118233125L13.40003438125,17.704573312500003L15.04503438125,17.697574312500002L12.55653478125,14.2518244125L14.20153378125,14.2518244125L16.04865928125,16.6685753125L17.693658281250002,14.2308264125L19.33778628125,14.2308264125L16.934161181249998,17.6975763125L18.59141028125,17.703697312499997Z" fill="#FF9533" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M10.38227634375,2.37548828125L0.92615223375,2.37548828125C0.42740228375,2.37548828125,0.02490234375,2.85586330125,0.02490234375,3.4464882812499997L0.02490234375,18.43261328125C0.02490234375,19.02323728125,0.42740231375,19.50186328125,0.92615223375,19.50186328125L10.38315134375,19.50186328125C10.88102634375,19.50186328125,11.28440134375,19.02323728125,11.28440134375,18.43261328125L11.28440134375,3.4464882812499997C11.28352534375,2.85586330125,10.88102534375,2.37548828125,10.38227634375,2.37548828125ZM3.62815214375,17.89798828125C3.62815214375,18.19286328125,3.42690204375,18.43261328125,3.17840214375,18.43261328125L1.82565224375,18.43261328125C1.57802724375,18.43261328125,1.37590234375,18.191988281249998,1.37590234375,17.89798828125L1.37590234375,16.29148728125C1.37590234375,15.99573828125,1.57715224375,15.75511128125,1.82565224375,15.75511128125L3.17665244375,15.75511128125C3.42602734375,15.75511128125,3.62815214375,15.99573728125,3.62815214375,16.29148728125L3.62815214375,17.89798828125ZM3.62815214375,14.15123828125C3.62815214375,14.44698828125,3.42690204375,14.68673828125,3.17840214375,14.68673828125L1.82565224375,14.68673828125C1.57802724375,14.68673828125,1.37590234375,14.44698828125,1.37590234375,14.15123828125L1.37590234375,12.54561328125C1.37590234375,12.25073818125,1.57715224375,12.01186368125,1.82565224375,12.01186368125L3.17665244375,12.01186368125C3.42602734375,12.01186368125,3.62815214375,12.25073818125,3.62815214375,12.54561328125L3.62815214375,14.15123828125ZM3.62815214375,10.40448858125C3.62815214375,10.70023918125,3.42690204375,10.94086358125,3.17840214375,10.94086358125L1.82565224375,10.94086358125C1.57802724375,10.94086358125,1.37590234375,10.70111368125,1.37590234375,10.40448858125L1.37590234375,8.79886388125C1.37590234375,8.503988781250001,1.57715224375,8.26423888125,1.82565224375,8.26423888125L3.17665244375,8.26423888125C3.42602734375,8.26423888125,3.62815214375,8.503988781250001,3.62815214375,8.79886388125L3.62815214375,10.40448858125ZM6.32927704375,18.43261528125L4.97827724375,18.43261528125C4.72890234375,18.43261528125,4.52852724375,18.19199028125,4.52852724375,17.89798928125L4.52852724375,16.29148928125C4.52852724375,15.99574028125,4.73065234375,15.75511328125,4.97827724375,15.75511328125L6.32927704375,15.75511328125C6.57865194375,15.75511328125,6.77902694375,15.99573828125,6.77902694375,16.29148928125L6.77902694375,17.89798928125L6.77990194375,17.89798928125C6.78077694375,18.19286428125,6.57865194375,18.43261528125,6.32927704375,18.43261528125ZM6.32927704375,14.68674028125L4.97827724375,14.68674028125C4.72890234375,14.68674028125,4.52852724375,14.44698928125,4.52852724375,14.15124028125L4.52852724375,12.54561528125C4.52852724375,12.25074008125,4.73065234375,12.01186558125,4.97827724375,12.01186558125L6.32927704375,12.01186558125C6.57865194375,12.01186558125,6.77902694375,12.25074008125,6.77902694375,12.54561528125L6.77902694375,14.15124028125L6.77990194375,14.15124028125C6.78077694375,14.44698928125,6.57865194375,14.68674028125,6.32927704375,14.68674028125ZM6.32927704375,10.93999008125L4.97827724375,10.93999008125C4.72890234375,10.93999008125,4.52852724375,10.70024018125,4.52852724375,10.40361498125L4.52852724375,8.79798988125C4.52852724375,8.50311518125,4.73065234375,8.26336478125,4.97827724375,8.26336478125L6.32927704375,8.26336478125C6.57865194375,8.26336478125,6.77902694375,8.50311518125,6.77902694375,8.79798988125L6.77902694375,10.40361498125L6.77990194375,10.40361498125C6.78077694375,10.70024018125,6.57865194375,10.93999008125,6.32927704375,10.93999008125ZM9.93252654375,17.89798928125C9.93252654375,18.19286428125,9.73127744375,18.43261528125,9.48277664375,18.43261528125L8.13090134375,18.43261528125C7.88152694375,18.43261528125,7.68027644375,18.19199028125,7.68027644375,17.89798928125L7.68027644375,16.29148928125C7.68027644375,15.99574028125,7.88152594375,15.75511328125,8.13090134375,15.75511328125L9.48190114375,15.75511328125C9.72952654375,15.75511328125,9.93165114375,15.99573828125,9.93165114375,16.29148928125L9.93165114375,17.89798928125L9.93252654375,17.89798928125ZM9.93252654375,14.15124028125C9.93252654375,14.44698928125,9.73127744375,14.68674028125,9.48277664375,14.68674028125L8.13090134375,14.68674028125C7.88152694375,14.68674028125,7.68027644375,14.44698928125,7.68027644375,14.15124028125L7.68027644375,12.54561528125C7.68027644375,12.25074008125,7.88152594375,12.01186558125,8.13090134375,12.01186558125L9.48190114375,12.01186558125C9.72952654375,12.01186558125,9.93165114375,12.25074008125,9.93165114375,12.54561528125L9.93165114375,14.15124028125L9.93252654375,14.15124028125ZM9.93252654375,10.40449048125C9.93252654375,10.70024108125,9.73127744375,10.94086548125,9.48277664375,10.94086548125L8.13090134375,10.94086548125C7.88152694375,10.94086548125,7.68027644375,10.70111558125,7.68027644375,10.40449048125L7.68027644375,8.79886528125C7.68027644375,8.50399068125,7.88152594375,8.26424028125,8.13090134375,8.26424028125L9.48190114375,8.26424028125C9.72952654375,8.26424028125,9.93165114375,8.50399068125,9.93165114375,8.79886528125L9.93165114375,10.40449048125L9.93252654375,10.40449048125ZM9.93252654375,6.65686558125L1.37590234375,6.65686558125L1.37590234375,3.98286338125L9.93165204375,3.98286338125L9.93165204375,6.65686318125L9.93252654375,6.65686558125ZM23.27452634375,4.50086328125C20.71282234375,4.50086328125,18.63615234375,6.57753468125,18.63615234375,9.13923788125C18.63615234375,11.70094208125,20.71282234375,13.77761328125,23.27452634375,13.77761328125C25.83623134375,13.77761328125,27.91290234375,11.70094208125,27.91290234375,9.13923788125C27.91290234375,6.57753468125,25.83623134375,4.50086328125,23.27452634375,4.50086328125ZM26.45340134375,8.56961348125C26.45340134375,8.606363281250001,26.41489934375,8.64398858125,26.37815134375,8.64398858125L25.85314934375,8.64398858125C25.81552534375,8.64398858125,25.77702534375,8.68161388125,25.74027634375,8.719239281250001L25.28965234375,9.20573898125L25.28965234375,9.317738981249999L25.66502734375,10.06761458125C25.70177634375,10.10436438125,25.74027634375,10.14198968125,25.77702734375,10.14198968125L26.00190134375,10.14198968125C26.04236234375,10.14420228125,26.07453734375,10.176757381249999,26.07627634375,10.21723988125L26.07627634375,10.81748958125C26.07627634375,10.85423948125,26.03952634375,10.89186478125,26.00190134375,10.89186478125L25.40252734375,10.89186478125C25.36577834375,10.89186478125,25.28965234375,10.85423948125,25.28965234375,10.81748958125L24.80140334375,9.842739581250001C24.80140334375,9.805114281249999,24.76465234375,9.805114281249999,24.72702834375,9.842739581250001L23.86515234375,10.81748958125C23.86515234375,10.85423948125,23.78990134375,10.89186478125,23.75227734375,10.89186478125L23.15290234375,10.89186478125C23.11527834375,10.89186478125,23.07852934375,10.85423948125,23.07852934375,10.81748958125L23.07852934375,10.21723988125C23.07852934375,10.17961458125,23.11527834375,10.14198968125,23.15290234375,10.14198968125L23.67790434375,10.14198968125C23.71640434375,10.14198968125,23.75227734375,10.10436438125,23.78990334375,10.10436438125L24.46452934375,9.317738981249999L24.46452934375,9.20573898125L24.24052834375,8.719239281250001C24.20202834375,8.68248938125,24.16527734375,8.64398858125,24.12677934375,8.64398858125L23.52565334375,8.64398858125C23.48890534375,8.64398858125,23.45215434375,8.606363281250001,23.45215434375,8.56961348125C23.45215434375,8.606363281250001,23.41452934375,8.64398858125,23.37690334375,8.64398858125L22.51590534375,8.64398858125L21.95152834375,10.89273838125C21.87890434375,11.26723858125,21.76602934375,11.60411358125,21.57703034375,11.79223828125C21.42828034375,11.97948838125,21.12903034375,12.01711368125,20.82802934375,12.01711368125L20.45352934375,12.01711368125L20.45352934375,11.26723768125L20.82802934375,11.26723768125C20.82802934375,11.26723768125,21.12990334375,11.34161278125,21.24190334375,10.89273738125L21.80365334375,8.64398768125L20.90502934375,8.64398768125C20.86652734375,8.64398768125,20.82890334375,8.606362781249999,20.82890334375,8.56961298125L20.82890334375,7.97023778125C20.82890334375,7.93261238125,20.86652734375,7.89498708125,20.90502934375,7.89498708125L21.99177934375,7.89498708125L22.17902934375,7.14598708125C22.29102934375,6.77061228125,22.47915434375,6.54573728125,22.66552934375,6.39611248125C23.37777934375,5.79586268125,24.20290334375,6.05836198125,24.57827934375,6.13361268125L24.57827934375,6.77061228125C24.20290334375,6.69711258125,23.64115534375,6.50811198125,23.22815534375,6.80823758125C23.11615534375,6.88348768125,22.92890534375,7.07161238125,22.89128134375,7.18361238125L22.70490634375,7.89498708125L23.37865634375,7.89498708125C23.41628034375,7.89498708125,23.45390734375,7.93261238125,23.45390734375,7.97023778125C23.45390734375,7.93261238125,23.49065634375,7.89498708125,23.52740634375,7.89498708125L24.50303034375,7.89498708125C24.54153034375,7.89498708125,24.57828134375,7.93261238125,24.61590534375,7.97023778125L24.95278134375,8.64486218125C24.95278134375,8.68248748125,24.99040634375,8.68248748125,25.02803034375,8.64486218125L25.66590734375,7.97023778125C25.66590734375,7.93261238125,25.74115734375,7.89498708125,25.77790634375,7.89498708125L26.37903234375,7.89498708125C26.41578134375,7.89498708125,26.45428234375,7.93261238125,26.45428234375,7.97023778125L26.45428234375,8.56961298125L26.45340134375,8.56961348125Z" fill="#FFDB42" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/bom.svg b/src/static/images/icon/bom.svg
new file mode 100644
index 0000000..42a7eca
--- /dev/null
+++ b/src/static/images/icon/bom.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_137_41249"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_137_41249)"><path d="M25,21.1513879C25,21.3787005,25.067284852,21.6009274,25.19337524,21.7900629L27.1094005,24.6641006C27.6657977,25.4986968,28.6024919,26,29.6055508,26L37,26C38.656855,26,40,24.656854199999998,40,23C40,21.343146,38.656855,20,37,20L26.1513879,20C25.51549393,20,25,20.51549393,25,21.1513879Z" fill="#FFDB42" fill-opacity="1" transform="matrix(-1,0,0,1,50,0)"/><rect x="2" y="2" width="25" height="21" rx="6" fill="#FF7B00" fill-opacity="1" style="opacity:0.800000011920929;"/><g><path d="M4.57750022,9.7000003L7.2685001,9.7000003Q8.5195003,9.7000003,9.145,10.037500399999999Q9.7705002,10.3750005,9.7705002,11.1760001Q9.7705002,12.4090004,8.285499999999999,12.6520004L8.285499999999999,12.6700001Q10.085499800000001,12.8950005,10.085499800000001,14.2360001Q10.085499800000001,15.217,9.3474998,15.6084995Q8.6094999,16,7.2235,16L4.57750022,16L4.57750022,15.8290005L5.1625001,15.8290005L5.1625001,9.8710003L4.57750022,9.8710003L4.57750022,9.7000003ZM6.9175,12.814000100000001L6.9175,15.8290005L7.3405001,15.8290005Q7.8355,15.8290005,8.0334997,15.4870005Q8.2315001,15.1450005,8.2315001,14.4790001L8.2315001,14.001999900000001Q8.2315001,13.408000000000001,7.9885000999999995,13.1110001Q7.7455001,12.814000100000001,7.124499999999999,12.814000100000001L6.9175,12.814000100000001ZM6.9175,9.8710003L6.9175,12.6430001L7.1515,12.6430001Q7.6195002,12.6430001,7.8040001,12.3594999Q7.9885000999999995,12.0760002,7.9885000999999995,11.4910002L7.9885000999999995,11.0680003Q7.9885000999999995,10.4020004,7.8265002,10.1365004Q7.6645,9.8710003,7.2055001,9.8710003L6.9175,9.8710003ZM14.8285,13.2189999L14.8285,12.5530005Q14.8285,10.9060001,14.6665,10.474000499999999Q14.4865,9.9790001,14.1535,9.8620005Q13.9644995,9.7900004,13.6945,9.7900004Q13.4244995,9.7900004,13.2309999,9.8620005Q13.0375004,9.9340005,12.9115,10.1140003Q12.7854996,10.2940001,12.7089996,10.4920001Q12.6324997,10.690000099999999,12.5874996,11.0500002Q12.5334997,11.572,12.5334997,12.589000200000001L12.5334997,13.2370005Q12.5334997,14.335,12.6189995,14.7985001Q12.7045002,15.2620001,12.8304996,15.46Q13.1094999,15.901,13.6945,15.901Q14.4055,15.901,14.617,15.3024998Q14.8285,14.704,14.8285,13.2189999ZM13.6765003,16.0900002Q12.1735001,16.0900002,11.4309998,15.2755003Q10.6884999,14.461,10.6884999,12.9175Q10.6884999,11.3740001,11.4849997,10.4875002Q12.2814999,9.601000299999999,13.7395,9.601000299999999Q15.1975,9.601000299999999,15.94,10.4425001Q16.682499999999997,11.2840004,16.682499999999997,12.8320003Q16.682499999999997,14.3800001,15.931,15.2349997Q15.1795,16.0900002,13.6765003,16.0900002ZM21.9835,9.7000003L24.386499,9.7000003L24.386499,9.8710003L23.8915,9.8710003L23.8915,15.8290005L24.386499,15.8290005L24.386499,16L21.6415,16L21.6415,15.8290005L22.136499,15.8290005L22.136499,10.2040005L22.1005,10.2040005L20.7505,16L19.625500000000002,16L18.0415,10.177L18.005499999999998,10.177L18.005499999999998,15.8290005L18.500500000000002,15.8290005L18.500500000000002,16L17.276501,16L17.276501,15.8290005L17.771501,15.8290005L17.771501,9.8710003L17.276501,9.8710003L17.276501,9.7000003L19.7065,9.7000003L20.9035,14.335L21.9835,9.7000003Z" fill="#FFFFFF" fill-opacity="1"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/gongxuguanli.svg b/src/static/images/icon/gongxuguanli.svg
new file mode 100644
index 0000000..f7bdcb4
--- /dev/null
+++ b/src/static/images/icon/gongxuguanli.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_137_41304"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_137_41307"><rect x="8" y="7" width="12" height="12" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_137_41304)"><path d="M22,22.0000005L22,22.9999995C22,25.2091389,23.7908611,27,26,27L34,27C36.209139,27,38,25.2091389,38,22.9999995L38,22.0000005C38,20.3431458,36.656855,19,35,19L25.0000005,19C23.3431458,19,22,20.3431458,22,22.0000005Z" fill="#FDC3C1" fill-opacity="1" transform="matrix(-1,0,0,1,44,0)"/><rect x="2" y="2" width="24" height="22" rx="6" fill="#FF2B00" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_137_41307)"><path d="M12.203554171875,9.8023956C12.203554171875,8.6526947,11.269422071874999,7.7185629,10.119721371875,7.7185629C8.970020531875,7.7185629,8.035888671875,8.6526947,8.035888671875,9.8023956C8.035888671875,10.3772457,8.251457571875,10.8802395,8.610738931875,11.239521C8.538882671875,11.6706586,8.467026411875,12.101797099999999,8.467026411875,12.5329351C8.467026411875,14.5449114,9.544870571875,16.2694607,11.053852571875,17.203594000000002L11.484991071875001,15.4790421C10.622715271875,14.760479,10.119721371875,13.682635300000001,10.119721371875,12.5329351C10.119721371875,12.317365599999999,10.119721371875,12.101797099999999,10.191577471875,11.886228599999999C11.269422071874999,11.886228599999999,12.203554671875,10.952096000000001,12.203554171875,9.8023956ZM13.999961371875,8.6526947C14.718523971875001,8.6526947,15.365230071875,8.8682635,15.940080671875,9.1556888C15.868224171875,9.3712578,15.796367671875,9.6586823,15.796367671875,9.874251600000001C15.796367671875,11.023952000000001,16.730499271875,11.9580836,17.880200371875,11.9580836C19.029902671875,11.9580836,19.964033671875,11.023952000000001,19.964033671875,9.874251600000001C19.964033671875,8.7245512,19.029902671875,7.7904191,17.880200371875,7.7904191C17.592776271875,7.7904191,17.377205871875,7.86227548,17.089782671875,7.93413174C16.227506671875,7.35928145,15.149661071875,7,13.999961371875,7C13.209542271875,7,12.419122671875,7.14371257,11.772416071875,7.502994L12.706547771875,8.8682635C13.137685771874999,8.724551,13.568823771875,8.6526947,13.999961371875,8.6526947ZM17.880200371875,12.6766467C17.808345771875,14.1137724,17.017926171875,15.335331,15.796367671875,15.910181C15.437087571875,15.2634745,14.718522571874999,14.760479,13.928104871875,14.760479C12.778404271875,14.760479,11.844271871875,15.6946106,11.844271871875,16.8443117C11.844271871875,17.994014,12.778404271875,18.928145,13.928104871875,18.928145C14.790379971875,18.928145,15.508942571875,18.425150000000002,15.868224171875,17.634729999999998C18.023912471875,16.8443117,19.532894671875,14.8323355,19.532894671875,12.4610786L19.532894671875,12.101797099999999C19.461038671875002,12.1736536,17.880200371875,12.6766467,17.880200371875,12.6766467Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/gongyiluxian.svg b/src/static/images/icon/gongyiluxian.svg
new file mode 100644
index 0000000..5b03c56
--- /dev/null
+++ b/src/static/images/icon/gongyiluxian.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_137_41224"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_137_41229"><rect x="8" y="6" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_137_41224)"><g transform="matrix(0,1.0000003576278687,-1.0000003576278687,0,29.00000089406967,-24.00000947713852)"><path d="M33.103223299999996,27.5C31.7891606,27.5,30.83216852,28.7456321,31.17075348,30.0153255L31.7081735,32.0306506C32.1751106,33.7816644,33.76091,35,35.5731134,35L41.426888,35C43.239091,35,44.824889,33.7816644,45.291826,32.0306506L45.829247,30.0153253C46.167831,28.7456321,45.210839,27.5,43.896776,27.5L33.103223299999996,27.5Z" fill="#69FFB4" fill-opacity="1" transform="matrix(1,0,0,-1,0,55)"/><path d="M26.5,8.500001000000001L26.5,17.999999000000003C26.5,21.313709,29.186291699999998,24,32.5000005,24L44.5,24C47.813709,24,50.5,21.313709,50.5,18L50.5,8.500001000000001C50.5,5.1862917,47.813709,2.5,44.5,2.5L32.500001,2.5C29.186291699999998,2.5,26.5,5.1862917,26.5,8.500001000000001Z" fill="#01BF34" fill-opacity="1" style="opacity:0.800000011920929;"/></g><g clip-path="url(#master_svg1_137_41229)"><path d="M20.366401,15.224C20.163159,15.224,19.9984,15.0592403,19.9984,14.8559999C19.9984,14.6527596,20.163159,14.4879999,20.366401,14.4879999C21.5888,14.4879999,23.014400000000002,13.776,23.014400000000002,11.7712002C23.014400000000002,9.7664003,21.4848,9.0544002,20.1744,9.0544002L13.0032001,9.0544002C12.7999592,9.0544002,12.635200000000001,8.889641,12.635200000000001,8.6864004C12.635200000000001,8.4831595,12.7999592,8.3184004,13.0032001,8.3184001L20.176001,8.3184001C21.139200000000002,8.3184001,22.020801,8.6272001,22.659201,9.1872001C23.372801000000003,9.8127999,23.750401,10.7055998,23.750401,11.7712002C23.750401,12.870400400000001,23.3744,13.7807999,22.664001,14.4032001C22.0592,14.9312,21.2432,15.224,20.366401,15.224ZM12.740799899999999,20.4032C11.7791998,20.4032,10.8992,20.096,10.2607999,19.536C9.54719996,18.9104,9.16959992,18.016,9.16959992,16.9488001C9.16959992,15.8495998,9.54559994,14.9392004,10.255999899999999,14.3168001C10.8607998,13.7872,11.6767998,13.4960003,12.5535998,13.4960003C12.7568407,13.4960003,12.9215999,13.6607594,12.9215999,13.8640003C12.9215999,14.0672407,12.7568407,14.2320004,12.5535998,14.2320004C11.3311999,14.2320004,9.90559983,14.9440002,9.90559983,16.9488001C9.90559983,17.796799999999998,10.1967999,18.500799999999998,10.7455997,18.9824C11.249599700000001,19.424,11.9583998,19.6672,12.7423997,19.6672L20.172800000000002,19.6672C20.37604,19.6672,20.540799,19.831960000000002,20.540799,20.0352C20.540799,20.238441,20.37604,20.4032,20.172800000000002,20.4032L12.740799899999999,20.4032ZM11.1775999,11.355199800000001C9.97599995,11.355199800000001,9,10.3792,9,9.1775999C9,7.97759986,9.97599995,7,11.1775999,7C11.7584,7,12.3055999,7.22720003,12.7167997,7.63840008C13.127999800000001,8.0496001,13.355199800000001,8.5968001,13.355199800000001,9.1775999C13.3536,10.3792,12.377599700000001,11.355199800000001,11.1775999,11.355199800000001ZM11.1775999,7.73599982C10.3823999,7.73599982,9.73599994,8.3823998,9.73599994,9.1775999C9.73599994,9.9728,10.3823999,10.6191998,11.1775999,10.6191998C11.9727998,10.6191998,12.6191998,9.9727998,12.6191998,9.1775999C12.6191998,8.7919998,12.4687998,8.4303999,12.1967998,8.1584001C11.9269657,7.8873536600000005,11.5600605,7.73529232,11.1775999,7.73599982ZM21.744,21.3536C22.744,21.3536,23.5536,20.544,23.5536,19.544C23.5536,18.544,22.744,17.7344,21.744,17.7344C20.744,17.7344,19.934401,18.544,19.934401,19.544C19.934401,20.023999,20.124800999999998,20.484799000000002,20.464001,20.823999C20.803201,21.161599000000002,21.264001,21.3536,21.744,21.3536ZM18.9136009,14.352000199999999C18.9136009,14.1008005,19.068801,13.8752003,19.304001,13.7872L19.425601,13.740799899999999L19.392001,13.6143999C19.321601,13.3392,19.212801,13.076799900000001,19.068801,12.8319998L18.998400699999998,12.7199998L18.8800011,12.7728C18.6508932,12.8759217,18.3818769,12.826487499999999,18.204396199999998,12.6486521C18.0269165,12.4708161,17.978021599999998,12.2017016,18.0816011,11.9727998L18.1344013,11.8543997L18.0224009,11.785599699999999C17.7782354,11.641330199999999,17.514800100000002,11.532507899999999,17.2400007,11.4623995L17.1136007,11.430399399999999L17.0672007,11.5519996C16.9783568,11.7868605,16.7535033,11.9422832,16.5023999,11.9423995C16.2511997,11.9423995,16.0271997,11.7871995,15.9375997,11.5519996L15.8911996,11.430399399999999L15.7727995,11.4655995C15.497599600000001,11.5359993,15.2351995,11.6447997,14.990399400000001,11.7887993L14.8783994,11.859199499999999L14.9311996,11.9775996C15.0144258,12.1639524,14.9969354,12.379872800000001,14.8847995,12.550399800000001C14.722160800000001,12.7994199,14.4025073,12.8930764,14.1311994,12.7711997L14.012799300000001,12.7183995L13.9423995,12.8303995C13.796959900000001,13.0741673,13.6875596,13.337698,13.6175995,13.612799599999999L13.5855994,13.7391996L13.7071996,13.785599699999999C13.9423995,13.8751998,14.097599500000001,14.0991993,14.097599500000001,14.3504C14.097599500000001,14.601599700000001,13.9423995,14.8255997,13.7071996,14.915200200000001L13.5855994,14.9616003L13.6175995,15.0880003C13.6879992,15.3632002,13.796799700000001,15.6255999,13.9407992,15.8704004L14.0111995,15.9824009L14.129599599999999,15.9296007C14.358708400000001,15.826478,14.6277251,15.8759127,14.8052053,16.0537481C14.9826856,16.2315845,15.0315804,16.500700000000002,14.9279995,16.7296009L14.8751993,16.848000499999998L14.9871993,16.9184008C15.2319994,17.060800999999998,15.495999300000001,17.168001,15.7695994,17.236801L15.895999400000001,17.268801L15.9423995,17.147201000000003C16.0312433,16.9123402,16.2560968,16.756917,16.507199800000002,16.7568007C16.7583995,16.7568007,16.9823995,16.9120007,17.0720005,17.147201000000003L17.1184006,17.268801L17.244800599999998,17.236801C17.520000500000002,17.166401,17.7824001,17.057601,18.0272007,16.9136009L18.139201200000002,16.8448009L18.086401000000002,16.7264013C18.0016012,16.5392017,18.017601,16.3232012,18.129600500000002,16.1504011C18.2416,15.9776011,18.432000199999997,15.873601,18.6368008,15.8752012C18.7216005,15.8752012,18.8064003,15.8928013,18.884800900000002,15.9280014L19.003201,15.9808016L19.073601,15.8688011C19.216001,15.6256008,19.321601,15.3648014,19.390401,15.0928011L19.4224,14.966401099999999L19.300800000000002,14.920001C19.066637999999998,14.8290253,18.9127045,14.6032133,18.9136009,14.352000199999999ZM18.184001000000002,15.3168001C18.1280012,15.3008003,18.0720005,15.2911997,18.0128012,15.2911997C17.841601400000002,15.2911997,17.678401,15.3647995,17.564801199999998,15.4911995C17.400196100000002,15.6712322,17.3392162,15.9230013,17.4032011,16.1583996C17.2944012,16.2207994,17.1792011,16.2719994,17.0608015,16.3119993C16.9594064,16.099060100000003,16.7446485,15.9633579,16.508801,15.9631996C16.2736011,15.9631996,16.057601,16.099199300000002,15.956800900000001,16.3119993C15.836801099999999,16.2735996,15.7232008,16.2223997,15.6144009,16.1583996C15.6752009,15.9231997,15.6144009,15.6735992,15.4528008,15.4927998C15.3391409,15.3667078,15.1777534,15.2941418,15.008000899999999,15.2927999C14.9504008,15.2927999,14.8928008,15.3008003,14.836801099999999,15.3184004C14.779201,15.2000008,14.732801,15.0768003,14.6976008,14.9488001C14.8990641,14.8178301,15.020664700000001,14.593893099999999,15.0208011,14.353600499999999C15.0208011,14.1136007,14.8992009,13.8896008,14.6976008,13.7584004C14.732801,13.6304007,14.779201,13.5072002,14.836801099999999,13.3888006C14.8928013,13.4048004,14.948801,13.4144006,15.008000899999999,13.4144006C15.1776009,13.4128008,15.339201,13.342400600000001,15.4528008,13.216000600000001C15.6174059,13.0359683,15.6783862,12.7841992,15.6144009,12.548800499999999C15.7232008,12.4864006,15.838400799999999,12.4352007,15.956800900000001,12.395200299999999C16.0581956,12.6081395,16.2729535,12.7438416,16.508801,12.744000400000001C16.7440009,12.744000400000001,16.9600005,12.6080003,17.0608015,12.395200299999999C17.1808014,12.4336004,17.2944012,12.486400100000001,17.4032011,12.548800499999999C17.3424015,12.7840004,17.4032011,13.0352006,17.566401499999998,13.216000600000001C17.6800013,13.342400600000001,17.8432016,13.4160004,18.0144014,13.4160004C18.0720015,13.4160004,18.1296015,13.4080005,18.1856012,13.3904004C18.243201300000003,13.5088005,18.2896013,13.632000399999999,18.3248014,13.7600002C18.1237259,13.8911381,18.0026922,14.1151409,18.0032015,14.355199800000001C18.0032015,14.5951996,18.124801599999998,14.819199600000001,18.3248014,14.9503994C18.288001100000002,15.0767994,18.241601,15.1999998,18.184001000000002,15.3168001ZM16.507199800000002,13.452799800000001C16.0112,13.452799800000001,15.6079998,13.8559999,15.6079998,14.352000199999999C15.6079998,14.848000500000001,16.0112,15.2512007,16.507199800000002,15.2512007C17.0032005,15.2512007,17.4064007,14.848000500000001,17.4064007,14.352000199999999C17.4064007,13.8559999,17.0032005,13.4544001,16.507199800000002,13.452799800000001Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="" fill="#FFFFFF" fill-opacity="1"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/guihuandengji.svg b/src/static/images/icon/guihuandengji.svg
new file mode 100644
index 0000000..5993654
--- /dev/null
+++ b/src/static/images/icon/guihuandengji.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_137_41279"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_137_41281"><rect x="8" y="6" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_137_41279)"><path d="M1,18C1,20.209139,2.7908611,22,5.000001,22L6.9999995,22C8.6568542,22,10,20.656855,10,19L10,9.0000005C10,7.3431458,8.6568542,6,6.9999995,6L5.0000005,6C2.7908611,6,1,7.7908611,1,10L1,18Z" fill="#FC99E5" fill-opacity="1"/><rect x="5" y="2" width="22" height="24" rx="6" fill="#815DFA" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_137_41281)"><path d="M16.679638871875,20.4578125L16.679638871875,7.9312499800000005L17.743700971875,7.24531245C17.842138271875,7.174999952,17.964013071875,7.1328125,18.093701371875,7.1328125L22.421826171875,7.1328125C22.960889171875,7.151562452,23.392138171875,7.59375,23.392138171875,8.1375L23.392138171875,19.0671875C23.364014171875,19.5640625,22.976514171875,19.9640625,22.485888171875,20.0109375L18.234326371875,20.0109375L16.584326771875,20.9640625C16.434327171875,21.0359375,16.271826771875,21.0671875,16.109326371875,21.0609375C16.428076771875,21.0437505,16.679638871875,20.7812505,16.679638871875,20.4578125ZM15.396826271875,20.4578125L15.396826271875,7.9312499800000005L14.332763671875,7.24531245C14.234326371875,7.174999952,14.112451071875,7.1328125,13.982763771875,7.1328125L9.654638651875,7.1328125C9.115576151875,7.151562452,8.684326171875,7.59375,8.684326171875,8.1375L8.684326171875,19.0671875C8.712451159875,19.5640625,9.099951151875,19.9640625,9.590576171875,20.0109375L13.843701371875,20.0109375L15.493701471875,20.9640625C15.643701571874999,21.0359375,15.806201471875,21.0671875,15.968701371875,21.0609375C15.649951471875,21.0437505,15.396826271875,20.7812505,15.396826271875,20.4578125Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M14.1343751,10.83447265625C14.1343751,10.48134755625,13.8484378,10.19384765625,13.4937501,10.19384765625L12.1171875,10.19384765625C11.7640624,10.19384765625,11.4765625,10.47978496625,11.4765625,10.83447265625C11.4765625,11.18759775625,11.76250005,11.47509765625,12.1171875,11.47509765625L13.4937501,11.47509765625C13.8468752,11.47353505625,14.1343751,11.18759775625,14.1343751,10.83447265625ZM14.1343751,13.69384765625C14.1343751,13.34072255625,13.8484378,13.05322265625,13.4937501,13.05322265625L11.8046875,13.05322265625C11.4515624,13.05322265625,11.1640625,13.33915995625,11.1640625,13.69384765625C11.1640625,14.04697275625,11.45000005,14.334473156249999,11.8046875,14.334473156249999L13.4937501,14.334473156249999C13.8468752,14.332911056250001,14.1343751,14.04697275625,14.1343751,13.69384765625ZM14.1343751,16.55322315625C14.1343751,16.20009855625,13.8484378,15.912598156249999,13.4937501,15.912598156249999L12.2734375,15.912598156249999C11.9203124,15.912598156249999,11.6328125,16.19853545625,11.6328125,16.55322315625C11.6328125,16.90634775625,11.91875005,17.19384815625,12.2734375,17.19384815625L13.4937501,17.19384815625C13.8468752,17.19384815625,14.1343751,16.90634775625,14.1343751,16.55322315625Z" fill="#815DFA" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/jichucanshu.svg b/src/static/images/icon/jichucanshu.svg
new file mode 100644
index 0000000..b184c59
--- /dev/null
+++ b/src/static/images/icon/jichucanshu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_137_41236"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_137_41241"><rect x="7" y="6" width="15" height="14" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_137_41236)"><path d="M25,21.1513879C25,21.3787005,25.067284852,21.6009274,25.19337524,21.7900629L27.1094005,24.6641006C27.6657977,25.4986968,28.6024919,26,29.6055508,26L37,26C38.656855,26,40,24.656854199999998,40,23C40,21.343146,38.656855,20,37,20L26.1513879,20C25.51549393,20,25,20.51549393,25,21.1513879Z" fill="#FFDB42" fill-opacity="1" transform="matrix(-1,0,0,1,50,0)"/><rect x="2" y="2" width="25" height="21" rx="6" fill="#FF7B00" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_137_41241)"><path d="M9.678466799999999,19L19.324707,19C20.797660999999998,18.995276,21.991028,17.829696,21.997802999999998,16.38916L21.997847999999998,8.619499900000001C21.995915,7.1755933,20.801091,6.004747971,19.324704,6L9.6784396,6C8.1999841,6.0019013807,7.0019443023,7.173578,7,8.619499900000001L7,16.38916C7.0067913504,17.83171,8.2034429,18.998117,9.678466799999999,19ZM8.2226562,16.38916L8.2227658,8.619499900000001C8.223738,7.8336385,8.874896,7.1968093,9.6784396,7.1958585L19.324704,7.1958585C20.128249,7.1968096,20.779405,7.8336387,20.780375,8.619499900000001L20.780273,16.38916C20.779304,17.17502,20.128252,17.811790000000002,19.324707,17.812744000000002L9.678466799999999,17.812744000000002C8.8749235,17.811790000000002,8.223628399999999,17.17502,8.2226562,16.38916Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M10.4777521,10.089073248C10.3469837,10.30475119,10.1729468,10.49624744,9.96464378,10.65365517C9.70487261,10.86177546,9.41337633,11.0346493,9.099201024,11.1669112L9,11.2089053L9,12.0736644L9.23431939,11.9990091C9.47174489,11.9133329,9.7006349,11.8092618,9.91846389,11.6879447C10.0381892,11.6241767,10.1476524,11.5588531,10.2468534,11.4950846L10.2468534,14.9121275L11.2063658,14.9121275L11.2063658,10.0019750439L10.5256423,10.0019750439L10.4777521,10.089073248ZM13.7873006,14.0209274C13.886501299999999,13.9260538,14.129372100000001,13.736305,14.4885483,13.4563463C14.8311844,13.2016056,15.1556678,12.9273148,15.4600329,12.6351361C15.6386623,12.4559331,15.784215,12.2516172,15.8910446,12.0301166C15.9808555,11.839932300000001,16.0273976,11.6351838,16.0278735,11.4282067C16.0362639,11.0429803,15.8632469,10.67284936,15.552392000000001,10.41102597C15.2064652,10.12802772,14.7514644,9.980976105,14.286725,10.0019761715C13.8312612,9.982462278,13.384000799999999,10.11711118,13.031321,10.37991947C12.697223900000001,10.66255075,12.4998209,11.0540031,12.4822946,11.468645L12.4669011,11.6241771L13.4572001,11.7174965L13.4572001,11.5464114C13.4430242,11.3382672,13.5226898,11.1338509,13.6778364,10.980274080000001C13.8367767,10.84198374,14.051708699999999,10.76988751,14.271332300000001,10.78119278C14.4805198,10.77137673,14.6852269,10.83866006,14.8391714,10.96783167C14.9768214,11.0840187,15.0533643,11.2481257,15.0495462,11.4188752C15.033392899999999,11.6317267,14.944266299999999,11.8346176,14.7947035,11.9990091C14.4746122,12.3486953,14.112628,12.664825,13.7154651,12.9415345C13.4162827,13.1567047,13.138164,13.395041899999999,12.8842304,13.653872C12.6985469,13.8477199,12.551005400000001,14.0692272,12.4480877,14.3086624C12.3868821,14.4523063,12.3583713,14.6057506,12.3642807,14.7597065L12.3642807,14.9152384L16.0347157,14.9152384L16.0347157,14.0598116L13.7359896,14.0598116L13.7873006,14.0209274ZM19.71712,12.5698113C19.601044,12.4313142,19.451921,12.3185742,19.280981,12.2400832C19.386072,12.1649587,19.476803,12.0745716,19.549506,11.9725679C19.683591,11.7880906,19.754429000000002,11.5715657,19.75304,11.3504392C19.752536,11.1138016,19.680339,10.88186604,19.544376,10.6800952C19.400192,10.46579963,19.191989,10.29319018,18.9440393,10.18239217C18.6848888,10.063231848,18.3980541,10.0024236778,18.107672700000002,10.0050853989C17.7032843,9.9939849926,17.3084183,10.1179171,16.9976482,10.35347748C16.687448,10.60396022,16.4845166,10.94603568,16.4246755,11.3193326L16.3938913,11.4748646L17.353403999999998,11.6303967L17.3807688,11.4748646C17.3993073,11.2775718,17.4895763,11.0915124,17.6373234,10.9460547C17.7680531,10.83463174,17.942295100000003,10.77528393,18.121356,10.78119051C18.2964592,10.77241272,18.4680042,10.82852536,18.5968361,10.93672252C18.7133751,11.0420121,18.775506,11.1872133,18.7678719,11.3364403C18.7851925,11.5141391,18.6938963,11.6861514,18.5301323,11.7843734C18.345854799999998,11.8907928,18.130917500000002,11.944935300000001,17.9126902,11.9399053L17.8323059,11.9399053L17.6612692,11.9227965L17.5500956,12.7968874L17.7963877,12.7377851C17.9239302,12.704607,18.0554733,12.6858013,18.1880589,12.6817935C18.406900399999998,12.6736355,18.6197557,12.7479084,18.7764254,12.8870962C18.9297466,13.0269716,19.012158,13.2185521,19.003902,13.415905500000001C19.009782,13.6304023,18.9167614,13.8373623,18.7473497,13.986708199999999C18.586937,14.1388984,18.363360399999998,14.2224822,18.131618500000002,14.2168956C17.9424763,14.223569399999999,17.758078599999997,14.1620855,17.6185102,14.0458102C17.4482222,13.878574799999999,17.3399401,13.6669579,17.3089342,13.440789L17.2764359,13.299254900000001L16.3357382,13.4127934L16.3528419,13.568325999999999C16.384346999999998,13.9641922,16.582900000000002,14.3336644,16.906997699999998,14.599504C17.241446500000002,14.8689198,17.6764793,15.0119162,18.123066899999998,14.9992218C18.618236500000002,15.0108728,19.098183,14.8430104,19.457145,14.5326242C19.807885,14.2425933,20.005249,13.8298612,19.999333999999998,13.3987994C20.009191,13.101628999999999,19.909892,12.8099413,19.71712,12.5698113Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/jieyuedengji.svg b/src/static/images/icon/jieyuedengji.svg
new file mode 100644
index 0000000..a8c248d
--- /dev/null
+++ b/src/static/images/icon/jieyuedengji.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_137_41267"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_137_41269"><rect x="9" y="7" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_137_41267)"><path d="M1,18C1,20.209139,2.7908611,22,5.000001,22L6.9999995,22C8.6568542,22,10,20.656855,10,19L10,9.0000005C10,7.3431458,8.6568542,6,6.9999995,6L5.0000005,6C2.7908611,6,1,7.7908611,1,10L1,18Z" fill="#7BF1FF" fill-opacity="1"/><rect x="5" y="2" width="22" height="24" rx="6" fill="#0066FF" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_137_41269)"><path d="M13.5,8L11.5,8C10.66999996,8,10,8.66999996,10,9.5L10,17.5C10,18.33,10.66999996,19,11.5,19L13.5,19C14.3299999,19,15,19.67,15,20.5C15,20.78,15.2199998,21,15.5,21C15.7800002,21,16,20.78,16,20.5L16,10.5C16,9.1199999,14.8800001,8,13.5,8ZM21.5,8L19.5,8C18.1199999,8,17,9.1199999,17,10.5L17,20.5C17,20.78,17.2200003,21,17.5,21C17.7799997,21,18,20.78,18,20.5C18,19.67,18.6700001,19,19.5,19L21.5,19C22.33,19,23,18.33,23,17.5L23,9.5C23,8.66999996,22.33,8,21.5,8Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/kucunguanli.svg b/src/static/images/icon/kucunguanli.svg
new file mode 100644
index 0000000..19f79e0
--- /dev/null
+++ b/src/static/images/icon/kucunguanli.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28.000038623820046" height="28.000038623820046" viewBox="0 0 28.000038623820046 28.000038623820046"><defs><clipPath id="master_svg0_137_41296"><rect x="0" y="28.000038623820046" width="28.000028610229492" height="28.000028610229492" rx="0"/></clipPath><clipPath id="master_svg1_137_41298"><rect x="25" y="35.000038623820046" width="15" height="15.000000953674316" rx="0"/></clipPath></defs><g transform="matrix(0,-1.0000003576278687,1.0000003576278687,0,-28.00004863741418,28.000038623820046)" clip-path="url(#master_svg0_137_41296)"><path d="M1,46.000038623820046C1,48.20917762382005,2.7908611,50.000038623820046,5.000001,50.000038623820046L6.9999995,50.000038623820046C8.6568542,50.000038623820046,10,48.65689362382005,10,47.000038623820046L10,37.000039123820045C10,35.34318442382005,8.6568542,34.000038623820046,6.9999995,34.000038623820046L5.0000005,34.000038623820046C2.7908611,34.000038623820046,1,35.79089972382005,1,38.000038623820046L1,46.000038623820046Z" fill="#7BF1FF" fill-opacity="1"/><rect x="5" y="30.000038623820046" width="22" height="24" rx="6" fill="#0066FF" fill-opacity="1" style="opacity:0.800000011920929;"/><g transform="matrix(0,0.9999996423721313,-0.9999996423721313,0,60.00002610683083,10.000047564516763)" clip-path="url(#master_svg1_137_41298)"><path d="M27.0000062114314,42.054564923820045C27.0000062114314,41.35603812382005,27.45221668,40.45351172382004,28.0130587,40.04109122382005C28.0130587,40.04109122382005,32.0526376,37.000038623820046,32.684216,37.000038623820046C33.315794,37.000038623820046,37.361689,40.04551222382005,37.361689,40.04551222382005C37.917479,40.45603852382005,38.368425,41.35414362382004,38.368425,42.05393362382004L38.368425,47.73498362382004C38.369824,48.43226062382004,37.805701,48.998645623820046,37.108425,49.000038623820046L28.260006,49.000038623820046C27.56244022,48.999341623820044,26.9979092889,48.43254862382005,27.0000062114314,47.73498362382004L27.0000062114314,42.054564923820045ZM29.5263219,41.42677502382005L29.5263219,42.678564023820044C29.5263219,43.036036523820044,29.8117952,43.31582642382005,30.1642168,43.31582642382005L31.4147429,43.31582642382005C31.7728481,43.31582642382005,32.0526376,43.03035302382005,32.0526376,42.67793322382005L32.0526376,41.427405823820045C32.0526376,41.06930062382005,31.767164700000002,40.789510923820046,31.4147429,40.789510923820046L30.1642172,40.789510923820046C29.8061118,40.789510923820046,29.5263224,41.07498402382004,29.5263224,41.427405823820045L29.5263219,41.42677502382005ZM34.128005,40.71688012382005L33.2437949,41.60172222382005C32.9911637,41.854353423820044,32.9943199,42.254774523820046,33.2437949,42.503616823820046L34.128005,43.387826423820044C34.3806367,43.64045762382005,34.7810588,43.63730092382005,35.0298996,43.387826423820044L35.9141111,42.503615423820044C36.166742299999996,42.25098422382005,36.1635847,41.85056402382005,35.9141111,41.60172172382005L35.0298996,40.717511623820045C34.7772684,40.464880223820046,34.3768468,40.468038123820044,34.128005,40.717511623820045L34.128005,40.71688012382005ZM29.5263219,45.21688032382005L29.5263219,46.468669423820046C29.5263219,46.826143723820046,29.8117952,47.105932623820046,30.1642168,47.105932623820046L31.4147429,47.105932623820046C31.7728481,47.105932623820046,32.0526376,46.82045892382005,32.0526376,46.46803902382005L32.0526376,45.21751262382004C32.0526376,44.859406923820046,31.767164700000002,44.57961702382005,31.4147429,44.57961702382005L30.1642172,44.57961702382005C29.8061118,44.57961702382005,29.5263224,44.86509232382004,29.5263224,45.21751262382004L29.5263219,45.21688032382005ZM33.3157954,45.21688032382005L33.3157954,46.468669423820046C33.3157954,46.826143723820046,33.6012692,47.105932623820046,33.9536896,47.105932623820046L35.204215,47.105932623820046C35.5623198,47.105932623820046,35.8421097,46.82045892382005,35.8421097,46.46803902382005L35.8421097,45.21751262382004C35.8421097,44.859406923820046,35.5566368,44.57961702382005,35.204215,44.57961702382005L33.9536896,44.57961702382005C33.595585299999996,44.57961702382005,33.3157954,44.86509232382004,33.3157954,45.21751262382004L33.3157954,45.21688032382005Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/shengchandingdan.svg b/src/static/images/icon/shengchandingdan.svg
new file mode 100644
index 0000000..3109429
--- /dev/null
+++ b/src/static/images/icon/shengchandingdan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34441"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34750"><rect x="7" y="5" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34441)"><g transform="matrix(1,3.7247229744963306e-9,-3.7247229744963306e-9,1,7.449445948992661e-9,-7.449445948992661e-9)"><path d="M8.6032233,27C7.2891606,27,6.33216852,28.2456321,6.67075348,29.5153255L7.2081735,31.5306506C7.6751106,33.2816644,9.260909999999999,34.5,11.0731134,34.5L16.926887999999998,34.5C18.739091000000002,34.5,20.324889,33.2816644,20.791826,31.5306506L21.329247000000002,29.5153253C21.667831,28.2456321,20.710839,27,19.396776,27L8.6032233,27Z" fill="#69FFB4" fill-opacity="1" transform="matrix(1,0,0,-1,0,54)"/><path d="M2,8.000001000000001L2,17.499999000000003C2,20.813709,4.6862917,23.5,8.000000499999999,23.5L20,23.5C23.313709,23.5,26,20.813709,26,17.5L26,8.000001000000001C26,4.6862917,23.313709,2,20,2L8.000001000000001,2C4.6862917,2,2,4.6862917,2,8.000001000000001Z" fill="#01BF34" fill-opacity="1" style="opacity:0.800000011920929;"/></g><g clip-path="url(#master_svg1_141_34750)"><path d="M10.5241249,13.861916515625001C10.4281249,13.975916815625,10.383124800000001,14.121916815625,10.3871248,14.299916315625001C10.3911247,14.477916715625,10.4491248,14.623916615625,10.5581248,14.737916015625C10.6671247,14.851916315625,10.8131247,14.908916515625,10.9961247,14.908916515625L12.3081245,14.908916515625C12.2621245,15.045916515625,12.235124599999999,15.177916515625,12.226124800000001,15.304916415625C12.2171249,15.432916615625,12.2171249,15.564916615625,12.226124800000001,15.700916315625C12.226124800000001,16.229916615625,12.2881246,16.728916015625,12.4111247,17.197916015624997C12.5341249,17.667916015625,12.705124900000001,18.102916015625,12.9241247,18.503916015625002L10.203125,18.503916015625002C9.7111249,18.485916015625,9.30112505,18.312916015625,8.97312498,17.983915015625C8.64512491,17.654915015625,8.472125053,17.245915015625002,8.453125,16.753916015625002L8.453125,8.263916015625C8.471125007,7.771915915625,8.64412498,7.359915975625,8.97312498,7.026916025625C9.30112505,6.693916085625,9.7111249,6.522916078525,10.203125,6.513916015625L17.8181248,6.513916015625C18.3101244,6.522915959325,18.722125,6.693916085625,19.055125,7.026916025625C19.388125000000002,7.359915975625,19.559125,7.771915915625,19.568126,8.263916015625L19.568126,10.533916015625C19.304126,10.405916215625,19.010126,10.314916115625,18.686126,10.260915715625C18.3621254,10.205915915624999,18.0461254,10.183915615625,17.736125899999998,10.192915915625L17.640126199999997,10.192915915625C17.5941267,10.101915815625,17.5331259,10.033916015625,17.455125799999998,9.987916015625C17.3771257,9.941916015625,17.293126100000002,9.919916115625,17.2021255,9.919916115625L10.818125,9.919916115625C10.6811249,9.919916115625,10.5601251,9.965916115625,10.456125,10.056916215625C10.351125,10.147916315625,10.299125,10.279916315625,10.299125,10.452916115625C10.299125,10.625916015625,10.351125,10.760916215624999,10.456125,10.855916015624999C10.561125,10.951916215625001,10.6821251,11.021915915625,10.818125,11.067915915625L14.851124800000001,11.067915915625C14.322125,11.368916015625,13.864124799999999,11.737916015625,13.477124700000001,12.174915815624999C13.0891247,12.612916015625,12.7871246,13.117916115625,12.5681248,13.692915915625L10.996125,13.692915915625C10.7771249,13.690916015625,10.6201251,13.747916215625,10.5241249,13.861916515625001ZM20.327126,13.061916315625C21.024124999999998,13.726916315625001,21.387126000000002,14.574916815625,21.414125,15.604916615625C21.387124999999997,16.643917015625,21.024124999999998,17.509916015625002,20.327126,18.202917015624998C19.630126,18.895918015625,18.766126,19.255917015625002,17.736125,19.282917015625C16.697125399999997,19.255917015625002,15.8311253,18.886917015625002,15.1381254,18.175917015625C14.4451256,17.464916015625,14.085125399999999,16.607917015625,14.0581255,15.605917015625C14.085125399999999,14.621916815625,14.4451256,13.784916915625,15.1381254,13.096917115625C15.8311253,12.408916915625,16.697125399999997,12.050917115625001,17.736125,12.023917215625C18.766125000000002,12.050917115625001,19.630125,12.396917315625,20.327126,13.061916315625ZM20.020125,16.671916015625C20.120126,16.571916015625,20.170125,16.452916115625,20.170125,16.316916515625C20.170125,16.180916815625,20.120125,16.059916515624998,20.020125,15.954916015625C19.920125,15.849916415625,19.801126,15.792916315625,19.665126,15.783916515625L17.8191261,15.783916515625L17.8191261,13.854915615625C17.8191261,13.717915515625,17.7661257,13.599915515625,17.6621265,13.499916115625C17.557127,13.399915715625,17.436126700000003,13.349916415625,17.3001261,13.349916415625C17.1641254,13.349916415625,17.0421257,13.399916615625,16.9381266,13.499916115625C16.833126999999998,13.599916415625,16.7761269,13.718915915625,16.767127000000002,13.854915615625L16.767127000000002,16.315916015625C16.7761269,16.452916115625,16.833126999999998,16.570916015625002,16.9381266,16.670916015625C17.043126100000002,16.770916015624998,17.1641264,16.825915015625,17.3001261,16.834915015625L19.665126,16.834915015625C19.801126,16.826915015624998,19.919126,16.771915015624998,20.020125,16.671916015625Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/shengchanhesuan.svg b/src/static/images/icon/shengchanhesuan.svg
new file mode 100644
index 0000000..ca37231
--- /dev/null
+++ b/src/static/images/icon/shengchanhesuan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34449"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34776"><rect x="6" y="5" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34449)"><path d="M8.6032233,27C7.2891606,27,6.33216852,28.2456321,6.67075348,29.5153255L7.2081735,31.5306506C7.6751106,33.2816644,9.260909999999999,34.5,11.0731134,34.5L16.926887999999998,34.5C18.739091000000002,34.5,20.324889,33.2816644,20.791826,31.5306506L21.329247000000002,29.5153253C21.667831,28.2456321,20.710839,27,19.396776,27L8.6032233,27Z" fill="#FC99E5" fill-opacity="1" transform="matrix(1,0,0,-1,0,54)"/><path d="M2,8.000001000000001L2,17.499999000000003C2,20.813709,4.6862917,23.5,8.000000499999999,23.5L20,23.5C23.313709,23.5,26,20.813709,26,17.5L26,8.000001000000001C26,4.6862917,23.313709,2,20,2L8.000001000000001,2C4.6862917,2,2,4.6862917,2,8.000001000000001Z" fill="#6234F9" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_34776)"><path d="M19.536021859374998,6.0341796875L15.876568759375,6.0341796875C15.067975059375,6.0341796875,14.412474659375,6.6896797375,14.412474659375,7.4982734875L14.412474659375,11.157726287500001C14.412474659375,11.9663200875,15.067975059375,12.621820487499999,15.876568759375,12.621820487499999L19.536021859374998,12.621820487499999C20.344615859375,12.621820487499999,21.000115859375,11.9663200875,21.000115859375,11.157726287500001L21.000115859375,7.4982890875C21.000131859375,6.6896796275,20.344632859375,6.03417956829071,19.536021859374998,6.0341796875ZM19.634476859375,9.7672736875L15.778131459375,9.7672736875C15.535570159375,9.7672736875,15.338896759375,9.5706016875,15.338896759375,9.3280391875C15.338896759375,9.0854770875,15.535568259375,8.888804887500001,15.778131459375,8.888804887500001L19.634475859375,8.888804887500001C19.877036859375,8.888804887500001,20.073709859375,9.0854770875,20.073709859375,9.3280391875C20.073693859375,9.5706016875,19.877037859375,9.7672736875,19.634476859375,9.7672736875ZM19.525537859375,13.4172105875L15.873585659375,13.4172105875C15.066647559375,13.4172105875,14.412490859375,14.0713681875,14.412490859375,14.8783044875L14.412490859375,18.5303366875C14.412490859375,19.3372726875,15.066647559375,19.9914306875,15.873585659375,19.9914306875L19.525553859375,19.9914306875C20.332491859375,19.9914306875,20.986648859375002,19.3372726875,20.986648859375002,18.5303366875L20.986648859375002,14.8783197875C20.986648859375002,14.0713672875,20.332491859375,13.4172105875,19.525537859375,13.4172105875ZM17.698413859375002,14.9017886875L17.699912859375,14.9017886875C17.941976859375,14.9017886875,18.138240859375,15.0980548875,18.138240859375,15.3401164875C18.138240859375,15.5821790875,17.941976859375,15.7784452875,17.699912859375,15.7784452875C17.457850859375,15.7784452875,17.260866859375,15.5821790875,17.260866859375,15.3401164875C17.260866859375,15.0980548875,17.456350859375,14.9017886875,17.698413859375002,14.9017886875ZM17.699912859375,18.526601687499998C17.457850859375,18.526601687499998,17.260866859375,18.3303366875,17.260866859375,18.0882736875C17.260866859375,17.846211687500002,17.456350859375,17.6499456875,17.698413859375002,17.6499456875L17.699912859375,17.6499456875C17.941976859375,17.6499456875,18.138240859375,17.846211687500002,18.138240859375,18.0882736875C18.138240859375,18.3303366875,17.941976859375,18.526601687499998,17.699912859375,18.526601687499998ZM19.623787859375,17.1426626875L15.775351559375,17.1426626875C15.533288959375,17.1426626875,15.337023759375,16.9463986875,15.337023759375,16.7043356875C15.337023759375,16.4622726875,15.533288959375,16.266008687499998,15.775351559375,16.266008687499998L19.623787859375,16.266008687499998C19.865851859375,16.266008687499998,20.062116859375,16.4622726875,20.062116859375,16.7043356875C20.062116859375,16.9463986875,19.865851859375,17.1426626875,19.623787859375,17.1426626875ZM12.146208759375,6.0341796875L8.492740259375001,6.0341796875C7.685443399375,6.0341796875,7.031005859375,6.6886171675,7.031005859375,7.4959140875L7.031005859375,11.1494612875C7.031005859375,11.9567579875,7.685443339375,12.6111950875,8.492740259375001,12.6111950875L12.146209259375,12.6111950875C12.953506459375,12.6111950875,13.607944059375,11.9567579875,13.607944059375,11.1494612875L13.607944059375,7.4959140875C13.607944059375,6.6886172275,12.953506459375,6.0341796875,12.146208759375,6.0341796875ZM12.244490159375001,9.7612421875L10.757990359375,9.7612421875L10.757990359375,11.2477421875C10.757990359375,11.4899139875,10.561646659375,11.6862578875,10.319474959375,11.6862578875C10.077303159374999,11.6862578875,9.880959259375,11.4899139875,9.880959259375,11.2477421875L9.880959259375,9.7612421875L8.394459259375001,9.7612421875C8.152287459375,9.7612421875,7.955943529375,9.5648979875,7.955943529375,9.3227262875C7.955943529375,9.0805546875,8.152287259375,8.884210787499999,8.394459259375001,8.884210787499999L9.880959259375,8.884210787499999L9.880959259375,7.3977107875C9.880959259375,7.1555389875,10.077303159374999,6.9591950775,10.319474959375,6.9591950775C10.561646659375,6.9591950775,10.757990359375,7.1555387875,10.757990359375,7.3977107875L10.757990359375,8.884210787499999L12.244490159375001,8.884210787499999C12.486661959374999,8.884210787499999,12.683006259375,9.0805546875,12.683006259375,9.3227262875C12.683022059375,9.5648979875,12.486661959374999,9.7612421875,12.244490159375001,9.7612421875ZM12.154397059375,13.4631013875L8.521380759374999,13.4631013875C7.718615169375,13.4631013875,7.067833904375,14.1138667875,7.067833904375,14.9166488875L7.067833904375,18.549820687500002C7.067833904375,19.3525866875,7.718615169375,20.003366687499998,8.521380759374999,20.003366687499998L12.154397059375,20.003366687499998C12.957162859375,20.003366687499998,13.607944059375,19.3525856875,13.607944059375,18.549820687500002L13.607944059375,14.9166488875C13.607944059375,14.1138820875,12.957162859375,13.4631013875,12.154397059375,13.4631013875ZM11.999740159375001,17.7785396875C12.170084459375001,17.9488036875,12.170084459375001,18.2248996875,11.999740159375001,18.3951646875C11.914646659375,18.4803366875,11.803005659375,18.5229136875,11.691427659375,18.5229136875C11.579850159374999,18.5229136875,11.468209259375,18.4803366875,11.383115259375,18.3951646875L10.337849859375,17.3498996875L9.292584159375,18.3951646875C9.207490659375,18.4803366875,9.095849559375,18.5229136875,8.984271759375,18.5229136875C8.872693759375,18.5229136875,8.761053059375,18.4803366875,8.675959359375,18.3951646875C8.505615459375,18.2248996875,8.505615459375,17.9488036875,8.675959359375,17.7785396875L9.721224759375,16.733273687500002L8.675959359375,15.6880092875C8.505615459375,15.5177430875,8.505615459375,15.2416486875,8.675959359375,15.0713843875C8.846162559375,14.9010400875,9.122381259375,14.9010400875,9.292584659375,15.0713843875L10.337850359375,16.1166486875L11.383115759375,15.0713843875C11.553318959375,14.9010400875,11.829538359375,14.9010400875,11.999741059375001,15.0713843875C12.170084959375,15.2416486875,12.170084959375,15.5177430875,11.999741059375001,15.6880092875L10.954475159375,16.733273687500002L11.999740159375001,17.7785396875Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/shengchanjihua.svg b/src/static/images/icon/shengchanjihua.svg
new file mode 100644
index 0000000..3d73a8c
--- /dev/null
+++ b/src/static/images/icon/shengchanjihua.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34460"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34770"><rect x="6.5" y="4" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34460)"><path d="M25,21.1513879C25,21.3787005,25.067284852,21.6009274,25.19337524,21.7900629L27.1094005,24.6641006C27.6657977,25.4986968,28.6024919,26,29.6055508,26L37,26C38.656855,26,40,24.656854199999998,40,23C40,21.343146,38.656855,20,37,20L26.1513879,20C25.51549393,20,25,20.51549393,25,21.1513879Z" fill="#FFDB42" fill-opacity="1" transform="matrix(-1,0,0,1,50,0)"/><rect x="2" y="2" width="25" height="21" rx="6" fill="#FF7B00" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_34770)"><path d="M19.6875,17.8359375L9.3125,17.8359375C8.59623432,17.8359375,8.015625,17.255328499999997,8.015625,16.5390625L8.015625,9.40625L20.984375,9.40625L20.984375,16.5390625C20.984375,17.255328499999997,20.403765999999997,17.8359375,19.6875,17.8359375ZM16.4453125,11.3515625C16.4453125,10.9934998,16.154999699999998,10.703125,15.796875,10.703125C15.7324848,10.703125,13.2675157,10.703125,13.203125,10.703125C12.8450623,10.703125,12.5546875,10.9934998,12.5546875,11.3515625C12.5546875,11.7096877,12.8450623,12,13.203125,12C13.2675157,12,13.4754062,12,13.5273438,12L14.9863281,12L13.203125,15.5664063C13.203125,15.7454996,13.348312400000001,15.890625,13.5273438,15.890625L14.1757812,15.890625C14.354875100000001,15.890625,14.5,15.7454996,14.5,15.5664063L16.4453125,11.6757813C16.4453125,11.6238437,16.4453125,11.4159532,16.4453125,11.3515625ZM8.015625,8.109375C8.015625,7.3931093,8.59623432,6.8125,9.3125,6.8125L10.6747971,6.8125C10.8093438,6.4365938,11.1598282,6.1640625,11.5820312,6.1640625C12.0042343,6.1640625,12.3547029,6.4365938,12.4892654,6.8125L16.5107498,6.8125C16.6452971,6.4365938,16.9957819,6.1640625,17.417984,6.1640625C17.8401871,6.1640625,18.190656,6.4365938,18.325219,6.8125L19.6875,6.8125C20.403765999999997,6.8125,20.984375,7.3931093,20.984375,8.109375L20.984375,8.7578125L8.015625,8.7578125L8.015625,8.109375ZM17.09375,8.109375L17.7421875,8.109375L17.7421875,6.8125L17.09375,6.8125L17.09375,8.109375ZM11.2578125,8.109375L11.90625,8.109375L11.90625,6.8125L11.2578125,6.8125L11.2578125,8.109375Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/shengchanpaichan.svg b/src/static/images/icon/shengchanpaichan.svg
new file mode 100644
index 0000000..06ee178
--- /dev/null
+++ b/src/static/images/icon/shengchanpaichan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_34407"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_34756"><rect x="9" y="7" width="14" height="14" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_34407)"><path d="M1,10L1,18C1,20.209139,2.7908611,22,5.000001,22L6.9999995,22C8.6568542,22,10,20.656855,10,19L10,9.0000005C10,7.3431458,8.6568542,6,6.9999995,6L5.0000005,6C2.7908611,6,1,7.7908611,1,10Z" fill="#7BF1FF" fill-opacity="1"/><rect x="5" y="2" width="22" height="24" rx="6" fill="#0066FF" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_34756)"><path d="M20.666666,7L11.3333335,7C10.0422223,7,9,8.0422223,9,9.3333335L9,18.666667C9,19.957777999999998,10.0422223,21,11.3333335,21L20.666667,21L19.888889,21L19.904445000000003,20.984445C20.106667,20.984445,20.308888,20.906667,20.464444999999998,20.758889L22.782222,18.441112C22.945556,18.277779000000002,23.015556,18.052222,23,17.834443999999998L23,9.3333335C23,8.0422223,21.957777999999998,7,20.666666,7ZM12.8888891,10.1111112L19.111111,10.1111112C19.538888999999998,10.1111112,19.888889,10.461111299999999,19.888889,10.8888891C19.888889,11.3166666,19.538888999999998,11.666667,19.111111,11.666667L12.8888891,11.666667C12.461111299999999,11.666667,12.1111114,11.3166666,12.1111114,10.8888891C12.1111114,10.4611115,12.4611115,10.1111114,12.8888891,10.1111112ZM12.1111112,14C12.1111112,13.572222199999999,12.461111299999999,13.2222223,12.8888891,13.2222223L19.111111,13.2222223C19.538888999999998,13.2222223,19.888889,13.572222199999999,19.888889,14C19.888889,14.427777800000001,19.538888999999998,14.777778099999999,19.111111,14.777778099999999L12.8888891,14.777778099999999C12.461111299999999,14.777778099999999,12.1111114,14.427778199999999,12.1111112,14ZM20.666666,17.888889L19.888888,17.888889L19.888888,18.666667C19.888888,19.094445,19.538888999999998,19.444446,19.11111,19.444446C18.6833315,19.444446,18.3333321,19.094445999999998,18.3333321,18.666667L18.3333321,17.111112C18.3333321,16.6833344,18.6833315,16.333334,19.11111,16.333334L20.666666,16.333334C21.094443,16.333334,21.444444,16.683333400000002,21.444444,17.111112C21.444444,17.538890000000002,21.094444,17.88889,20.666666,17.888889Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/shengchanshikuang.svg b/src/static/images/icon/shengchanshikuang.svg
new file mode 100644
index 0000000..b97e4f1
--- /dev/null
+++ b/src/static/images/icon/shengchanshikuang.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_42939"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_42954"><rect x="6" y="5" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_42939)"><path d="M22,22.0000005L22,22.9999995C22,25.2091389,23.7908611,27,26,27L34,27C36.209139,27,38,25.2091389,38,22.9999995L38,22.0000005C38,20.3431458,36.656855,19,35,19L25.0000005,19C23.3431458,19,22,20.3431458,22,22.0000005Z" fill="#FDC3C1" fill-opacity="1" transform="matrix(-1,0,0,1,44,0)"/><rect x="2" y="2" width="24" height="22" rx="6" fill="#FF2B00" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_42954)"><path d="M19.5976818125,7.975256428125C19.429908812500003,7.822927328125,19.2089708125,7.739575828125,18.980501812500002,7.739575828125C18.9596658125,7.739575828125,18.9388328125,7.739575828125,18.9180168125,7.741015028125C18.9054158125,7.742438228125,18.759929812499998,7.7511042281249996,18.5325648125,7.7511042281249996C18.1557318125,7.7511042281249996,17.4200373125,7.725233628125,16.7231331125,7.560001828125C15.823278412499999,7.345890628125,14.7847490125,6.354385998125,14.4826178125,6.158966188125C14.3303046125,6.059849266125,14.1550002125,6.010986328125,13.9793429125,6.010986328125C13.8044057125,6.010986328125,13.6290998125,6.059849266125,13.4771390125,6.157543068125C13.4405084125,6.181974528125,12.3530998125,7.3070687281249995,11.2894523125,7.560001828125C10.5929158125,7.725233928125,9.8431994125,7.7511042281249996,9.4667181125,7.7511042281249996C9.2393200125,7.7511042281249996,9.0942183125,7.742438228125,9.0798602125,7.741015028125C9.0600977525,7.739575828125,9.0403350025,7.739575828125,9.0202204625,7.739575828125C8.7913834425,7.739575828125,8.5697414925,7.822911628125,8.4012634725,7.975256428125C8.2184271925,8.140488628125,8.1142578125,8.371819928125,8.1142578125,8.614663628125001L8.1142578125,10.803070528125C8.1142578125,18.892904328125,13.5874648125,19.934664328125002,13.8194995125,19.976283328125C13.8726792125,19.984965328125,13.926195612499999,19.989251328125,13.9797263125,19.989251328125C14.0328917125,19.989251328125,14.0871253125,19.984965328125,14.1395697125,19.976283328125C14.3719735125,19.934649328124998,19.8853928125,18.892889328125,19.8853928125,10.803071028125L19.8853928125,8.614663628125001C19.8853928125,8.371819928125,19.7808728125,8.140504328125001,19.5976818125,7.975256428125ZM17.5744924125,11.104817828125L13.779988812500001,14.776156428125C13.756276612499999,14.814913728125,13.7271771125,14.853767428125,13.6919684125,14.886800728125C13.580604512499999,14.994567828125,13.4333286125,15.046293228125,13.287458412500001,15.043415028125C13.1416373125,15.046293228125,12.9946804125,14.994567828125,12.8833489125,14.886800728125C12.8481564125,14.853767428125,12.8187046125,14.814913728125,12.7949929125,14.776156428125L10.7671649125,12.813325428125001C10.5512478125,12.604953728125,10.5512478125,12.267311128125,10.7671649125,12.057500328125C10.9830506125,11.849193528125,11.3332775125,11.849193528125,11.549195812499999,12.057500328125L13.287475112500001,13.740152828125L16.7928295125,10.349056728125C17.0087471125,10.140685528125001,17.3586063125,10.140685528125001,17.5745096125,10.349056728125C17.7903948125,10.557348228125,17.7903948125,10.896494828125,17.5744924125,11.104817828125Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/static/images/icon/shengchanzhuisu.svg b/src/static/images/icon/shengchanzhuisu.svg
new file mode 100644
index 0000000..8e70971
--- /dev/null
+++ b/src/static/images/icon/shengchanzhuisu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="28" height="28" viewBox="0 0 28 28"><defs><clipPath id="master_svg0_141_42929"><rect x="0" y="0" width="28" height="28" rx="0"/></clipPath><clipPath id="master_svg1_141_42949"><rect x="6" y="8" width="16" height="16" rx="0"/></clipPath></defs><g clip-path="url(#master_svg0_141_42929)"><path d="M6,13.0000005L6,15.590147C6,16.842423,7.1377500000000005,17.7866721,8.368577,17.555892L14,16.5L19.631422999999998,17.555892C20.86225,17.7866721,22,16.842423,22,15.590147L22,13.0000005C22,11.3431458,20.656855,10,19,10L9.0000005,10C7.3431458,10,6,11.3431458,6,13.0000005Z" fill="#FC99E5" fill-opacity="1" transform="matrix(1,0,0,-1,0,20)"/><rect x="2" y="5" width="24" height="22" rx="6" fill="#6234F9" fill-opacity="1" style="opacity:0.800000011920929;"/><g clip-path="url(#master_svg1_141_42949)"><path d="M19.743700671875,9.612548828125L8.276513691875,9.612548828125C7.901513691875,9.612548828125,7.598388671875,9.915673848125,7.598388671875,10.290673968125L7.598388671875,21.709423828125C7.598388671875,22.082861828125,7.901513691875,22.385986828125,8.276513691875,22.385986828125L19.204639671875,22.385986828125L18.021826671875,21.204736828125C17.992138671875,21.175048828125,17.967139671875,21.143798828125,17.943701671875,21.109424828125C17.193701771875,21.618798828125,16.309327171874997,21.892236828125,15.381201771875,21.892236828125C14.159326571874999,21.892236828125,13.010889071874999,21.415673828125,12.146826771875,20.551611828125C11.282764471875,19.687549828125,10.806201971875,18.539112128124998,10.806201971875,17.317236928125C10.806201971875,16.095361728125,11.282764471875,14.946924228124999,12.146826771875,14.082861928125C13.010889071874999,13.218799828125,14.159326571874999,12.742237328125,15.381201771875,12.742237328125C16.603076971874998,12.742237328125,17.751514671875,13.218799828125,18.617138671875,14.082861928125C19.481200671875,14.946924228124999,19.957763671875,16.095361728125,19.957763671875,17.317236928125C19.957763671875,18.417237328124997,19.571825671875,19.456299828124997,18.865575671875,20.282861828125C18.879638671875,20.293799828125,18.892138671875,20.306298828125,18.906200671875,20.318798828125L20.410887671875003,21.823486828125C20.417137671875,21.785985828125,20.421825671875,21.746923828125,20.421825671875,21.707860828125L20.421825671875,10.289111378125C20.420263671875,9.915673848125,20.117137671875,9.612548828125,19.743700671875,9.612548828125ZM12.195263871875,12.225048828125C12.082763671875,12.337548928124999,11.926513671875,12.407861428124999,11.753076571874999,12.407861428124999L9.846826571874999,12.407861428124999C9.501513871875,12.407861428124999,9.221826471875,12.128174028125,9.221826471875,11.782861428124999C9.221826471875,11.610986428124999,9.292138971875,11.454736428124999,9.404638871875001,11.340673928125C9.517138871875,11.226611328125,9.673388971875,11.157861428124999,9.846826571874999,11.157861428124999L11.754639171874999,11.157861428124999C12.099951771875,11.157861428124999,12.379639171874999,11.437548828125,12.379639171874999,11.782861428124999C12.378076571874999,11.956299028125,12.309326671874999,12.112549028125,12.195263871875,12.225048828125ZM17.787451671875,15.867236128125L18.546826671875,15.867236128125C17.995263671875,14.670361528125,16.784326571875,13.837549228124999,15.382763871875,13.837549228124999C13.464014071874999,13.837549228124999,11.901514071874999,15.400049228124999,11.901514071874999,17.318799028125C11.901514071874999,19.239111928125,13.464014071874999,20.800048828125,15.382763871875,20.800048828125C16.682764071875,20.800048828125,17.818701671875,20.082860828125,18.417138671875,19.025049228125L17.787451671875,19.025049228125C17.442139671874997,19.025049228125,17.162451771875,18.745361328125,17.162451771875,18.400049228125L17.162451771875,16.492236128125C17.162451771875,16.146924028125,17.442139671874997,15.867236128125,17.787451671875,15.867236128125ZM15.045264271875,18.398486128125C15.045264271875,18.743798228125,14.765576371875,19.023486128125,14.420264271875,19.023486128125C14.248389271875,19.023486128125,14.092139271875,18.953173628125,13.978076971875,18.840673428125C13.865576771875,18.728173228125,13.795264271875,18.571923228125,13.795264271875,18.398486128125L13.795264271875,16.492236128125C13.795264271875,16.146924028125,14.074952171875001,15.867236128125,14.420264271875,15.867236128125C14.592139271875,15.867236128125,14.748389271875,15.937548628125,14.862451571874999,16.050048828125C14.974951771875,16.162549028125,15.045264271875,16.318799028125,15.045264271875,16.492236128125L15.045264271875,18.398486128125ZM16.787451771875,18.398486128125C16.787451771875,18.743798228125,16.507763871875,19.023486128125,16.162451771875,19.023486128125C15.990576771875,19.023486128125,15.834326771875,18.953173628125,15.720264471875,18.840673428125C15.606202171875,18.728173228125,15.537451771875,18.571923228125,15.537451771875,18.398486128125L15.537451771875,16.492236128125C15.537451771875,16.146924028125,15.817139671875,15.867236128125,16.162451771875,15.867236128125C16.334326771875,15.867236128125,16.490576771875,15.937548628125,16.604639071875,16.050048828125C16.717139271875,16.162549028125,16.787451771875,16.318799028125,16.787451771875,16.492236128125L16.787451771875,18.398486128125Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
index 64ada0b..0813795 100644
--- a/src/store/modules/user.ts
+++ b/src/store/modules/user.ts
@@ -1,5 +1,7 @@
import { logout, getInfo, loginCheckFactory } from "@/api/login";
+import { getRouters as getRoutersApi } from "@/api/menu";
import { getToken, setToken, removeToken } from "@/utils/auth";
+import defAva from "@/static/images/profile.jpg";
import { defineStore } from "pinia";
import config from "@/config.js";
@@ -25,14 +27,20 @@
}),
actions: {
loginCheckFactory(userInfo: any) {
- const userName = userInfo.userName;
+ const userName = userInfo.userName.trim();
const password = userInfo.password;
+ const factoryId = userInfo.factoryId;
return new Promise((resolve, reject) => {
- loginCheckFactory(userName, password)
+ loginCheckFactory(userName, password, factoryId)
.then((res: any) => {
- setToken(res.token);
- this.token = res.token;
- resolve(null);
+ const token = res.token || res.data?.token;
+ if (token) {
+ setToken(token);
+ this.token = token;
+ resolve(null);
+ } else {
+ reject("鏈幏鍙栧埌鐧诲綍浠ょ墝");
+ }
})
.catch((error: any) => {
reject(error);
@@ -43,26 +51,29 @@
return new Promise((resolve, reject) => {
getInfo()
.then((res: any) => {
- const user = res.user;
+ // 鍏煎 res.data 缁撴瀯
+ const data = res.data || res;
+ const user = data.user || {};
let avatar = user.avatar || "";
avatar = config.baseUrl + "/profile/" + avatar;
- if (res.roles && res.roles.length > 0) {
- this.roles = res.roles;
- this.permissions = res.permissions;
+ if (data.roles && data.roles.length > 0) {
+ // 楠岃瘉杩斿洖鐨剅oles鏄惁鏄竴涓潪绌烘暟缁�
+ this.roles = data.roles;
+ this.permissions = data.permissions;
} else {
this.roles = ["ROLE_DEFAULT"];
}
- this.id = user.userId;
- this.name = user.userName;
+ this.id = user.userId || "";
+ this.name = user.userName || "";
this.avatar = avatar;
- this.currentFactoryName = user.currentFactoryName;
- this.nickName = user.nickName;
- this.roleName = user.roles[0].roleName;
- this.currentDeptId = user.tenantId;
+ this.currentFactoryName = user.currentFactoryName || "";
+ this.nickName = user.nickName || "";
+ this.roleName = Array.isArray(user.roles) && user.roles.length > 0 ? user.roles[0].roleName || "" : "";
+ this.currentDeptId = user.tenantId || "";
this.currentLoginTime = this.getCurrentTime();
- resolve(res);
+ resolve(data);
})
- .catch((error) => {
+ .catch(error => {
reject(error);
});
});
@@ -80,25 +91,32 @@
removeToken();
resolve(null);
})
- .catch((error) => {
+ .catch(error => {
reject(error);
});
});
},
getCurrentTime() {
const now = new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, "0");
- const day = String(now.getDate()).padStart(2, "0");
- const hours = String(now.getHours()).padStart(2, "0");
- const minutes = String(now.getMinutes()).padStart(2, "0");
- const seconds = String(now.getSeconds()).padStart(2, "0");
+ const year = now.getFullYear(); // 鑾峰彇骞翠唤
+ const month = String(now.getMonth() + 1).padStart(2, "0"); // 鏈堜唤浠�0寮�濮嬶紝瑕�+1锛屽苟琛ラ浂
+ const day = String(now.getDate()).padStart(2, "0"); // 鏃ユ湡琛ラ浂
+ const hours = String(now.getHours()).padStart(2, "0"); // 灏忔椂琛ラ浂
+ const minutes = String(now.getMinutes()).padStart(2, "0"); // 鍒嗛挓琛ラ浂
+ const seconds = String(now.getSeconds()).padStart(2, "0"); // 绉掓暟琛ラ浂
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
getRouters() {
- return new Promise((resolve) => {
- this.routers = [];
- resolve({ data: [] });
+ return new Promise((resolve, reject) => {
+ getRoutersApi()
+ .then((res: any) => {
+ // 瀛樺偍璺敱鏉冮檺鏁版嵁
+ this.routers = res.data || [];
+ resolve(res);
+ })
+ .catch(error => {
+ reject(error);
+ });
});
},
},
diff --git a/src/utils/versionUpgrade.js b/src/utils/versionUpgrade.js
index e0c818a..6008dbf 100644
--- a/src/utils/versionUpgrade.js
+++ b/src/utils/versionUpgrade.js
@@ -1,6 +1,7 @@
import config from "@/config";
import { getAllVersion } from "@/api/viewIndex";
import bus from "@/plugins/bus";
+let hasTriggeredVersionCheckInSession = false;
function compareVersion(v1, v2) {
const s1 = String(v1 || "").replace(/[^\d.]/g, "").split(".").map((n) => Number(n) || 0);
@@ -264,12 +265,17 @@
let lastVersionCheckAt = 0;
const triggerVersionCheck = async (from = "unknown") => {
+ if (hasTriggeredVersionCheckInSession) {
+ console.log(`${logPrefix} 璺宠繃鐗堟湰妫�鏌ワ紝鏈浼氳瘽宸叉娴嬭繃锛屾潵婧�=${from}`);
+ return;
+ }
const now = Date.now();
if (now - lastVersionCheckAt < throttleMs) {
console.log(`${logPrefix} 璺宠繃閲嶅妫�鏌ワ紝鏉ユ簮=${from}`);
return;
}
lastVersionCheckAt = now;
+ hasTriggeredVersionCheckInSession = true;
console.log(`${logPrefix} 瑙﹀彂鐗堟湰妫�鏌ワ紝鏉ユ簮=${from}`);
const currentVersion = await getCurrentVersion(logPrefix);
await checkAppVersionUpgrade(logPrefix, currentVersion);
--
Gitblit v1.9.3