From e449a5408265e4bd1f6c66f5be28a42efac444ee Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期日, 20 九月 2026 15:22:46 +0800
Subject: [PATCH] 天津豹鸣app 1.需求修改
---
src/pages/sales/contractManagement/detail.vue | 304
src/api/salesManagement/indicatorStats.js | 38
src/utils/delivery.js | 75
src/pages/sales/salesAccount/goOut.vue | 60
src/pages/sales/biddingManagement/edit.vue | 337 +
src/api/projectManagement/project.js | 94
src/pages/projectManagement/project/_components/ProductRow.vue | 363 +
src/pages/projectManagement/project/_utils/mapper.js | 318 +
src/api/projectManagement/projectType.js | 10
src/pages/sales/lawsuitManagement/detail.vue | 262
src/pages/sales/contractManagement/edit.vue | 628 ++
src/pages/basicData/customerFile/index.vue | 4
src/api/collaborativeApproval/houseLeaseManagement.js | 36
src/pages/projectManagement/project/_utils/dict.js | 43
src/api/salesManagement/lawsuitManagement.js | 36
src/pages/sales/salesAccount/index.vue | 29
src/pages/inventoryManagement/fuelRecord/edit.vue | 295
src/components/FilterPanel.vue | 241
src/pages/inventoryManagement/fuelRecord/index.vue | 377 +
src/pages/sales/lawsuitManagement/index.vue | 264
src/pages/qualityManagement/finalInspection/_utils/inspection.js | 61
src/utils/paymentCycle.js | 33
src/api/collaborativeApproval/broadbandCommunication.js | 36
src/pages/sales/salesQuotation/edit.vue | 45
src/static/images/icon/susongguanli.svg | 10
src/utils/warning.js | 137
src/pages/projectManagement/project/edit.vue | 574 +
src/pages/sales/deliveryLedger/detail.vue | 405 +
src/pages/projectManagement/project/index.vue | 357 +
src/pages/cooperativeOffice/houseLeaseManagement/edit.vue | 603 +
src/api/qualityManagement/inspectionReport.js | 44
src/pages/projectManagement/project/_components/TeamRow.vue | 148
src/utils/projectOptions.js | 31
src/pages/qualityManagement/finalInspection/fileList.vue | 109
src/pages/projectManagement/project/archive.vue | 186
src/pages/sales/lawsuitManagement/edit.vue | 344 +
src/static/images/icon/baobiaoguanli.svg | 7
src/static/images/icon/zhaotoubiaoguanli.svg | 9
src/static/images/icon/kuandaiyutongxin.svg | 6
src/static/images/icon/cheliangguanli.svg | 12
src/pages/oa/_utils/finReimbursementMappers.js | 2
src/pages/equipmentManagement/deviceInfo/index.vue | 198
src/pages/projectManagement/project/progress.vue | 326 +
src/pages/sales/salesQuotation/detail.vue | 4
src/pages/inventoryManagement/fuelCard/edit.vue | 223
src/pages/inventoryManagement/fuelRecord/detail.vue | 255
src/components/FormPicker.vue | 184
src/pages/cooperativeOffice/broadbandCommunication/detail.vue | 206
src/pages/works.vue | 178
src/pages/equipmentManagement/ledger/index.vue | 29
src/pages/inventoryManagement/vehicleInfo/edit.vue | 249
src/pages/sales/biddingManagement/index.vue | 305
src/static/images/icon/jiayoukaguanli.svg | 8
src/api/projectManagement/config.js | 17
src/utils/detailCache.js | 30
src/pages/qualityManagement/finalInspection/add.vue | 194
src/static/images/icon/xiangmuguanli.svg | 7
src/api/salesManagement/deliveryLedger.js | 45
src/pages/cooperativeOffice/houseLeaseManagement/index.vue | 255
src/pages/sales/receiptPaymentLedger/index.vue | 300
src/pages/qualityManagement/inspectionReport/index.vue | 265
src/pages/sales/deliveryLedger/edit.vue | 385 +
src/pages/inventoryManagement/vehicleInfo/index.vue | 331 +
src/pages/sales/receiptPaymentLedger/detail.vue | 371
src/api/salesManagement/contractManagement.js | 45
src/pages/cooperativeOffice/broadbandCommunication/edit.vue | 316 +
src/pages/qualityManagement/finalInspection/detail.vue | 80
src/pages/inventoryManagement/fuelCard/detail.vue | 273
src/static/images/icon/jiayoujilu.svg | 8
src/utils/file.js | 156
src/api/salesManagement/biddingManagement.js | 36
src/utils/prefill.js | 26
src/pages/cooperativeOffice/houseLeaseManagement/detail.vue | 406 +
src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue | 5
src/pages/projectManagement/project/config.vue | 150
src/utils/list.js | 48
src/api/qualityManagement/qualityInspectFile.js | 13
src/pages/equipmentManagement/ledger/detail.vue | 272
src/pages/inventoryManagement/vehicleInfo/detail.vue | 510 +
src/pages.json | 259
src/pages/basicData/customerFile/edit.vue | 3
src/api/inventoryManagement/vehicle.js | 183
src/pages/qualityManagement/inspectionReport/edit.vue | 290
src/pages/sales/salesAccount/view.vue | 146
src/pages/sales/salesAccount/detail.vue | 409 +
src/pages/projectManagement/project/_utils/stage.js | 131
src/pages/sales/biddingManagement/detail.vue | 246
src/pages/inventoryManagement/fuelCard/index.vue | 282
src/pages/sales/deliveryLedger/index.vue | 336 +
src/static/images/icon/fangwuzulin.svg | 9
src/api/projectManagement/role.js | 10
src/pages/cooperativeOffice/broadbandCommunication/index.vue | 262
src/pages/projectManagement/project/_components/AddressRow.vue | 94
src/pages/qualityManagement/inspectionReport/detail.vue | 264
src/pages/sales/salesQuotation/index.vue | 68
src/pages/sales/contractManagement/index.vue | 279
src/pages/projectManagement/project/detail.vue | 948 +++
src/pages/inventoryManagement/fuelCard/recharge.vue | 401 +
src/pages/oa/ReimburseManage/reimburse-form/index.vue | 10
src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js | 1
100 files changed, 17,840 insertions(+), 473 deletions(-)
diff --git a/src/api/collaborativeApproval/broadbandCommunication.js b/src/api/collaborativeApproval/broadbandCommunication.js
new file mode 100644
index 0000000..ea5737e
--- /dev/null
+++ b/src/api/collaborativeApproval/broadbandCommunication.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ锛坵arnStatus: normal 姝e父 / expiring 鍗冲皢鍒版湡 / overdue 宸查�炬湡锛�
+export function broadbandListPage(query) {
+ return request({
+ url: "/broadband/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getBroadbandDetail(id) {
+ return request({
+ url: "/broadband/getDetail",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板涓庝慨鏀瑰叡鐢紝闈� id 鏄惁瀛樺湪鍖哄垎
+export function addOrUpdateBroadband(data) {
+ return request({
+ url: "/broadband/addOrUpdate",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎锛歜ody 涓� id 鏁扮粍
+export function delBroadband(ids) {
+ return request({
+ url: "/broadband/del",
+ method: "delete",
+ data: Array.isArray(ids) ? ids : [ids],
+ });
+}
diff --git a/src/api/collaborativeApproval/houseLeaseManagement.js b/src/api/collaborativeApproval/houseLeaseManagement.js
new file mode 100644
index 0000000..720d3fb
--- /dev/null
+++ b/src/api/collaborativeApproval/houseLeaseManagement.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ锛坵arnStatus: normal 姝e父 / expiring 鍗冲皢鍒版湡 / overdue 宸查�炬湡锛�
+export function houseLeaseListPage(query) {
+ return request({
+ url: "/houseLease/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getHouseLeaseDetail(id) {
+ return request({
+ url: "/houseLease/getDetail",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板涓庝慨鏀瑰叡鐢紝闈� id 鏄惁瀛樺湪鍖哄垎锛涙湀搴﹁垂鐢ㄥ瓙琛ㄩ殢 costList 涓�璧锋彁浜�
+export function addOrUpdateHouseLease(data) {
+ return request({
+ url: "/houseLease/addOrUpdate",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎锛歜ody 涓� id 鏁扮粍
+export function delHouseLease(ids) {
+ return request({
+ url: "/houseLease/del",
+ method: "delete",
+ data: Array.isArray(ids) ? ids : [ids],
+ });
+}
diff --git a/src/api/inventoryManagement/vehicle.js b/src/api/inventoryManagement/vehicle.js
new file mode 100644
index 0000000..049282c
--- /dev/null
+++ b/src/api/inventoryManagement/vehicle.js
@@ -0,0 +1,183 @@
+import request from "@/utils/request";
+
+// ---------------- 杞﹁締妗f ----------------
+
+export function listVehicleInfo(query) {
+ return request({
+ url: "/vehicle/info/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 30 澶╁唴骞存/淇濋櫓鍒版湡鐨勮溅杈嗭紝鏃犲弬鏁帮紝杩斿洖鏁扮粍
+export function getVehicleWarningList() {
+ return request({
+ url: "/vehicle/info/warningList",
+ method: "get",
+ });
+}
+
+export function addVehicleInfo(data) {
+ return request({
+ url: "/vehicle/info",
+ method: "post",
+ data,
+ });
+}
+
+export function updateVehicleInfo(data) {
+ return request({
+ url: "/vehicle/info",
+ method: "put",
+ data,
+ });
+}
+
+export function delVehicleInfo(id) {
+ return request({
+ url: `/vehicle/info/${id}`,
+ method: "delete",
+ });
+}
+
+// ---------------- 杞﹁締璐圭敤 ----------------
+// 绠$悊绔病鏈夊崟鐙殑璐圭敤 api 鏂囦欢锛岀洿鎺ュ唴鑱� request锛岃繖閲岃ˉ榻�
+
+export function listVehicleCost(query) {
+ return request({
+ url: "/vehicle/cost/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function addVehicleCost(data) {
+ return request({
+ url: "/vehicle/cost",
+ method: "post",
+ data,
+ });
+}
+
+export function delVehicleCost(id) {
+ return request({
+ url: `/vehicle/cost/${id}`,
+ method: "delete",
+ });
+}
+
+// ---------------- 鍔犳补鍗� ----------------
+
+export function listFuelCardPage(query) {
+ return request({
+ url: "/vehicle/fuelCard/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍏ㄩ噺鍗″垪琛紙涓嶅垎椤碉級锛岀敤浜庡姞娌硅褰曠殑涓嬫媺涓庝綑棰濆睍绀�
+export function listAllFuelCard() {
+ return request({
+ url: "/vehicle/fuelCard/listAll",
+ method: "get",
+ });
+}
+
+export function addFuelCard(data) {
+ return request({
+ url: "/vehicle/fuelCard",
+ method: "post",
+ data,
+ });
+}
+
+export function updateFuelCard(data) {
+ return request({
+ url: "/vehicle/fuelCard",
+ method: "put",
+ data,
+ });
+}
+
+export function delFuelCard(id) {
+ return request({
+ url: `/vehicle/fuelCard/${id}`,
+ method: "delete",
+ });
+}
+
+// ---------------- 鍔犳补鍗″厖鍊� ----------------
+
+export function listRecharge(query) {
+ return request({
+ url: "/vehicle/fuelCardRecharge/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function addRecharge(data) {
+ return request({
+ url: "/vehicle/fuelCardRecharge/add",
+ method: "post",
+ data,
+ });
+}
+
+export function delRecharge(id) {
+ return request({
+ url: `/vehicle/fuelCardRecharge/${id}`,
+ method: "delete",
+ });
+}
+
+// ---------------- 鍔犳补璁板綍 ----------------
+
+export function listFuelRecord(query) {
+ return request({
+ url: "/vehicle/fuelRecord/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function addFuelRecord(data) {
+ return request({
+ url: "/vehicle/fuelRecord",
+ method: "post",
+ data,
+ });
+}
+
+export function updateFuelRecord(data) {
+ return request({
+ url: "/vehicle/fuelRecord",
+ method: "put",
+ data,
+ });
+}
+
+export function delFuelRecord(id) {
+ return request({
+ url: `/vehicle/fuelRecord/${id}`,
+ method: "delete",
+ });
+}
+
+// 鎻愪氦锛氫粠鍔犳补鍗℃墸鍑忎綑棰濄�傛棤 body 鐨� POST
+export function submitFuelRecord(id) {
+ return request({
+ url: `/vehicle/fuelRecord/submit/${id}`,
+ method: "post",
+ });
+}
+
+// 鎾ゅ洖锛氶��杩樺姞娌瑰崱浣欓銆傛棤 body 鐨� POST
+export function revokeFuelRecord(id) {
+ return request({
+ url: `/vehicle/fuelRecord/revoke/${id}`,
+ method: "post",
+ });
+}
diff --git a/src/api/projectManagement/config.js b/src/api/projectManagement/config.js
new file mode 100644
index 0000000..d0835cd
--- /dev/null
+++ b/src/api/projectManagement/config.js
@@ -0,0 +1,17 @@
+import request from "@/utils/request";
+
+// 椤圭洰绠$悊鍏ㄥ眬閰嶇疆锛堝悎鍚屽師浠跺洖浼犻璀︽彁鍓嶅ぉ鏁扮瓑锛�
+export function getProjectConfig() {
+ return request({
+ url: "/projectManagement/config",
+ method: "get",
+ });
+}
+
+export function updateProjectConfig(data) {
+ return request({
+ url: "/projectManagement/config",
+ method: "put",
+ data,
+ });
+}
diff --git a/src/api/projectManagement/project.js b/src/api/projectManagement/project.js
new file mode 100644
index 0000000..3d98536
--- /dev/null
+++ b/src/api/projectManagement/project.js
@@ -0,0 +1,94 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ椤圭洰锛堟柊澧炰笌缂栬緫鍏辩敤 /save锛岄潬 info.id 鍖哄垎锛�
+export function listProject(data) {
+ return request({
+ url: "/projectManagement/info/listPage",
+ method: "post",
+ data,
+ });
+}
+
+// 鏌ヨ椤圭洰璇︽儏锛坕d 鍦ㄨ矾寰勪笂锛孭OST锛�
+export function getProject(id) {
+ return request({
+ url: `/projectManagement/info/${id}`,
+ method: "post",
+ });
+}
+
+// 鏂板 / 缂栬緫椤圭洰
+export function saveProject(data) {
+ return request({
+ url: "/projectManagement/info/save",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎椤圭洰锛坆ody 涓� id 鏁扮粍锛�
+export function delProject(ids) {
+ return request({
+ url: "/projectManagement/info/remove",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鏇存柊鍗曟嵁鐘舵��
+export function updateStatus(data) {
+ return request({
+ url: "/projectManagement/info/updateStatus",
+ method: "post",
+ data,
+ });
+}
+
+// 鎻愪氦锛歳eviewStatus 缃� 0
+export function submitProject(data) {
+ return request({
+ url: "/projectManagement/info/updateStatus",
+ method: "post",
+ data: { ...data, reviewStatus: 0 },
+ });
+}
+
+// 瀹℃牳锛歳eviewStatus 缃� 1
+export function auditProject(data) {
+ return request({
+ url: "/projectManagement/info/updateStatus",
+ method: "post",
+ data: { ...data, reviewStatus: 1 },
+ });
+}
+
+// 闃舵锛堣繘搴︽眹鎶ワ級
+export function saveStage(data) {
+ return request({
+ url: "/projectManagement/info/saveStage",
+ method: "post",
+ data,
+ });
+}
+
+export function listStage(projectId) {
+ return request({
+ url: `/projectManagement/info/listStage/${projectId}`,
+ method: "post",
+ });
+}
+
+export function deleteStage(stageId) {
+ return request({
+ url: `/projectManagement/info/deleteStage/${stageId}`,
+ method: "post",
+ });
+}
+
+// 椤圭洰褰掓。闄勪欢
+export function getProjectAttachments(id) {
+ return request({
+ url: `/projectManagement/info/attachments/${id}`,
+ method: "get",
+ });
+}
diff --git a/src/api/projectManagement/projectType.js b/src/api/projectManagement/projectType.js
new file mode 100644
index 0000000..fbb699e
--- /dev/null
+++ b/src/api/projectManagement/projectType.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// 椤圭洰绫诲瀷锛堢鐞嗙銆岃鍒掋�嶏級涓嬫媺
+export function listPlan(data) {
+ return request({
+ url: "/projectManagement/plan/listPage",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/projectManagement/role.js b/src/api/projectManagement/role.js
new file mode 100644
index 0000000..ad3f410
--- /dev/null
+++ b/src/api/projectManagement/role.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// 椤圭洰鍥㈤槦瑙掕壊涓嬫媺
+export function findRoleListPage(query) {
+ return request({
+ url: "/projectManagement/roles/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/qualityManagement/inspectionReport.js b/src/api/qualityManagement/inspectionReport.js
new file mode 100644
index 0000000..49601f2
--- /dev/null
+++ b/src/api/qualityManagement/inspectionReport.js
@@ -0,0 +1,44 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鎶ュ憡鍒楄〃
+export function listInspectionReport(query) {
+ return request({
+ url: "/quality/inspectionReport/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ鎶ュ憡璇︾粏
+export function getInspectionReport(id) {
+ return request({
+ url: `/quality/inspectionReport/${id}`,
+ method: "get",
+ });
+}
+
+// 鏂板鎶ュ憡
+export function addInspectionReport(data) {
+ return request({
+ url: "/quality/inspectionReport",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼鎶ュ憡
+export function updateInspectionReport(data) {
+ return request({
+ url: "/quality/inspectionReport",
+ method: "put",
+ data,
+ });
+}
+
+// 娉ㄩ攢浣滃簾锛堝悗绔叆鍙備负閫楀彿鍒嗛殧鐨� id 涓诧級
+export function delInspectionReport(ids) {
+ return request({
+ url: `/quality/inspectionReport/${ids}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/qualityManagement/qualityInspectFile.js b/src/api/qualityManagement/qualityInspectFile.js
new file mode 100644
index 0000000..20b0ef6
--- /dev/null
+++ b/src/api/qualityManagement/qualityInspectFile.js
@@ -0,0 +1,13 @@
+/**
+ * 鍑哄巶妫�楠屾姤鍛婃枃浠讹紙闇�姹備節.4锛�
+ *
+ * /quality/qualityInspect/downPdf 鏄� POST + body 杩斿洖浜岃繘鍒讹紝鑰岋細
+ * request.ts 涓嶉�忎紶 responseType锛堜細鎶婁簩杩涘埗褰� JSON 瑙f瀽锛�
+ * uni.downloadFile 鍙敮鎸� GET
+ * 鎵�浠ヨ繖閲屼笉璧� request锛岀洿鎺ヤ氦缁� utils/file 鐨� downloadFileByPost 钀界洏鍐� openDocument銆�
+ */
+import { downloadFileByPost } from "@/utils/file";
+
+export function downloadQualityInspectPdf(id, fileName = "鍑哄巶妫�楠屾姤鍛�.pdf") {
+ return downloadFileByPost("/quality/qualityInspect/downPdf", { id }, fileName);
+}
diff --git a/src/api/salesManagement/biddingManagement.js b/src/api/salesManagement/biddingManagement.js
new file mode 100644
index 0000000..f15270a
--- /dev/null
+++ b/src/api/salesManagement/biddingManagement.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ锛堟姇鏍囨棩鏈熷尯闂村弬鏁颁负 bidDateStart / bidDateEnd锛�
+export function biddingListPage(query) {
+ return request({
+ url: "/sales/bidding/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getBiddingDetail(id) {
+ return request({
+ url: "/sales/bidding/getDetail",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板涓庝慨鏀瑰叡鐢紝闈� id 鏄惁瀛樺湪鍖哄垎
+export function addOrUpdateBidding(data) {
+ return request({
+ url: "/sales/bidding/addOrUpdate",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎锛歜ody 涓� id 鏁扮粍
+export function delBidding(ids) {
+ return request({
+ url: "/sales/bidding/del",
+ method: "delete",
+ data: Array.isArray(ids) ? ids : [ids],
+ });
+}
diff --git a/src/api/salesManagement/contractManagement.js b/src/api/salesManagement/contractManagement.js
new file mode 100644
index 0000000..812d391
--- /dev/null
+++ b/src/api/salesManagement/contractManagement.js
@@ -0,0 +1,45 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function contractListPage(query) {
+ return request({
+ url: "/sales/contract/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 涓嶅垎椤电殑鍚堝悓绮剧畝鍒楄〃锛屼緵璇夎绠$悊绛夋ā鍧楃殑銆屽叧鑱旈攢鍞悎鍚屻�嶄笅鎷変娇鐢�
+export function contractListNoPage(params) {
+ return request({
+ url: "/sales/contract/listNoPage",
+ method: "get",
+ params,
+ });
+}
+
+export function getContractDetail(id) {
+ return request({
+ url: "/sales/contract/getDetail",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板涓庝慨鏀瑰叡鐢紝闈� id 鏄惁瀛樺湪鍖哄垎
+export function addOrUpdateContract(data) {
+ return request({
+ url: "/sales/contract/addOrUpdate",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎锛歜ody 涓� id 鏁扮粍
+export function delContract(ids) {
+ return request({
+ url: "/sales/contract/del",
+ method: "delete",
+ data: Array.isArray(ids) ? ids : [ids],
+ });
+}
diff --git a/src/api/salesManagement/deliveryLedger.js b/src/api/salesManagement/deliveryLedger.js
new file mode 100644
index 0000000..abccfec
--- /dev/null
+++ b/src/api/salesManagement/deliveryLedger.js
@@ -0,0 +1,45 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ锛堝彂璐ф棩鏈熷尯闂村弬鏁颁负 beginShippingDate / endShippingDate锛�
+export function deliveryLedgerListPage(query) {
+ return request({
+ url: "/shippingInfo/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 璇︽儏锛堝悗绔矾寰勫氨鏄� getDateil锛屽嬁"绾犳"鎴� getDetail锛�
+export function getDeliveryDetail(id) {
+ return request({
+ url: `/shippingInfo/getDateil/${id}`,
+ method: "get",
+ });
+}
+
+// 鍙戣揣鍙拌处淇敼
+export function addOrUpdateDeliveryLedger(data) {
+ return request({
+ url: "/shippingInfo/update",
+ method: "post",
+ data,
+ });
+}
+
+// 琛ュ厖鍙戣揣淇℃伅鍚庢彁浜わ紙浼氭墸鍑忓簱瀛橈級
+export function deductStock(data) {
+ return request({
+ url: "/shippingInfo/deductStock",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎锛歜ody 涓� id 鏁扮粍
+export function delDeliveryLedger(ids) {
+ return request({
+ url: "/shippingInfo/delete",
+ method: "delete",
+ data: Array.isArray(ids) ? ids : [ids],
+ });
+}
diff --git a/src/api/salesManagement/indicatorStats.js b/src/api/salesManagement/indicatorStats.js
new file mode 100644
index 0000000..7f0d788
--- /dev/null
+++ b/src/api/salesManagement/indicatorStats.js
@@ -0,0 +1,38 @@
+// 瀹㈡埛寰�鏉ワ紙闇�姹備竷锛夛細椤圭洰 鈫� 瀹㈡埛 鈫� 鍚堝悓 涓夌骇寰�鏉ユ眹鎬伙紝涓庣鐞嗙 salesManagement/receiptPaymentLedger 鍚屼竴濂楁帴鍙�
+import request from "@/utils/request";
+
+// 椤堕儴缁熻鏉�
+export function transactionsSummary(query) {
+ return request({
+ url: "/metricStatistics/transactionsSummary",
+ method: "get",
+ params: query,
+ });
+}
+
+// 绗竴绾э細椤圭洰寰�鏉ユ眹鎬�
+export function projectTransactions(query) {
+ return request({
+ url: "/metricStatistics/projectTransactions",
+ method: "get",
+ params: query,
+ });
+}
+
+// 绗簩绾э細椤圭洰涓嬬殑瀹㈡埛寰�鏉�
+export function customewTransactions(query) {
+ return request({
+ url: "/metricStatistics/customewTransactions",
+ method: "get",
+ params: query,
+ });
+}
+
+// 绗笁绾э細瀹㈡埛涓嬬殑鍚堝悓寰�鏉ユ槑缁�
+export function customewTransactionsDetails(query) {
+ return request({
+ url: "/metricStatistics/customewTransactionsDetails",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/salesManagement/lawsuitManagement.js b/src/api/salesManagement/lawsuitManagement.js
new file mode 100644
index 0000000..42ae3b4
--- /dev/null
+++ b/src/api/salesManagement/lawsuitManagement.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ锛堢珛妗堟椂闂村尯闂村弬鏁颁负 filingDateStart / filingDateEnd锛�
+export function lawsuitListPage(query) {
+ return request({
+ url: "/sales/lawsuit/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function getLawsuitDetail(id) {
+ return request({
+ url: "/sales/lawsuit/getDetail",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板涓庝慨鏀瑰叡鐢紝闈� id 鏄惁瀛樺湪鍖哄垎
+export function addOrUpdateLawsuit(data) {
+ return request({
+ url: "/sales/lawsuit/addOrUpdate",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎锛歜ody 涓� id 鏁扮粍
+export function delLawsuit(ids) {
+ return request({
+ url: "/sales/lawsuit/del",
+ method: "delete",
+ data: Array.isArray(ids) ? ids : [ids],
+ });
+}
diff --git a/src/components/FilterPanel.vue b/src/components/FilterPanel.vue
new file mode 100644
index 0000000..a36e683
--- /dev/null
+++ b/src/components/FilterPanel.vue
@@ -0,0 +1,241 @@
+<!--
+ 澶氭潯浠舵煡璇㈠脊灞傦紙schema 椹卞姩锛�
+
+ <FilterPanel v-model:show="showFilter" :model-value="query" :fields="filterFields"
+ @search="onSearch" @reset="onReset" />
+
+ 娉ㄦ剰锛歲uery 鐢� reactive 瀹氫箟鏃朵笉瑕佸啓 v-model="query"銆�
+ 缁勪欢鍥炲悙鐨勬槸鏁翠釜绛涢�夊璞★紝reactive 甯搁噺涓嶅彲鏁翠綋閲嶆柊璧嬪��
+ 锛堜細缂栬瘧鎴� `query = $event`锛岃繍琛屾椂鎶� Assignment to constant variable锛夈��
+ 鐢� :model-value 浼犲叆 + 鍦� @search/@reset 閲� Object.assign(query, values) 鍐欏洖锛�
+
+ const query = reactive({ projectName: '', bidResult: '' });
+ const onSearch = values => { Object.assign(query, values); getList(); };
+
+ 椤甸潰鑻ュ彟鏈夋悳绱㈣緭鍏ユ缁戝埌 query 鐨勬煇涓瓧娈碉紝涓よ竟浼氬ぉ鐒跺悓姝ワ紙鍚屼竴涓璞★級銆�
+
+ fields 鏀寔鐨勭被鍨嬶細
+ { prop: 'projectName', label: '椤圭洰鍚嶇О', type: 'input' }
+ { prop: 'bidResult', label: '鎷涙姇鏍囩粨鏋�', type: 'select', options: [...] }
+ { prop: 'filingDate', label: '绔嬫鏃堕棿', type: 'date' }
+ { prop: 'bidDateRange', label: '鎶曟爣鏃ユ湡', type: 'daterange' }
+
+ daterange 绫诲瀷鐨勫�兼槸 [寮�濮�, 缁撴潫] 涓ゅ厓绱犳暟缁勶紝椤甸潰鍦ㄦ彁浜ゅ墠鑷鎷嗘垚鍚庣瑕佺殑鍙傛暟鍚�
+ 锛堝悇妯″潡鐨勫尯闂村弬鏁板悕涓嶇粺涓�锛屽 bidDateStart/bidDateEnd銆乥eginShippingDate/endShippingDate锛夈��
+-->
+<template>
+ <up-popup :show="show"
+ mode="bottom"
+ :round="16"
+ @close="close">
+ <view class="filter-panel">
+ <view class="filter-panel__header">
+ <text class="filter-panel__title">{{ title }}</text>
+ <u-icon name="close"
+ size="20"
+ color="#909399"
+ @click="close"></u-icon>
+ </view>
+
+ <scroll-view scroll-y
+ class="filter-panel__body">
+ <view v-for="field in fields"
+ :key="field.prop"
+ class="filter-panel__field">
+ <text class="filter-panel__label">{{ field.label }}</text>
+ <view class="filter-panel__control">
+ <u-input v-if="field.type === 'input'"
+ v-model="local[field.prop]"
+ :placeholder="field.placeholder || '璇疯緭鍏�'"
+ clearable />
+ <FormPicker v-else-if="field.type === 'select'"
+ v-model="local[field.prop]"
+ type="select"
+ :options="field.options || []"
+ :title="`閫夋嫨${field.label}`"
+ :placeholder="field.placeholder || '璇烽�夋嫨'" />
+ <FormPicker v-else-if="field.type === 'date'"
+ v-model="local[field.prop]"
+ type="date"
+ :placeholder="field.placeholder || '璇烽�夋嫨'" />
+ <view v-else-if="field.type === 'daterange'"
+ class="filter-panel__range">
+ <FormPicker v-model="local[field.prop][0]"
+ type="date"
+ placeholder="寮�濮嬫棩鏈�" />
+ <text class="filter-panel__range-sep">鑷�</text>
+ <FormPicker v-model="local[field.prop][1]"
+ type="date"
+ placeholder="缁撴潫鏃ユ湡" />
+ </view>
+ </view>
+ </view>
+ </scroll-view>
+
+ <view class="filter-panel__footer">
+ <u-button class="filter-panel__btn"
+ @click="reset">閲嶇疆</u-button>
+ <u-button class="filter-panel__btn"
+ type="primary"
+ @click="search">鏌ヨ</u-button>
+ </view>
+ </view>
+ </up-popup>
+</template>
+
+<script setup>
+ import { ref, watch } from "vue";
+ import FormPicker from "@/components/FormPicker.vue";
+
+ const props = defineProps({
+ show: {
+ type: Boolean,
+ default: false,
+ },
+ modelValue: {
+ type: Object,
+ default: () => ({}),
+ },
+ fields: {
+ type: Array,
+ default: () => [],
+ },
+ title: {
+ type: String,
+ default: "绛涢��",
+ },
+ });
+
+ const emit = defineEmits(["update:show", "update:modelValue", "search", "reset"]);
+
+ const buildEmpty = () => {
+ const empty = {};
+ props.fields.forEach(field => {
+ empty[field.prop] = field.type === "daterange" ? [] : "";
+ });
+ return empty;
+ };
+
+ // 蹇呴』鍏堢敤 buildEmpty() 閾烘弧瀛楁锛氬脊灞傚嵆浣块殣钘忔椂涔熶細娓叉煋瀛愯妭鐐癸紝
+ // daterange 鐨� v-model 浼氬 local[prop][0] 鍙栧�硷紝local[prop] 涓� undefined 浼氱洿鎺ユ姤閿�
+ const local = ref(buildEmpty());
+
+ const syncFromParent = () => {
+ const source = props.modelValue || {};
+ const next = buildEmpty();
+ props.fields.forEach(field => {
+ const value = source[field.prop];
+ if (field.type === "daterange") {
+ next[field.prop] = Array.isArray(value) ? [...value] : [];
+ } else if (value !== undefined && value !== null) {
+ next[field.prop] = value;
+ }
+ });
+ local.value = next;
+ };
+
+ watch(
+ () => props.show,
+ show => {
+ if (show) syncFromParent();
+ },
+ { immediate: true }
+ );
+
+ // fields 鏄紓姝ユ嬁鍒扮殑鍦烘櫙锛氳ˉ涓�娆¢摵搴曪紝閬垮厤瀛楁缂哄け
+ watch(
+ () => props.fields,
+ () => {
+ if (props.show) {
+ syncFromParent();
+ } else {
+ local.value = buildEmpty();
+ }
+ },
+ { deep: true }
+ );
+
+ const close = () => {
+ emit("update:show", false);
+ };
+
+ const search = () => {
+ emit("update:modelValue", { ...local.value });
+ emit("search", { ...local.value });
+ close();
+ };
+
+ const reset = () => {
+ local.value = buildEmpty();
+ emit("update:modelValue", { ...local.value });
+ emit("reset", { ...local.value });
+ };
+</script>
+
+<style scoped lang="scss">
+ .filter-panel {
+ background: #ffffff;
+ padding: 16px;
+ padding-bottom: calc(16px + env(safe-area-inset-bottom));
+ }
+
+ .filter-panel__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .filter-panel__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .filter-panel__body {
+ max-height: 55vh;
+ padding: 8px 0;
+ }
+
+ .filter-panel__field {
+ display: flex;
+ align-items: center;
+ padding: 10px 0;
+ border-bottom: 1px solid #f8f8f8;
+ }
+
+ .filter-panel__label {
+ width: 90px;
+ flex-shrink: 0;
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .filter-panel__control {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .filter-panel__range {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .filter-panel__range-sep {
+ flex-shrink: 0;
+ font-size: 13px;
+ color: #909399;
+ }
+
+ .filter-panel__footer {
+ display: flex;
+ gap: 12px;
+ padding-top: 16px;
+ }
+
+ .filter-panel__btn {
+ flex: 1;
+ }
+</style>
diff --git a/src/components/FormPicker.vue b/src/components/FormPicker.vue
new file mode 100644
index 0000000..e6ad427
--- /dev/null
+++ b/src/components/FormPicker.vue
@@ -0,0 +1,184 @@
+<!--
+ 閫氱敤琛ㄥ崟閫夋嫨椤�
+ 鏀惧湪 u-form-item 閲屼娇鐢紝鏇夸唬鍚勯〉闈㈡墜鍐欑殑銆屽彧璇� u-input + up-action-sheet/up-datetime-picker銆嶇粍鍚堛��
+
+ 涓嬫媺锛�
+ <u-form-item label="鎵�灞為」鐩�" prop="projectId" required border-bottom>
+ <FormPicker v-model="form.projectId" :display-text="form.projectName"
+ type="select" title="閫夋嫨椤圭洰" :options="projectOptions"
+ @change="onProjectChange" />
+ </u-form-item>
+
+ 鏃ユ湡锛�
+ <u-form-item label="绔嬫鏃堕棿" prop="filingDate" required border-bottom>
+ <FormPicker v-model="form.filingDate" type="date" placeholder="璇烽�夋嫨绔嬫鏃堕棿" />
+ </u-form-item>
+
+ options 鏀寔涓夌鍐欐硶锛屽彇鍊间紭鍏堢骇锛�
+ 鏄剧ず鏂囨湰 item.name 鈫� item.text 鈫� item.label 鈫� item.displayText
+ 瀹為檯鍊� item.value 鈫� item.id 鈫� item.code
+ 涔熷彲浠ョ敤 label-key / value-key 鏄惧紡鎸囧畾銆�
+-->
+<template>
+ <view class="form-picker">
+ <u-input :modelValue="text"
+ :placeholder="placeholder"
+ :disabled="disabled"
+ :readonly="true"
+ :clearable="clearable && !disabled && !readonly"
+ @click="open"
+ @update:modelValue="onInputUpdate">
+ <template #suffix>
+ <u-icon v-if="!disabled && !readonly"
+ name="arrow-right"
+ color="#c0c4cc"
+ @click="open"></u-icon>
+ </template>
+ </u-input>
+
+ <up-action-sheet v-if="type === 'select'"
+ :show="showSheet"
+ :actions="actions"
+ :title="title"
+ @select="onSelect"
+ @close="showSheet = false"></up-action-sheet>
+
+ <up-datetime-picker v-if="type === 'date'"
+ :show="showPicker"
+ v-model="pickerValue"
+ :mode="dateMode"
+ @confirm="onDateConfirm"
+ @cancel="showPicker = false"></up-datetime-picker>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { formatDateToYMD } from "@/utils/ruoyi";
+ import { parseDate } from "@/utils/warning";
+
+ const props = defineProps({
+ modelValue: {
+ type: [String, Number, Boolean, null],
+ default: "",
+ },
+ // select 涓嬫媺 | date 鏃ユ湡
+ type: {
+ type: String,
+ default: "select",
+ },
+ // 鐩存帴鎸囧畾鏄剧ず鏂囨湰锛涗笉浼犲垯鎸� options 鍙嶆煡
+ displayText: {
+ type: String,
+ default: "",
+ },
+ options: {
+ type: Array,
+ default: () => [],
+ },
+ title: {
+ type: String,
+ default: "",
+ },
+ placeholder: {
+ type: String,
+ default: "璇烽�夋嫨",
+ },
+ labelKey: {
+ type: String,
+ default: "",
+ },
+ valueKey: {
+ type: String,
+ default: "",
+ },
+ // 鏃ユ湡绮惧害锛歞ate | datetime | time | year-month
+ dateMode: {
+ type: String,
+ default: "date",
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ readonly: {
+ type: Boolean,
+ default: false,
+ },
+ clearable: {
+ type: Boolean,
+ default: true,
+ },
+ });
+
+ const emit = defineEmits(["update:modelValue", "change"]);
+
+ const showSheet = ref(false);
+ const showPicker = ref(false);
+ const pickerValue = ref(Date.now());
+
+ const optionValue = item =>
+ props.valueKey
+ ? item?.[props.valueKey]
+ : (item?.value ?? item?.id ?? item?.code);
+
+ const optionLabel = item =>
+ props.labelKey
+ ? item?.[props.labelKey]
+ : (item?.name ?? item?.text ?? item?.label ?? item?.displayText ?? "");
+
+ const actions = computed(() =>
+ (props.options || []).map(item => ({
+ text: String(optionLabel(item) ?? ""),
+ value: optionValue(item),
+ data: item,
+ }))
+ );
+
+ const text = computed(() => {
+ if (props.displayText) return props.displayText;
+ if (props.modelValue === undefined || props.modelValue === null) return "";
+ if (props.type === "date") return String(props.modelValue);
+ const matched = (props.options || []).find(
+ item => String(optionValue(item)) === String(props.modelValue)
+ );
+ return matched ? String(optionLabel(matched)) : "";
+ });
+
+ const open = () => {
+ if (props.disabled || props.readonly) return;
+ if (props.type === "select") {
+ showSheet.value = true;
+ return;
+ }
+ const base = parseDate(props.modelValue);
+ pickerValue.value = base ? base.getTime() : Date.now();
+ showPicker.value = true;
+ };
+
+ // 鐢ㄦ埛鐐瑰嚮 u-input 鐨勬竻闄ゆ寜閽椂锛屾妸绌哄�煎悓姝ュ嚭鍘�
+ const onInputUpdate = value => {
+ if (value !== "" && value !== undefined) return;
+ emit("update:modelValue", "");
+ emit("change", "", null);
+ };
+
+ const onSelect = item => {
+ showSheet.value = false;
+ emit("update:modelValue", item.value);
+ emit("change", item.value, item.data);
+ };
+
+ const onDateConfirm = e => {
+ showPicker.value = false;
+ const value = formatDateToYMD(e.value);
+ emit("update:modelValue", value);
+ emit("change", value, null);
+ };
+</script>
+
+<style scoped lang="scss">
+ .form-picker {
+ width: 100%;
+ }
+</style>
diff --git a/src/pages.json b/src/pages.json
index e61cc28..98adae1 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -1486,6 +1486,265 @@
"navigationBarTitleText": "鍏憡閫氱煡",
"navigationStyle": "custom"
}
+ },
+ {
+ "path": "pages/projectManagement/project/index",
+ "style": {
+ "navigationBarTitleText": "椤圭洰绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/projectManagement/project/detail",
+ "style": {
+ "navigationBarTitleText": "椤圭洰璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/projectManagement/project/edit",
+ "style": {
+ "navigationBarTitleText": "椤圭洰缂栬緫",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/projectManagement/project/archive",
+ "style": {
+ "navigationBarTitleText": "鏂囨。褰掓。",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/projectManagement/project/progress",
+ "style": {
+ "navigationBarTitleText": "杩涘害姹囨姤",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/projectManagement/project/config",
+ "style": {
+ "navigationBarTitleText": "棰勮閰嶇疆",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/biddingManagement/index",
+ "style": {
+ "navigationBarTitleText": "鎷涙姇鏍囩鐞�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/biddingManagement/detail",
+ "style": {
+ "navigationBarTitleText": "鎷涙姇鏍囪鎯�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/biddingManagement/edit",
+ "style": {
+ "navigationBarTitleText": "鎷涙姇鏍囩紪杈�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/lawsuitManagement/index",
+ "style": {
+ "navigationBarTitleText": "璇夎绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/lawsuitManagement/detail",
+ "style": {
+ "navigationBarTitleText": "璇夎璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/lawsuitManagement/edit",
+ "style": {
+ "navigationBarTitleText": "璇夎缂栬緫",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/deliveryLedger/index",
+ "style": {
+ "navigationBarTitleText": "鍙戣揣鍙拌处",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/deliveryLedger/detail",
+ "style": {
+ "navigationBarTitleText": "鍙戣揣璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/deliveryLedger/edit",
+ "style": {
+ "navigationBarTitleText": "琛ュ厖鍙戣揣淇℃伅",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/contractManagement/index",
+ "style": {
+ "navigationBarTitleText": "鍚堝悓绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/contractManagement/detail",
+ "style": {
+ "navigationBarTitleText": "鍚堝悓璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/sales/contractManagement/edit",
+ "style": {
+ "navigationBarTitleText": "鍚堝悓缂栬緫",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/qualityManagement/inspectionReport/index",
+ "style": {
+ "navigationBarTitleText": "鎶ヨ〃绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/qualityManagement/inspectionReport/detail",
+ "style": {
+ "navigationBarTitleText": "鎶ュ憡璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/qualityManagement/inspectionReport/edit",
+ "style": {
+ "navigationBarTitleText": "鎶ュ憡缂栬緫",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/vehicleInfo/index",
+ "style": {
+ "navigationBarTitleText": "杞﹁締绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/vehicleInfo/detail",
+ "style": {
+ "navigationBarTitleText": "杞﹁締璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/vehicleInfo/edit",
+ "style": {
+ "navigationBarTitleText": "杞﹁締缂栬緫",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/fuelCard/index",
+ "style": {
+ "navigationBarTitleText": "鍔犳补鍗$鐞�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/fuelCard/detail",
+ "style": {
+ "navigationBarTitleText": "鍔犳补鍗¤鎯�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/fuelCard/edit",
+ "style": {
+ "navigationBarTitleText": "鍔犳补鍗$紪杈�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/fuelCard/recharge",
+ "style": {
+ "navigationBarTitleText": "鍔犳补鍗″厖鍊�",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/fuelRecord/index",
+ "style": {
+ "navigationBarTitleText": "鍔犳补璁板綍",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/fuelRecord/detail",
+ "style": {
+ "navigationBarTitleText": "鍔犳补璁板綍璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/inventoryManagement/fuelRecord/edit",
+ "style": {
+ "navigationBarTitleText": "鍔犳补璁板綍缂栬緫",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/cooperativeOffice/broadbandCommunication/index",
+ "style": {
+ "navigationBarTitleText": "瀹藉甫涓庨�氫俊绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/cooperativeOffice/broadbandCommunication/detail",
+ "style": {
+ "navigationBarTitleText": "瀹藉甫涓庨�氫俊璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/cooperativeOffice/broadbandCommunication/edit",
+ "style": {
+ "navigationBarTitleText": "瀹藉甫涓庨�氫俊缂栬緫",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/cooperativeOffice/houseLeaseManagement/index",
+ "style": {
+ "navigationBarTitleText": "鎴垮眿绉熻祦绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/cooperativeOffice/houseLeaseManagement/detail",
+ "style": {
+ "navigationBarTitleText": "鎴垮眿绉熻祦璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/cooperativeOffice/houseLeaseManagement/edit",
+ "style": {
+ "navigationBarTitleText": "鎴垮眿绉熻祦缂栬緫",
+ "navigationStyle": "custom"
+ }
}
],
"subPackages": [
diff --git a/src/pages/basicData/customerFile/edit.vue b/src/pages/basicData/customerFile/edit.vue
index c10973e..cc888ce 100644
--- a/src/pages/basicData/customerFile/edit.vue
+++ b/src/pages/basicData/customerFile/edit.vue
@@ -199,9 +199,10 @@
customerType: [{ required: true, message: "璇烽�夋嫨瀹㈡埛鍒嗙被", trigger: "change" }],
};
+ // 涓庡悗绔�/绠$悊绔彇鍊间繚鎸佷竴鑷达紙绠$悊绔笅鎷変负銆岄浂鍞鎴� / 杩涢攢鍟嗗鎴枫�嶏級
const customerTypeActions = [
{ name: "闆跺敭瀹㈡埛", value: "闆跺敭瀹㈡埛" },
- { name: "缁忛攢鍟嗗鎴�", value: "缁忛攢鍟嗗鎴�" },
+ { name: "杩涢攢鍟嗗鎴�", value: "杩涢攢鍟嗗鎴�" },
];
const pageTitle = computed(() =>
diff --git a/src/pages/basicData/customerFile/index.vue b/src/pages/basicData/customerFile/index.vue
index 1d6fb9d..3e36018 100644
--- a/src/pages/basicData/customerFile/index.vue
+++ b/src/pages/basicData/customerFile/index.vue
@@ -102,10 +102,12 @@
const customerName = ref("");
const list = ref([]);
+ // 瀹㈡埛鍒嗙被鍙栧�煎繀椤讳笌鍚庣涓�鑷达紙绠$悊绔笅鎷変负銆岄浂鍞鎴� / 杩涢攢鍟嗗鎴枫�嶏級锛�
+ // 鍐欐垚銆岀粡閿�鍟嗗鎴枫�嶄細绛涗笉鍑轰换浣曟暟鎹�
const tabList = reactive([
{ name: "鍏ㄩ儴瀹㈡埛", value: "" },
{ name: "闆跺敭瀹㈡埛", value: "闆跺敭瀹㈡埛" },
- { name: "缁忛攢鍟嗗鎴�", value: "缁忛攢鍟嗗鎴�" },
+ { name: "杩涢攢鍟嗗鎴�", value: "杩涢攢鍟嗗鎴�" },
]);
const tabValue = ref(0);
diff --git a/src/pages/cooperativeOffice/broadbandCommunication/detail.vue b/src/pages/cooperativeOffice/broadbandCommunication/detail.vue
new file mode 100644
index 0000000..a2505f8
--- /dev/null
+++ b/src/pages/cooperativeOffice/broadbandCommunication/detail.vue
@@ -0,0 +1,206 @@
+<template>
+ <view class="broadband-detail">
+ <PageHeader title="瀹藉甫涓庨�氫俊璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <text class="hero-title">{{ detail.office || "-" }}</text>
+ <up-tag :text="warn.text || '鏃犵即璐规棩鏈�'"
+ :type="warnTagType(warn.level)"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <view v-if="warn.text"
+ class="warn-banner"
+ :style="{ background: warn.level === 'normal' ? '#f0f9eb' : '#fef0f0' }">
+ <text class="warn-banner-text"
+ :style="{ color: warn.color }">缂磋垂鐘舵�侊細{{ warn.text }}锛堜笅娆$即璐� {{ detail.nextPaymentDate || "-" }}锛�</text>
+ </view>
+
+ <u-cell-group title="瀹夎浣嶇疆"
+ class="detail-section">
+ <u-cell title="鍖哄煙"
+ :value="detail.area || '-'" />
+ <u-cell title="鍔炰簨澶�"
+ :value="detail.office || '-'" />
+ <u-cell title="椹荤渷浜哄憳"
+ :value="detail.residentStaff || '-'" />
+ <u-cell title="鍦板潃"
+ :value="detail.address || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="瀹夎涓庣即璐�"
+ class="detail-section">
+ <u-cell title="瀹夎鏃ユ湡"
+ :value="detail.installDate || '-'" />
+ <u-cell title="瀹夎璐�"
+ :value="formatAmount(detail.installFee)" />
+ <u-cell title="缂磋垂鍛ㄦ湡"
+ :value="cycleText(detail.paymentCycle)" />
+ <u-cell title="缂磋垂鏂瑰紡"
+ :value="detail.paymentMethod || '-'" />
+ <u-cell title="棰勫瓨"
+ :value="formatAmount(detail.prepaidAmount)" />
+ <u-cell title="璧勮垂"
+ :value="formatAmount(detail.monthlyFee)" />
+ <u-cell title="涓嬫缂磋垂鏃ユ湡"
+ :value="detail.nextPaymentDate || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="濂楅鍐呭"
+ class="detail-section">
+ <u-cell title="濂楅鍐呭"
+ :value="detail.packageContent || '-'" />
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { getWarnInfo } from "@/utils/warning";
+ import { cycleText } from "@/utils/paymentCycle";
+ import { getBroadbandDetail } from "@/api/collaborativeApproval/broadbandCommunication";
+
+ // 瀹藉甫鍒版湡鎻愬墠 15 澶╅璀�
+ const WARN_DAYS = 15;
+
+ const loading = ref(true);
+ const broadbandId = ref("");
+ const detail = ref({});
+
+ const warn = computed(() =>
+ getWarnInfo(detail.value.nextPaymentDate, WARN_DAYS)
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("broadbandDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/cooperativeOffice/broadbandCommunication/edit?id=${broadbandId.value}`,
+ });
+ };
+
+ const warnTagType = level => {
+ if (level === "overdue") return "error";
+ if (level === "warning") return "warning";
+ if (level === "normal") return "success";
+ return "info";
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const data = await loadDetailData({
+ cacheKey: "broadbandDetail",
+ id: broadbandId.value,
+ fetcher: getBroadbandDetail,
+ });
+ detail.value = data || {};
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇瀹藉甫涓庨�氫俊璇︽儏澶辫触", icon: "none" });
+ detail.value = {};
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ broadbandId.value = options?.id || "";
+ if (!broadbandId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .broadband-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #f4f1ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(124, 92, 252, 0.1);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .warn-banner {
+ margin-top: 12px;
+ padding: 10px 14px;
+ border-radius: 10px;
+ }
+
+ .warn-banner-text {
+ font-size: 13px;
+ line-height: 1.5;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/cooperativeOffice/broadbandCommunication/edit.vue b/src/pages/cooperativeOffice/broadbandCommunication/edit.vue
new file mode 100644
index 0000000..b9a4df9
--- /dev/null
+++ b/src/pages/cooperativeOffice/broadbandCommunication/edit.vue
@@ -0,0 +1,316 @@
+<template>
+ <view class="account-detail">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="瀹夎浣嶇疆"
+ class="form-section">
+ <up-form-item label="鍖哄煙"
+ prop="area"
+ required
+ border-bottom>
+ <up-input v-model="form.area"
+ placeholder="璇疯緭鍏ュ尯鍩�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍔炰簨澶�"
+ prop="office"
+ required
+ border-bottom>
+ <up-input v-model="form.office"
+ placeholder="璇疯緭鍏ュ姙浜嬪"
+ clearable />
+ </up-form-item>
+ <up-form-item label="椹荤渷浜哄憳"
+ prop="residentStaff"
+ required
+ border-bottom>
+ <up-input v-model="form.residentStaff"
+ placeholder="璇疯緭鍏ラ┗鐪佷汉鍛�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍦板潃"
+ prop="address"
+ required>
+ <up-textarea v-model="form.address"
+ placeholder="璇疯緭鍏ヨ缁嗗湴鍧�"
+ auto-height
+ maxlength="200" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="瀹夎涓庣即璐�"
+ class="form-section">
+ <up-form-item label="瀹夎鏃ユ湡"
+ prop="installDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.installDate"
+ type="date"
+ placeholder="璇烽�夋嫨瀹夎鏃ユ湡"
+ @change="calculateNextPayment" />
+ </up-form-item>
+ <up-form-item label="瀹夎璐�"
+ prop="installFee"
+ border-bottom>
+ <up-input v-model="form.installFee"
+ type="number"
+ placeholder="璇疯緭鍏ュ畨瑁呰垂"
+ clearable />
+ </up-form-item>
+ <up-form-item label="缂磋垂鍛ㄦ湡"
+ prop="paymentCycle"
+ required
+ border-bottom>
+ <FormPicker v-model="form.paymentCycle"
+ type="select"
+ title="閫夋嫨缂磋垂鍛ㄦ湡"
+ :options="paymentCycleOptions"
+ placeholder="璇烽�夋嫨缂磋垂鍛ㄦ湡"
+ @change="calculateNextPayment" />
+ </up-form-item>
+ <up-form-item label="缂磋垂鏂瑰紡"
+ prop="paymentMethod"
+ required
+ border-bottom>
+ <FormPicker v-model="form.paymentMethod"
+ type="select"
+ title="閫夋嫨缂磋垂鏂瑰紡"
+ :options="paymentMethodOptions"
+ placeholder="璇烽�夋嫨缂磋垂鏂瑰紡" />
+ </up-form-item>
+ <up-form-item label="棰勫瓨"
+ prop="prepaidAmount"
+ border-bottom>
+ <up-input v-model="form.prepaidAmount"
+ type="number"
+ placeholder="璇疯緭鍏ラ瀛橀噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璧勮垂"
+ prop="monthlyFee"
+ border-bottom>
+ <up-input v-model="form.monthlyFee"
+ type="number"
+ placeholder="璇疯緭鍏ヨ祫璐�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="涓嬫缂磋垂鏃ユ湡"
+ prop="nextPaymentDate">
+ <up-input v-model="form.nextPaymentDate"
+ placeholder="鎸夊畨瑁呮棩鏈熶笌缂磋垂鍛ㄦ湡鑷姩璁$畻"
+ disabled />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="濂楅鍐呭"
+ class="form-section">
+ <up-form-item label="濂楅鍐呭"
+ prop="packageContent">
+ <up-textarea v-model="form.packageContent"
+ placeholder="璇疯緭鍏ュ椁愬唴瀹�"
+ auto-height
+ maxlength="500" />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </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 FormPicker from "@/components/FormPicker.vue";
+ import { addMonths } from "@/utils/warning";
+ import { loadDetailData } from "@/utils/detailCache";
+ import {
+ PAYMENT_CYCLE_OPTIONS,
+ PAYMENT_METHOD_OPTIONS,
+ } from "@/utils/paymentCycle";
+ import {
+ addOrUpdateBroadband,
+ getBroadbandDetail,
+ } from "@/api/collaborativeApproval/broadbandCommunication";
+
+ const formRef = ref();
+ const loading = ref(false);
+ const broadbandId = ref("");
+
+ const paymentCycleOptions = PAYMENT_CYCLE_OPTIONS;
+ const paymentMethodOptions = PAYMENT_METHOD_OPTIONS;
+
+ const form = ref({
+ id: undefined,
+ area: "",
+ office: "",
+ residentStaff: "",
+ address: "",
+ installDate: "",
+ installFee: "",
+ paymentCycle: "",
+ paymentMethod: "",
+ prepaidAmount: "",
+ monthlyFee: "",
+ packageContent: "",
+ nextPaymentDate: "",
+ });
+
+ const rules = {
+ area: [{ required: true, message: "璇疯緭鍏ュ尯鍩�", trigger: "blur" }],
+ office: [{ required: true, message: "璇疯緭鍏ュ姙浜嬪", trigger: "blur" }],
+ residentStaff: [
+ { required: true, message: "璇疯緭鍏ラ┗鐪佷汉鍛�", trigger: "blur" },
+ ],
+ address: [{ required: true, message: "璇疯緭鍏ュ湴鍧�", trigger: "blur" }],
+ installDate: [
+ { required: true, message: "璇烽�夋嫨瀹夎鏃ユ湡", trigger: "change" },
+ ],
+ paymentCycle: [
+ { required: true, message: "璇烽�夋嫨缂磋垂鍛ㄦ湡", trigger: "change" },
+ ],
+ paymentMethod: [
+ { required: true, message: "璇烽�夋嫨缂磋垂鏂瑰紡", trigger: "change" },
+ ],
+ };
+
+ const pageTitle = computed(() =>
+ broadbandId.value ? "缂栬緫瀹藉甫涓庨�氫俊" : "鏂板瀹藉甫涓庨�氫俊"
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const numOrNull = value =>
+ value === "" || value === null || value === undefined
+ ? null
+ : Number.isNaN(Number(value))
+ ? null
+ : Number(value);
+
+ // 涓嬫缂磋垂鏃ユ湡 = 瀹夎鏃ユ湡 + 缂磋垂鍛ㄦ湡鏈堬紝鍙
+ const calculateNextPayment = () => {
+ const months = Number(form.value.paymentCycle);
+ form.value.nextPaymentDate =
+ form.value.installDate && Number.isFinite(months) && months > 0
+ ? addMonths(form.value.installDate, months)
+ : "";
+ };
+
+ const loadDetail = async () => {
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "broadbandDetail",
+ id: broadbandId.value,
+ fetcher: getBroadbandDetail,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鏈壘鍒板甯︿笌閫氫俊淇℃伅", icon: "none" });
+ return;
+ }
+
+ form.value = {
+ id: data.id,
+ area: data.area || "",
+ office: data.office || "",
+ residentStaff: data.residentStaff || "",
+ address: data.address || "",
+ installDate: (data.installDate || "").slice(0, 10),
+ installFee: data.installFee ?? "",
+ paymentCycle: data.paymentCycle ?? "",
+ paymentMethod: data.paymentMethod || "",
+ prepaidAmount: data.prepaidAmount ?? "",
+ monthlyFee: data.monthlyFee ?? "",
+ packageContent: data.packageContent || "",
+ nextPaymentDate: (data.nextPaymentDate || "").slice(0, 10),
+ };
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ calculateNextPayment();
+ const payload = {
+ id: form.value.id,
+ area: form.value.area,
+ office: form.value.office,
+ residentStaff: form.value.residentStaff,
+ address: form.value.address,
+ installDate: form.value.installDate,
+ installFee: numOrNull(form.value.installFee),
+ paymentCycle: numOrNull(form.value.paymentCycle),
+ paymentMethod: form.value.paymentMethod,
+ prepaidAmount: numOrNull(form.value.prepaidAmount),
+ monthlyFee: numOrNull(form.value.monthlyFee),
+ packageContent: form.value.packageContent,
+ nextPaymentDate: form.value.nextPaymentDate || null,
+ };
+
+ loading.value = true;
+ addOrUpdateBroadband(payload)
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 1000);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ broadbandId.value = options.id;
+ form.value.id = options.id;
+ loadDetail();
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/cooperativeOffice/broadbandCommunication/index.vue b/src/pages/cooperativeOffice/broadbandCommunication/index.vue
new file mode 100644
index 0000000..8d04133
--- /dev/null
+++ b/src/pages/cooperativeOffice/broadbandCommunication/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"
+ v-model="query.area"
+ placeholder="璇疯緭鍏ュ尯鍩熸悳绱�"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="broadbandList.length > 0"
+ class="ledger-list">
+ <view v-for="item in broadbandList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="wifi"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.office || "-" }}</text>
+ </view>
+ <up-tag :text="warnOf(item).text || '鏃犵即璐规棩鏈�'"
+ :type="warnTagType(warnOf(item).level)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍖哄煙</text>
+ <text class="detail-value">{{ item.area || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">椹荤渷浜哄憳</text>
+ <text class="detail-value">{{ item.residentStaff || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍦板潃</text>
+ <text class="detail-value">{{ item.address || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹夎鏃ユ湡</text>
+ <text class="detail-value">{{ item.installDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹夎璐�</text>
+ <text class="detail-value">{{ formatAmount(item.installFee) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">缂磋垂鍛ㄦ湡</text>
+ <text class="detail-value">{{ cycleText(item.paymentCycle) }}</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">{{ formatAmount(item.monthlyFee) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嬫缂磋垂鏃ユ湡</text>
+ <text class="detail-value">{{ item.nextPaymentDate || "-" }}</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"
+ @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">
+ <text>鏆傛棤瀹藉甫涓庨�氫俊鏁版嵁</text>
+ </view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="瀹藉甫涓庨�氫俊绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ broadbandListPage,
+ delBroadband,
+ } from "@/api/collaborativeApproval/broadbandCommunication";
+ import { unwrapList } from "@/utils/list";
+ import { getWarnInfo } from "@/utils/warning";
+ import { cycleText } from "@/utils/paymentCycle";
+
+ // 瀹藉甫鍒版湡鎻愬墠 15 澶╅璀�
+ const WARN_DAYS = 15;
+
+ const WARN_STATUS_OPTIONS = [
+ { name: "姝e父", value: "normal" },
+ { name: "鍗冲皢鍒版湡", value: "expiring" },
+ { name: "宸查�炬湡", value: "overdue" },
+ ];
+
+ const broadbandList = ref([]);
+ const showFilter = ref(false);
+
+ const query = reactive({
+ area: "",
+ office: "",
+ residentStaff: "",
+ warnStatus: "",
+ });
+
+ const filterFields = [
+ { prop: "area", label: "鍖哄煙", type: "input", placeholder: "璇疯緭鍏ュ尯鍩�" },
+ { prop: "office", label: "鍔炰簨澶�", type: "input", placeholder: "璇疯緭鍏ュ姙浜嬪" },
+ {
+ prop: "residentStaff",
+ label: "椹荤渷浜哄憳",
+ type: "input",
+ placeholder: "璇疯緭鍏ラ┗鐪佷汉鍛�",
+ },
+ {
+ prop: "warnStatus",
+ label: "棰勮鐘舵��",
+ type: "select",
+ options: WARN_STATUS_OPTIONS,
+ },
+ ];
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("broadbandDetail");
+ uni.navigateTo({
+ url: "/pages/cooperativeOffice/broadbandCommunication/edit",
+ });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("broadbandDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/cooperativeOffice/broadbandCommunication/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("broadbandDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/cooperativeOffice/broadbandCommunication/detail?id=${item.id}`,
+ });
+ };
+
+ const warnOf = item => getWarnInfo(item?.nextPaymentDate, WARN_DAYS);
+
+ const warnTagType = level => {
+ if (level === "overdue") return "error";
+ if (level === "warning") return "warning";
+ if (level === "normal") return "success";
+ return "info";
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const getList = () => {
+ broadbandListPage({
+ current: -1,
+ size: -1,
+ area: query.area || undefined,
+ office: query.office || undefined,
+ residentStaff: query.residentStaff || undefined,
+ warnStatus: query.warnStatus || undefined,
+ })
+ .then(res => {
+ broadbandList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ュ甯︿笌閫氫俊璁板綍鍚楋紵",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delBroadband(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/cooperativeOffice/houseLeaseManagement/detail.vue b/src/pages/cooperativeOffice/houseLeaseManagement/detail.vue
new file mode 100644
index 0000000..4f4861f
--- /dev/null
+++ b/src/pages/cooperativeOffice/houseLeaseManagement/detail.vue
@@ -0,0 +1,406 @@
+<template>
+ <view class="house-lease-detail">
+ <PageHeader title="鎴垮眿绉熻祦璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <view class="hero-left">
+ <text class="hero-title">{{ detail.office || "-" }}</text>
+ <text class="hero-sub">{{ detail.address || "-" }}</text>
+ </view>
+ <up-tag :text="warn.text || '鏃犲埌鏈熸棩'"
+ :type="warnTagType(warn.level)"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <view v-if="warn.text"
+ class="warn-banner"
+ :style="{ background: warn.level === 'normal' ? '#f0f9eb' : '#fef0f0' }">
+ <text class="warn-banner-text"
+ :style="{ color: warn.color }">鍚堝悓鐘舵�侊細{{ warn.text }}锛堝埌鏈熸棩 {{ detail.contractEndDate || "-" }}锛�</text>
+ </view>
+
+ <u-cell-group title="鎴垮眿涓庝汉鍛�"
+ class="detail-section">
+ <u-cell title="鍔炰簨澶�"
+ :value="detail.office || '-'" />
+ <u-cell title="浣跨敤浜�"
+ :value="detail.userName || '-'" />
+ <u-cell title="椹荤渷浜哄憳"
+ :value="detail.residentStaff || '-'" />
+ <u-cell title="鍦板潃"
+ :value="detail.address || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鍚堝悓淇℃伅"
+ class="detail-section">
+ <u-cell title="璧风鏃堕棿"
+ :value="detail.rentStartDate || '-'" />
+ <u-cell title="鍚堝悓璧风鏃�"
+ :value="detail.contractStartDate || '-'" />
+ <u-cell title="鍚堝悓鍒版湡鏃�"
+ :value="detail.contractEndDate || '-'" />
+ <u-cell title="鎵跨浜�"
+ :value="detail.tenant || '-'" />
+ <u-cell title="鍚堝悓鐢叉柟"
+ :value="detail.partyA || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="璐圭敤淇℃伅"
+ class="detail-section">
+ <u-cell title="鐢佃瘽"
+ :value="detail.phone || '-'" />
+ <u-cell title="闈㈢Н"
+ :value="detail.area != null && detail.area !== '' ? String(detail.area) : '-'" />
+ <u-cell title="鏀粯鏂瑰紡"
+ :value="detail.paymentMethod || '-'" />
+ <u-cell title="鎶奸噾"
+ :value="formatAmount(detail.deposit)" />
+ <u-cell title="绉熼噾(鍏�/鏈�)"
+ :value="formatAmount(detail.rent)" />
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+
+ <view v-if="!loading"
+ class="cost-section">
+ <view class="cost-header">
+ <text class="cost-header-title">鏈堝害璐圭敤</text>
+ <text class="cost-header-tip">{{ costList.length }} 涓湀</text>
+ </view>
+ <scroll-view v-if="costList.length > 0"
+ scroll-x
+ class="cost-scroll">
+ <view class="cost-table">
+ <view class="cost-row cost-head">
+ <text class="cost-cell col-month">鏈堜唤</text>
+ <text class="cost-cell">鎴跨</text>
+ <text class="cost-cell">姘寸數</text>
+ <text class="cost-cell">鐗╀笟</text>
+ <text class="cost-cell">蹇��</text>
+ <text class="cost-cell">瀹藉甫</text>
+ <text class="cost-cell">鏈堝悎璁�</text>
+ </view>
+ <view v-for="row in costList"
+ :key="row.month"
+ class="cost-row">
+ <text class="cost-cell col-month">{{ row.month }}</text>
+ <text class="cost-cell">{{ money(row.rent) }}</text>
+ <text class="cost-cell">{{ money(row.waterElectricity) }}</text>
+ <text class="cost-cell">{{ money(row.property) }}</text>
+ <text class="cost-cell">{{ money(row.express) }}</text>
+ <text class="cost-cell">{{ money(row.broadband) }}</text>
+ <text class="cost-cell cost-total">{{ rowTotal(row) }}</text>
+ </view>
+ <view class="cost-row cost-foot">
+ <text class="cost-cell col-month">绱</text>
+ <text class="cost-cell">{{ totals.rent }}</text>
+ <text class="cost-cell">{{ totals.waterElectricity }}</text>
+ <text class="cost-cell">{{ totals.property }}</text>
+ <text class="cost-cell">{{ totals.express }}</text>
+ <text class="cost-cell">{{ totals.broadband }}</text>
+ <text class="cost-cell cost-total">{{ totals.total }}</text>
+ </view>
+ </view>
+ </scroll-view>
+ <view v-else
+ class="cost-empty">
+ <text>鏆傛棤鏈堝害璐圭敤鏁版嵁</text>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { getWarnInfo } from "@/utils/warning";
+ import { getHouseLeaseDetail } from "@/api/collaborativeApproval/houseLeaseManagement";
+
+ // 鍚堝悓鍒版湡鎻愬墠 30 澶╅璀�
+ const WARN_DAYS = 30;
+
+ const COST_FIELDS = [
+ "rent",
+ "waterElectricity",
+ "property",
+ "express",
+ "broadband",
+ ];
+
+ const loading = ref(true);
+ const leaseId = ref("");
+ const detail = ref({});
+ const costList = ref([]);
+
+ const warn = computed(() =>
+ getWarnInfo(detail.value.contractEndDate, WARN_DAYS)
+ );
+
+ const money = value =>
+ value === null || value === undefined || value === ""
+ ? "-"
+ : Number(value).toFixed(2);
+
+ const rowTotal = row =>
+ COST_FIELDS.reduce((sum, key) => sum + (Number(row[key]) || 0), 0).toFixed(2);
+
+ const totals = computed(() => {
+ const acc = { total: 0 };
+ COST_FIELDS.forEach(key => {
+ acc[key] = 0;
+ });
+ costList.value.forEach(row => {
+ COST_FIELDS.forEach(key => {
+ acc[key] += Number(row[key]) || 0;
+ });
+ acc.total += Number(rowTotal(row)) || 0;
+ });
+ const shown = { total: acc.total.toFixed(2) };
+ COST_FIELDS.forEach(key => {
+ shown[key] = acc[key].toFixed(2);
+ });
+ return shown;
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("houseLeaseDetail", {
+ ...detail.value,
+ costList: costList.value,
+ });
+ uni.navigateTo({
+ url: `/pages/cooperativeOffice/houseLeaseManagement/edit?id=${leaseId.value}`,
+ });
+ };
+
+ const warnTagType = level => {
+ if (level === "overdue") return "error";
+ if (level === "warning") return "warning";
+ if (level === "normal") return "success";
+ return "info";
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const data = await loadDetailData({
+ cacheKey: "houseLeaseDetail",
+ id: leaseId.value,
+ fetcher: getHouseLeaseDetail,
+ });
+ detail.value = data || {};
+ const rows = data?.costList || data?.costVOs || [];
+ costList.value = Array.isArray(rows) ? rows : [];
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇鎴垮眿绉熻祦璇︽儏澶辫触", icon: "none" });
+ detail.value = {};
+ costList.value = [];
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ leaseId.value = options?.id || "";
+ if (!leaseId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .house-lease-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eaf4ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(76, 154, 255, 0.1);
+ }
+
+ .hero-left {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-width: 70%;
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ }
+
+ .hero-sub {
+ font-size: 13px;
+ color: #606266;
+ word-break: break-all;
+ }
+
+ .warn-banner {
+ margin-top: 12px;
+ padding: 10px 14px;
+ border-radius: 10px;
+ }
+
+ .warn-banner-text {
+ font-size: 13px;
+ line-height: 1.5;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+
+ .cost-section {
+ margin: 12px 12px 0;
+ border-radius: 12px;
+ background: #ffffff;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .cost-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 16px 10px;
+ background: #f8fbff;
+ }
+
+ .cost-header-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .cost-header-tip {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .cost-scroll {
+ width: 100%;
+ white-space: nowrap;
+ }
+
+ .cost-table {
+ display: inline-block;
+ min-width: 100%;
+ padding-bottom: 6px;
+ }
+
+ .cost-row {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .cost-row:last-child {
+ border-bottom: none;
+ }
+
+ .cost-head {
+ background: #fafbfc;
+ }
+
+ .cost-head .cost-cell {
+ font-size: 12px;
+ font-weight: 600;
+ color: #606266;
+ }
+
+ .cost-foot {
+ background: #f8fbff;
+ }
+
+ .cost-foot .cost-cell {
+ font-size: 13px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .cost-cell {
+ width: 76px;
+ flex: 0 0 76px;
+ text-align: center;
+ font-size: 13px;
+ color: #333;
+ line-height: 44px;
+ }
+
+ .col-month {
+ width: 78px;
+ flex: 0 0 78px;
+ color: #606266;
+ }
+
+ .cost-total {
+ color: #2979ff;
+ font-weight: 600;
+ }
+
+ .cost-empty {
+ padding: 24px 16px;
+ text-align: center;
+ font-size: 13px;
+ color: #909399;
+ }
+</style>
diff --git a/src/pages/cooperativeOffice/houseLeaseManagement/edit.vue b/src/pages/cooperativeOffice/houseLeaseManagement/edit.vue
new file mode 100644
index 0000000..1fc31b9
--- /dev/null
+++ b/src/pages/cooperativeOffice/houseLeaseManagement/edit.vue
@@ -0,0 +1,603 @@
+<template>
+ <view class="account-detail">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="鎴垮眿涓庝汉鍛�"
+ class="form-section">
+ <up-form-item label="鍔炰簨澶�"
+ prop="office"
+ required
+ border-bottom>
+ <up-input v-model="form.office"
+ placeholder="璇疯緭鍏ュ姙浜嬪"
+ clearable />
+ </up-form-item>
+ <up-form-item label="浣跨敤浜�"
+ prop="userName"
+ required
+ border-bottom>
+ <up-input v-model="form.userName"
+ placeholder="璇疯緭鍏ヤ娇鐢ㄤ汉"
+ clearable />
+ </up-form-item>
+ <up-form-item label="椹荤渷浜哄憳"
+ prop="residentStaff"
+ required
+ border-bottom>
+ <up-input v-model="form.residentStaff"
+ placeholder="璇疯緭鍏ラ┗鐪佷汉鍛�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍦板潃"
+ prop="address"
+ required>
+ <up-textarea v-model="form.address"
+ placeholder="璇疯緭鍏ヨ缁嗗湴鍧�"
+ auto-height
+ maxlength="200" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鍚堝悓淇℃伅"
+ class="form-section">
+ <up-form-item label="璧风鏃堕棿"
+ prop="rentStartDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.rentStartDate"
+ type="date"
+ placeholder="璇烽�夋嫨璧风鏃堕棿" />
+ </up-form-item>
+ <up-form-item label="鍚堝悓璧风鏃�"
+ prop="contractStartDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.contractStartDate"
+ type="date"
+ placeholder="璇烽�夋嫨鍚堝悓璧风鏃�"
+ @change="rebuildMonths" />
+ </up-form-item>
+ <up-form-item label="鍚堝悓鍒版湡鏃�"
+ prop="contractEndDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.contractEndDate"
+ type="date"
+ placeholder="璇烽�夋嫨鍚堝悓鍒版湡鏃�"
+ @change="rebuildMonths" />
+ </up-form-item>
+ <up-form-item label="鎵跨浜�"
+ prop="tenant"
+ required
+ border-bottom>
+ <up-input v-model="form.tenant"
+ placeholder="璇疯緭鍏ユ壙绉熶汉"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍚堝悓鐢叉柟"
+ prop="partyA"
+ required
+ border-bottom>
+ <up-input v-model="form.partyA"
+ placeholder="璇疯緭鍏ュ悎鍚岀敳鏂�"
+ clearable />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="璐圭敤淇℃伅"
+ class="form-section">
+ <up-form-item label="鐢佃瘽"
+ prop="phone"
+ border-bottom>
+ <up-input v-model="form.phone"
+ type="number"
+ placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="闈㈢Н"
+ prop="area"
+ border-bottom>
+ <up-input v-model="form.area"
+ type="number"
+ placeholder="璇疯緭鍏ラ潰绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鏀粯鏂瑰紡"
+ prop="paymentMethod"
+ required
+ border-bottom>
+ <FormPicker v-model="form.paymentMethod"
+ type="select"
+ title="閫夋嫨鏀粯鏂瑰紡"
+ :options="paymentMethodOptions"
+ placeholder="璇烽�夋嫨鏀粯鏂瑰紡" />
+ </up-form-item>
+ <up-form-item label="鎶奸噾"
+ prop="deposit"
+ border-bottom>
+ <up-input v-model="form.deposit"
+ type="number"
+ placeholder="璇疯緭鍏ユ娂閲�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绉熼噾(鍏�/鏈�)"
+ prop="rent"
+ required>
+ <up-input v-model="form.rent"
+ type="number"
+ placeholder="璇疯緭鍏ョ閲�"
+ clearable
+ @change="applyRentToMonths" />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <view class="cost-section">
+ <view class="cost-header">
+ <text class="cost-header-title">鏈堝害璐圭敤</text>
+ <text class="cost-header-tip">{{ costHint }}</text>
+ </view>
+ <scroll-view v-if="costList.length > 0"
+ scroll-x
+ class="cost-scroll">
+ <view class="cost-table">
+ <view class="cost-row cost-head">
+ <text class="cost-cell col-month">鏈堜唤</text>
+ <text class="cost-cell">鎴跨</text>
+ <text class="cost-cell">姘寸數</text>
+ <text class="cost-cell">鐗╀笟</text>
+ <text class="cost-cell">蹇��</text>
+ <text class="cost-cell">瀹藉甫</text>
+ <text class="cost-cell">鏈堝悎璁�</text>
+ </view>
+ <view v-for="row in costList"
+ :key="row.month"
+ class="cost-row">
+ <text class="cost-cell col-month">{{ row.month }}</text>
+ <view class="cost-cell">
+ <up-input v-model="row.rent"
+ type="number"
+ border="none"
+ input-align="center" />
+ </view>
+ <view class="cost-cell">
+ <up-input v-model="row.waterElectricity"
+ type="number"
+ border="none"
+ input-align="center" />
+ </view>
+ <view class="cost-cell">
+ <up-input v-model="row.property"
+ type="number"
+ border="none"
+ input-align="center" />
+ </view>
+ <view class="cost-cell">
+ <up-input v-model="row.express"
+ type="number"
+ border="none"
+ input-align="center" />
+ </view>
+ <view class="cost-cell">
+ <up-input v-model="row.broadband"
+ type="number"
+ border="none"
+ input-align="center" />
+ </view>
+ <text class="cost-cell cost-total">{{ rowTotal(row) }}</text>
+ </view>
+ <view class="cost-row cost-foot">
+ <text class="cost-cell col-month">绱</text>
+ <text class="cost-cell">{{ totals.rent }}</text>
+ <text class="cost-cell">{{ totals.waterElectricity }}</text>
+ <text class="cost-cell">{{ totals.property }}</text>
+ <text class="cost-cell">{{ totals.express }}</text>
+ <text class="cost-cell">{{ totals.broadband }}</text>
+ <text class="cost-cell cost-total">{{ totals.total }}</text>
+ </view>
+ </view>
+ </scroll-view>
+ <view v-else
+ class="cost-empty">
+ <text>{{ costHint }}</text>
+ </view>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </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 FormPicker from "@/components/FormPicker.vue";
+ import { monthRange } from "@/utils/warning";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { PAYMENT_METHOD_OPTIONS } from "@/utils/paymentCycle";
+ import {
+ addOrUpdateHouseLease,
+ getHouseLeaseDetail,
+ } from "@/api/collaborativeApproval/houseLeaseManagement";
+
+ // 鏈堝害璐圭敤浜斾釜閲戦鍒楋紝椤哄簭涓庤〃澶翠竴鑷�
+ const COST_FIELDS = [
+ "rent",
+ "waterElectricity",
+ "property",
+ "express",
+ "broadband",
+ ];
+
+ const formRef = ref();
+ const loading = ref(false);
+ const leaseId = ref("");
+ const costList = ref([]);
+
+ const paymentMethodOptions = PAYMENT_METHOD_OPTIONS;
+
+ // 璁板綍涓婁竴娆¤嚜鍔ㄧ亴鍏ョ殑绉熼噾锛岀敤浜庡垽鏂煇鏈堟埧绉熸槸銆岃嚜鍔ㄥ甫鍑恒�嶈繕鏄�屾墜鏀硅繃銆�
+ const lastAppliedRent = ref("");
+
+ const form = ref({
+ id: undefined,
+ office: "",
+ userName: "",
+ residentStaff: "",
+ address: "",
+ rentStartDate: "",
+ contractStartDate: "",
+ contractEndDate: "",
+ tenant: "",
+ partyA: "",
+ phone: "",
+ area: "",
+ paymentMethod: "",
+ deposit: "",
+ rent: "",
+ });
+
+ const rules = {
+ office: [{ required: true, message: "璇疯緭鍏ュ姙浜嬪", trigger: "blur" }],
+ userName: [{ required: true, message: "璇疯緭鍏ヤ娇鐢ㄤ汉", trigger: "blur" }],
+ residentStaff: [
+ { required: true, message: "璇疯緭鍏ラ┗鐪佷汉鍛�", trigger: "blur" },
+ ],
+ address: [{ required: true, message: "璇疯緭鍏ュ湴鍧�", trigger: "blur" }],
+ rentStartDate: [
+ { required: true, message: "璇烽�夋嫨璧风鏃堕棿", trigger: "change" },
+ ],
+ contractStartDate: [
+ { required: true, message: "璇烽�夋嫨鍚堝悓璧风鏃�", trigger: "change" },
+ ],
+ contractEndDate: [
+ { required: true, message: "璇烽�夋嫨鍚堝悓鍒版湡鏃�", trigger: "change" },
+ ],
+ tenant: [{ required: true, message: "璇疯緭鍏ユ壙绉熶汉", trigger: "blur" }],
+ partyA: [{ required: true, message: "璇疯緭鍏ュ悎鍚岀敳鏂�", trigger: "blur" }],
+ paymentMethod: [
+ { required: true, message: "璇烽�夋嫨鏀粯鏂瑰紡", trigger: "change" },
+ ],
+ rent: [{ required: true, message: "璇疯緭鍏ョ閲�", trigger: "blur" }],
+ };
+
+ const pageTitle = computed(() =>
+ leaseId.value ? "缂栬緫鎴垮眿绉熻祦" : "鏂板鎴垮眿绉熻祦"
+ );
+
+ const costHint = computed(() => {
+ if (!form.value.contractStartDate || !form.value.contractEndDate) {
+ return "璇峰厛閫夋嫨鍚堝悓璧风鏃ヤ笌鍚堝悓鍒版湡鏃ワ紝绯荤粺鎸夋湀浠借嚜鍔ㄧ敓鎴愯垂鐢ㄨ";
+ }
+ return `鍏� ${costList.value.length} 涓湀`;
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ const numOrNull = value =>
+ value === "" || value === null || value === undefined
+ ? null
+ : Number.isNaN(Number(value))
+ ? null
+ : Number(value);
+
+ const rowTotal = row =>
+ COST_FIELDS.reduce((sum, key) => sum + (Number(row[key]) || 0), 0).toFixed(2);
+
+ const totals = computed(() => {
+ const acc = { total: 0 };
+ COST_FIELDS.forEach(key => {
+ acc[key] = 0;
+ });
+ costList.value.forEach(row => {
+ COST_FIELDS.forEach(key => {
+ acc[key] += Number(row[key]) || 0;
+ });
+ acc.total += Number(rowTotal(row)) || 0;
+ });
+ const shown = { total: acc.total.toFixed(2) };
+ COST_FIELDS.forEach(key => {
+ shown[key] = acc[key].toFixed(2);
+ });
+ return shown;
+ });
+
+ /**
+ * 鎸夊悎鍚岃捣姝㈡棩鏈熼噸寤烘湀浠借銆�
+ * 宸插瓨鍦ㄧ殑鏈堜唤鎸� month 淇濈暀鍘熷~鍐欏�硷紝鏂板鏈堜唤鎴跨榛樿鍙栬〃鍗曠閲戙��
+ */
+ const rebuildMonths = () => {
+ const months = monthRange(
+ form.value.contractStartDate,
+ form.value.contractEndDate
+ );
+ const oldMap = new Map(costList.value.map(row => [row.month, row]));
+ costList.value = months.map(month => {
+ const old = oldMap.get(month);
+ if (old) return old;
+ return {
+ month,
+ rent: form.value.rent ?? "",
+ waterElectricity: "",
+ property: "",
+ express: "",
+ broadband: "",
+ };
+ });
+ lastAppliedRent.value = form.value.rent ?? "";
+ };
+
+ // 琛ㄥ崟绉熼噾鍙樺寲鏃讹紝鍙鐩栥�屾湭鎵嬫敼銆嶇殑鏈堝害鎴跨锛堢┖鍊兼垨浠嶇瓑浜庝笂娆¤嚜鍔ㄥ甫鍑虹殑鍊硷級
+ const applyRentToMonths = () => {
+ const next = form.value.rent;
+ costList.value.forEach(row => {
+ const untouched =
+ row.rent === "" ||
+ row.rent === null ||
+ row.rent === undefined ||
+ String(row.rent) === String(lastAppliedRent.value);
+ if (untouched) row.rent = next;
+ });
+ lastAppliedRent.value = next ?? "";
+ };
+
+ const loadDetail = async () => {
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "houseLeaseDetail",
+ id: leaseId.value,
+ fetcher: getHouseLeaseDetail,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鏈壘鍒版埧灞嬬璧佷俊鎭�", icon: "none" });
+ return;
+ }
+
+ form.value = {
+ id: data.id,
+ office: data.office || "",
+ userName: data.userName || "",
+ residentStaff: data.residentStaff || "",
+ address: data.address || "",
+ rentStartDate: (data.rentStartDate || "").slice(0, 10),
+ contractStartDate: (data.contractStartDate || "").slice(0, 10),
+ contractEndDate: (data.contractEndDate || "").slice(0, 10),
+ tenant: data.tenant || "",
+ partyA: data.partyA || "",
+ phone: data.phone || "",
+ area: data.area ?? "",
+ paymentMethod: data.paymentMethod || "",
+ deposit: data.deposit ?? "",
+ rent: data.rent ?? "",
+ };
+
+ const rows = data.costList || data.costVOs || [];
+ costList.value = (Array.isArray(rows) ? rows : []).map(row => ({
+ month: row.month,
+ rent: row.rent ?? "",
+ waterElectricity: row.waterElectricity ?? "",
+ property: row.property ?? "",
+ express: row.express ?? "",
+ broadband: row.broadband ?? "",
+ }));
+ lastAppliedRent.value = form.value.rent ?? "";
+ };
+
+ const buildCostList = () =>
+ costList.value.map(row => ({
+ ...row,
+ rent: numOrNull(row.rent),
+ waterElectricity: numOrNull(row.waterElectricity),
+ property: numOrNull(row.property),
+ express: numOrNull(row.express),
+ broadband: numOrNull(row.broadband),
+ }));
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ const payload = {
+ id: form.value.id,
+ office: form.value.office,
+ userName: form.value.userName,
+ residentStaff: form.value.residentStaff,
+ address: form.value.address,
+ rentStartDate: form.value.rentStartDate,
+ contractStartDate: form.value.contractStartDate,
+ contractEndDate: form.value.contractEndDate,
+ tenant: form.value.tenant,
+ partyA: form.value.partyA,
+ phone: form.value.phone,
+ area: numOrNull(form.value.area),
+ paymentMethod: form.value.paymentMethod,
+ deposit: numOrNull(form.value.deposit),
+ rent: numOrNull(form.value.rent),
+ costList: buildCostList(),
+ };
+
+ loading.value = true;
+ addOrUpdateHouseLease(payload)
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 1000);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ leaseId.value = options.id;
+ form.value.id = options.id;
+ loadDetail();
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+
+ .cost-section {
+ margin: 12px 12px 0;
+ border-radius: 12px;
+ background: #ffffff;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .cost-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 16px 10px;
+ background: #f8fbff;
+ }
+
+ .cost-header-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .cost-header-tip {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .cost-scroll {
+ width: 100%;
+ white-space: nowrap;
+ }
+
+ .cost-table {
+ display: inline-block;
+ min-width: 100%;
+ padding-bottom: 6px;
+ }
+
+ .cost-row {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .cost-row:last-child {
+ border-bottom: none;
+ }
+
+ .cost-head {
+ background: #fafbfc;
+ }
+
+ .cost-head .cost-cell {
+ font-size: 12px;
+ font-weight: 600;
+ color: #606266;
+ line-height: 36px;
+ }
+
+ .cost-foot {
+ background: #f8fbff;
+ }
+
+ .cost-foot .cost-cell {
+ font-size: 13px;
+ font-weight: 600;
+ color: #22324d;
+ line-height: 42px;
+ }
+
+ .cost-cell {
+ width: 76px;
+ flex: 0 0 76px;
+ text-align: center;
+ font-size: 13px;
+ color: #333;
+ line-height: 44px;
+ }
+
+ .col-month {
+ width: 78px;
+ flex: 0 0 78px;
+ color: #606266;
+ }
+
+ .cost-total {
+ color: #2979ff;
+ font-weight: 600;
+ }
+
+ .cost-empty {
+ padding: 24px 16px;
+ text-align: center;
+ font-size: 13px;
+ color: #909399;
+ line-height: 1.6;
+ }
+</style>
diff --git a/src/pages/cooperativeOffice/houseLeaseManagement/index.vue b/src/pages/cooperativeOffice/houseLeaseManagement/index.vue
new file mode 100644
index 0000000..1cd37de
--- /dev/null
+++ b/src/pages/cooperativeOffice/houseLeaseManagement/index.vue
@@ -0,0 +1,255 @@
+<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="query.office"
+ placeholder="璇疯緭鍏ュ姙浜嬪鎼滅储"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="leaseList.length > 0"
+ class="ledger-list">
+ <view v-for="item in leaseList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="home"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.office || "-" }}</text>
+ </view>
+ <up-tag :text="warnOf(item).text || '鏃犲埌鏈熸棩'"
+ :type="warnTagType(warnOf(item).level)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">浣跨敤浜�</text>
+ <text class="detail-value">{{ item.userName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">椹荤渷浜哄憳</text>
+ <text class="detail-value">{{ item.residentStaff || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍦板潃</text>
+ <text class="detail-value">{{ item.address || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵跨浜�</text>
+ <text class="detail-value">{{ item.tenant || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堝悓鐢叉柟</text>
+ <text class="detail-value">{{ item.partyA || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堝悓璧风鏃�</text>
+ <text class="detail-value">{{ item.contractStartDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堝悓鍒版湡鏃�</text>
+ <text class="detail-value">{{ item.contractEndDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绉熼噾(鍏�/鏈�)</text>
+ <text class="detail-value">{{ formatAmount(item.rent) }}</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"
+ @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">
+ <text>鏆傛棤鎴垮眿绉熻祦鏁版嵁</text>
+ </view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鎴垮眿绉熻祦绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ houseLeaseListPage,
+ delHouseLease,
+ } from "@/api/collaborativeApproval/houseLeaseManagement";
+ import { unwrapList } from "@/utils/list";
+ import { getWarnInfo } from "@/utils/warning";
+
+ // 鍚堝悓鍒版湡鎻愬墠 30 澶╅璀�
+ const WARN_DAYS = 30;
+
+ const WARN_STATUS_OPTIONS = [
+ { name: "姝e父", value: "normal" },
+ { name: "鍗冲皢鍒版湡", value: "expiring" },
+ { name: "宸查�炬湡", value: "overdue" },
+ ];
+
+ const leaseList = ref([]);
+ const showFilter = ref(false);
+
+ const query = reactive({
+ office: "",
+ userName: "",
+ tenant: "",
+ partyA: "",
+ warnStatus: "",
+ });
+
+ const filterFields = [
+ { prop: "office", label: "鍔炰簨澶�", type: "input", placeholder: "璇疯緭鍏ュ姙浜嬪" },
+ { prop: "userName", label: "浣跨敤浜�", type: "input", placeholder: "璇疯緭鍏ヤ娇鐢ㄤ汉" },
+ { prop: "tenant", label: "鎵跨浜�", type: "input", placeholder: "璇疯緭鍏ユ壙绉熶汉" },
+ { prop: "partyA", label: "鍚堝悓鐢叉柟", type: "input", placeholder: "璇疯緭鍏ュ悎鍚岀敳鏂�" },
+ {
+ prop: "warnStatus",
+ label: "棰勮鐘舵��",
+ type: "select",
+ options: WARN_STATUS_OPTIONS,
+ },
+ ];
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("houseLeaseDetail");
+ uni.navigateTo({
+ url: "/pages/cooperativeOffice/houseLeaseManagement/edit",
+ });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("houseLeaseDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/cooperativeOffice/houseLeaseManagement/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("houseLeaseDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/cooperativeOffice/houseLeaseManagement/detail?id=${item.id}`,
+ });
+ };
+
+ const warnOf = item => getWarnInfo(item?.contractEndDate, WARN_DAYS);
+
+ const warnTagType = level => {
+ if (level === "overdue") return "error";
+ if (level === "warning") return "warning";
+ if (level === "normal") return "success";
+ return "info";
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const getList = () => {
+ houseLeaseListPage({
+ current: -1,
+ size: -1,
+ office: query.office || undefined,
+ userName: query.userName || undefined,
+ tenant: query.tenant || undefined,
+ partyA: query.partyA || undefined,
+ warnStatus: query.warnStatus || undefined,
+ })
+ .then(res => {
+ leaseList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ユ埧灞嬬璧佽褰曞悧锛�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delHouseLease(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/equipmentManagement/deviceInfo/index.vue b/src/pages/equipmentManagement/deviceInfo/index.vue
index 5972626..b7c4190 100644
--- a/src/pages/equipmentManagement/deviceInfo/index.vue
+++ b/src/pages/equipmentManagement/deviceInfo/index.vue
@@ -6,9 +6,18 @@
<view class="info-card">
<view class="card-header">
<text class="card-title">鍩烘湰淇℃伅</text>
- <text class="device-status">姝e父</text>
+ <text
+ class="device-status"
+ :class="deviceInfo.status === 1 ? 'status-enabled' : 'status-disabled'"
+ >
+ {{ deviceInfo.status === 1 ? '鍚敤' : '鍋滅敤' }}
+ </text>
</view>
<view class="card-content">
+ <view class="info-row">
+ <text class="label">璁惧缂栧彿锛�</text>
+ <text class="value">{{ deviceInfo.uniqueCode || '--' }}</text>
+ </view>
<view class="info-row">
<text class="label">璁惧鍚嶇О锛�</text>
<text class="value">{{ deviceInfo.deviceName || '--' }}</text>
@@ -18,12 +27,99 @@
<text class="value">{{ deviceInfo.deviceModel || '--' }}</text>
</view>
<view class="info-row">
- <text class="label">鐢熶骇鍘傚锛�</text>
- <text class="value">{{ deviceInfo.supplierName || '--' }}</text>
+ <text class="label">璁惧鍝佺墝锛�</text>
+ <text class="value">{{ deviceInfo.deviceBrand || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">璁惧绫诲瀷锛�</text>
+ <text class="value">{{ deviceInfo.type || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">鍑哄巶缂栧彿锛�</text>
+ <text class="value">{{ deviceInfo.factoryNumber || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">瀛樻斁浣嶇疆锛�</text>
+ <text class="value">{{ deviceInfo.storageLocation || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">璐d换浜猴細</text>
+ <text class="value">{{ deviceInfo.responsiblePerson || '--' }}</text>
</view>
<view class="info-row">
<text class="label">鍗曚綅锛�</text>
<text class="value">{{ deviceInfo.unit || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">寤烘。鏃ユ湡锛�</text>
+ <text class="value">{{ formatDate(deviceInfo.filingDate) }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 閲囪喘淇℃伅 -->
+ <view class="info-card">
+ <view class="card-header">
+ <text class="card-title">閲囪喘淇℃伅</text>
+ </view>
+ <view class="card-content">
+ <view class="info-row">
+ <text class="label">渚涘簲鍟嗭細</text>
+ <text class="value">{{ deviceInfo.supplierName || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">鍒堕�犲巶鍟嗭細</text>
+ <text class="value">{{ deviceInfo.manufacturer || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">璐疆鏃ユ湡锛�</text>
+ <text class="value">{{ formatDate(deviceInfo.purchaseDate) }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">璧勪骇鍘熷�硷細</text>
+ <text class="value">{{ deviceInfo.originalAssetValue ?? '--' }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鎶�鏈弬鏁� -->
+ <view class="info-card">
+ <view class="card-header">
+ <text class="card-title">鎶�鏈弬鏁�</text>
+ </view>
+ <view class="card-content">
+ <view class="info-row">
+ <text class="label">涓昏鎶�鏈弬鏁帮細</text>
+ <text class="value">{{ deviceInfo.technicalParameters || '--' }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">涓昏鐢ㄩ�旓細</text>
+ <text class="value">{{ deviceInfo.mainPurpose || '--' }}</text>
+ </view>
+ </view>
+ </view>
+
+ <!-- 杩愯绠$悊 -->
+ <view class="info-card">
+ <view class="card-header">
+ <text class="card-title">杩愯绠$悊</text>
+ </view>
+ <view class="card-content">
+ <view class="info-row">
+ <text class="label">鍚敤鏃ユ湡锛�</text>
+ <text class="value">{{ formatDate(deviceInfo.activationDate) }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">棰勮杩愯鏃堕棿锛�</text>
+ <text class="value">{{ formatDate(deviceInfo.planRuntimeTime) }}</text>
+ </view>
+ <view class="info-row">
+ <text class="label">鍚敤鎶樻棫锛�</text>
+ <text class="value">{{ deviceInfo.isDepr === 1 ? '鏄�' : '鍚�' }}</text>
+ </view>
+ <view v-if="deviceInfo.isDepr === 1" class="info-row">
+ <text class="label">姣忓勾鎶樻棫閲戦锛�</text>
+ <text class="value">{{ deviceInfo.annualDepreciationAmount ?? '--' }}</text>
</view>
</view>
</view>
@@ -49,25 +145,94 @@
</view>
</view>
</view>
+
+ <!-- 鎶ヤ慨璁板綍 -->
+ <view class="info-card">
+ <view class="card-header">
+ <text class="card-title">鎶ヤ慨璁板綍</text>
+ </view>
+ <view class="card-content">
+ <view v-if="repairRecords.length === 0" class="empty-tip">鏆傛棤鎶ヤ慨璁板綍</view>
+ <view v-else class="change-record" v-for="(item, index) in repairRecords" :key="index">
+ <view class="record-header">
+ <text class="record-time">{{ formatDate(item.repairTime) }}</text>
+ <text class="record-status" :class="item.status === 1 ? 'status-approved' : 'status-rejected'">
+ {{ item.status === 1 ? '瀹岀粨' : '寰呯淮淇�' }}
+ </text>
+ </view>
+ <view class="record-reason">鎶ヤ慨浜猴細{{ item.repairName || '--' }}銆�缁翠慨浜猴細{{ item.maintenanceName || '--' }}</view>
+ <view class="record-reason">缁翠慨椤圭洰锛歿{ item.machineryCategory || '--' }}</view>
+ <view class="record-reason">鏁呴殰鐜拌薄锛歿{ item.remark || '--' }}</view>
+ <view class="record-reason">缁翠慨缁撴灉锛歿{ item.maintenanceResult || '--' }}</view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 淇濆吇璁板綍 -->
+ <view class="info-card">
+ <view class="card-header">
+ <text class="card-title">淇濆吇璁板綍</text>
+ </view>
+ <view class="card-content">
+ <view v-if="upkeepRecords.length === 0" class="empty-tip">鏆傛棤淇濆吇璁板綍</view>
+ <view v-else class="change-record" v-for="(item, index) in upkeepRecords" :key="index">
+ <view class="record-header">
+ <text class="record-time">{{ formatDate(item.maintenancePlanTime) }}</text>
+ <text class="record-status" :class="item.status === 1 ? 'status-approved' : 'status-rejected'">
+ {{ item.status === 1 ? '瀹岀粨' : '寰呬繚鍏�' }}
+ </text>
+ </view>
+ <view class="record-reason">淇濆吇椤圭洰锛歿{ item.machineryCategory || '--' }}</view>
+ <view class="record-reason">淇濆吇浜猴細{{ item.maintenancePerson || '--' }}銆�褰曞叆浜猴細{{ item.createUserName || '--' }}</view>
+ <view class="record-reason">瀹為檯淇濆吇鏃ユ湡锛歿{ formatDate(item.maintenanceActuallyTime) }}</view>
+ </view>
+ </view>
+ </view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
+import dayjs from 'dayjs'
import PageHeader from '@/components/PageHeader.vue'
import { getDeviceInfo } from '@/api/equipmentManagement/deviceInfo'
import { listStatusChangeRecord } from '@/api/device/statusChangeRecord'
+import { getRepairPage } from '@/api/equipmentManagement/repair'
+import { getUpkeepPage } from '@/api/equipmentManagement/upkeep'
+import { unwrapList } from '@/utils/list'
const deviceInfo = reactive({
+ uniqueCode: '',
deviceName: '',
deviceModel: '',
+ deviceBrand: '',
+ type: '',
+ factoryNumber: '',
+ storageLocation: '',
+ responsiblePerson: '',
+ filingDate: '',
supplierName: '',
+ manufacturer: '',
+ purchaseDate: '',
+ originalAssetValue: null,
unit: '',
+ technicalParameters: '',
+ mainPurpose: '',
+ status: null,
+ activationDate: '',
+ planRuntimeTime: '',
+ isDepr: 2,
+ annualDepreciationAmount: null,
})
const changeRecords = ref([])
const recordsLoading = ref(false)
+const repairRecords = ref([])
+const upkeepRecords = ref([])
+
+// 鍚庣鏃ユ湡澶氬甫鏃跺垎绉掞紝杩欓噷缁熶竴鍙樉绀哄埌鏃�
+const formatDate = (d) => (d ? dayjs(d).format('YYYY-MM-DD') : '--')
const statusText = (s) => (s == 1 ? '鍚敤' : '鍋滅敤')
@@ -112,6 +277,18 @@
uni.navigateBack()
}
+// 鎶ヤ慨/淇濆吇鍒楄〃鎺ュ彛鍙彁渚� deviceName 杩囨护锛堜笌绠$悊绔竴鑷达紝鏃� deviceId 缁村害锛�
+const fetchDeviceHistory = async (deviceName) => {
+ if (!deviceName) return
+ const params = { current: -1, size: -1, deviceName }
+ const [repair, upkeep] = await Promise.all([
+ getRepairPage(params).catch(() => null),
+ getUpkeepPage(params).catch(() => null),
+ ])
+ repairRecords.value = unwrapList(repair)
+ upkeepRecords.value = unwrapList(upkeep)
+}
+
onLoad((options) => {
const deviceId = options?.deviceId || ''
if (!deviceId) {
@@ -119,6 +296,9 @@
return
}
fetchDeviceInfo(deviceId)
+ // 灞ュ巻閲岀殑鎶ヤ慨/淇濆吇璁板綍瑕佺瓑鍩烘湰淇℃伅鎷垮埌璁惧鍚嶇О鍚庢墠鑳芥煡
+ .then(() => fetchDeviceHistory(deviceInfo.deviceName))
+ .catch(() => {})
fetchChangeRecords(deviceId)
})
</script>
@@ -161,6 +341,14 @@
background: #52c41a;
}
+.status-enabled {
+ background: #52c41a;
+}
+
+.status-disabled {
+ background: #bfbfbf;
+}
+
.card-content {
padding: 16px 20px;
}
@@ -176,8 +364,10 @@
}
.label {
- width: 88px;
+ /* 銆屼富瑕佹妧鏈弬鏁帮細銆嶆瘮鍘熸潵鏈�闀跨殑鏍囩鏇村锛屾敼鎴愭寜鍐呭鎾戝紑锛岄伩鍏嶆崲琛屾垨鎴柇 */
+ min-width: 88px;
flex-shrink: 0;
+ white-space: nowrap;
color: #666666;
font-size: 14px;
}
diff --git a/src/pages/equipmentManagement/ledger/detail.vue b/src/pages/equipmentManagement/ledger/detail.vue
index 1b4ca9f..8a2925d 100644
--- a/src/pages/equipmentManagement/ledger/detail.vue
+++ b/src/pages/equipmentManagement/ledger/detail.vue
@@ -7,6 +7,21 @@
<u-form @submit="sendForm" ref="formRef" :model="form" :rules="formRules" label-width="110">
<!-- 鍩烘湰淇℃伅 -->
<u-cell-group title="鍩烘湰淇℃伅">
+ <u-form-item label="璁惧缂栧彿" prop="uniqueCode" border-bottom>
+ <u-input
+ v-model="form.uniqueCode"
+ :disabled="form.autoGenerateUniqueCode || operationType === 'edit'"
+ :placeholder="form.autoGenerateUniqueCode ? '淇濆瓨鍚庤嚜鍔ㄧ敓鎴�' : '璇疯緭鍏ヨ澶囩紪鍙�'"
+ clearable
+ />
+ </u-form-item>
+ <u-form-item v-if="operationType !== 'edit'" label="鑷姩鐢熸垚缂栧彿" prop="autoGenerateUniqueCode" border-bottom>
+ <u-switch
+ v-model="form.autoGenerateUniqueCode"
+ :activeValue="true"
+ :inactiveValue="false"
+ />
+ </u-form-item>
<u-form-item label="璁惧鍚嶇О" prop="deviceName" required border-bottom>
<u-input
v-model="form.deviceName"
@@ -22,13 +37,29 @@
clearable
/>
</u-form-item>
- <!-- <u-form-item label="璁惧鍝佺墝" prop="deviceBrand" required border-bottom>
+ <u-form-item label="璁惧鍝佺墝" prop="deviceBrand" border-bottom>
<u-input
v-model="form.deviceBrand"
placeholder="璇疯緭鍏ヨ澶囧搧鐗�"
clearable
/>
- </u-form-item> -->
+ </u-form-item>
+ <u-form-item label="璁惧绫诲瀷" prop="type" required border-bottom>
+ <FormPicker
+ v-model="form.type"
+ type="select"
+ title="閫夋嫨璁惧绫诲瀷"
+ :options="deviceTypeOptions"
+ placeholder="璇烽�夋嫨璁惧绫诲瀷"
+ />
+ </u-form-item>
+ <u-form-item label="鍑哄巶缂栧彿" prop="factoryNumber" border-bottom>
+ <u-input
+ v-model="form.factoryNumber"
+ placeholder="璇疯緭鍏ュ嚭鍘傜紪鍙�"
+ clearable
+ />
+ </u-form-item>
<u-form-item label="渚涘簲鍟�" prop="supplierName" required border-bottom>
<u-input
v-model="form.supplierName"
@@ -36,13 +67,27 @@
clearable
/>
</u-form-item>
- <!-- <u-form-item label="瀛樻斁浣嶇疆" prop="storageLocation" required border-bottom>
+ <u-form-item label="瀛樻斁浣嶇疆" prop="storageLocation" border-bottom>
<u-input
v-model="form.storageLocation"
placeholder="璇疯緭鍏ュ瓨鏀句綅缃�"
clearable
/>
- </u-form-item> -->
+ </u-form-item>
+ <u-form-item label="璐d换浜�" prop="responsiblePerson" border-bottom>
+ <u-input
+ v-model="form.responsiblePerson"
+ placeholder="璇疯緭鍏ヨ矗浠讳汉"
+ clearable
+ />
+ </u-form-item>
+ <u-form-item label="寤烘。鏃ユ湡" prop="filingDate" border-bottom>
+ <FormPicker
+ v-model="form.filingDate"
+ type="date"
+ placeholder="璇烽�夋嫨寤烘。鏃ユ湡"
+ />
+ </u-form-item>
<u-form-item label="鍗曚綅" prop="unit" required border-bottom>
<u-input
v-model="form.unit"
@@ -50,13 +95,6 @@
clearable
/>
</u-form-item>
- <!-- <u-form-item label="鍚敤鎶樻棫" prop="enableDepreciation" required border-bottom>
- <u-switch
- v-model="form.enableDepreciation"
- :active-value="true"
- :inactive-value="false"
- />
- </u-form-item> -->
<u-form-item label="鏁伴噺" prop="number" required border-bottom>
<u-input
v-model="form.number"
@@ -122,7 +160,99 @@
</template>
</u-form-item>
</u-cell-group>
-
+
+ <u-cell-group title="閲囪喘淇℃伅">
+ <u-form-item label="鍒堕�犲巶鍟�" prop="manufacturer" border-bottom>
+ <u-input
+ v-model="form.manufacturer"
+ placeholder="璇疯緭鍏ュ埗閫犲巶鍟�"
+ clearable
+ />
+ </u-form-item>
+ <u-form-item label="璐疆鏃ユ湡" prop="purchaseDate" border-bottom>
+ <FormPicker
+ v-model="form.purchaseDate"
+ type="date"
+ placeholder="璇烽�夋嫨璐疆鏃ユ湡"
+ />
+ </u-form-item>
+ <u-form-item label="璧勪骇鍘熷��" prop="originalAssetValue" border-bottom>
+ <u-input
+ v-model="form.originalAssetValue"
+ type="number"
+ placeholder="璇疯緭鍏ヨ祫浜у師鍊�"
+ clearable
+ />
+ </u-form-item>
+ </u-cell-group>
+ <u-cell-group title="鎶�鏈弬鏁�">
+ <u-form-item label="涓昏鎶�鏈弬鏁�" prop="technicalParameters" border-bottom>
+ <up-textarea
+ v-model="form.technicalParameters"
+ placeholder="璇疯緭鍏ヤ富瑕佹妧鏈弬鏁�"
+ :maxlength="500"
+ count
+ auto-height
+ />
+ </u-form-item>
+ <u-form-item label="涓昏鐢ㄩ��" prop="mainPurpose" border-bottom>
+ <up-textarea
+ v-model="form.mainPurpose"
+ placeholder="璇疯緭鍏ヤ富瑕佺敤閫�"
+ :maxlength="500"
+ count
+ auto-height
+ />
+ </u-form-item>
+ </u-cell-group>
+ <u-cell-group title="杩愯绠$悊">
+ <u-form-item label="璁惧鐘舵��" prop="status" required border-bottom>
+ <FormPicker
+ v-model="form.status"
+ type="select"
+ title="閫夋嫨璁惧鐘舵��"
+ :options="statusOptions"
+ placeholder="璇烽�夋嫨璁惧鐘舵��"
+ />
+ </u-form-item>
+ <u-form-item label="鍚敤鏃ユ湡" prop="activationDate" border-bottom>
+ <FormPicker
+ v-model="form.activationDate"
+ type="date"
+ placeholder="璇烽�夋嫨鍚敤鏃ユ湡"
+ />
+ </u-form-item>
+ <u-form-item label="棰勮杩愯鏃堕棿" prop="planRuntimeTime" border-bottom>
+ <FormPicker
+ v-model="form.planRuntimeTime"
+ type="date"
+ placeholder="璇烽�夋嫨棰勮杩愯鏃堕棿"
+ />
+ </u-form-item>
+ <u-form-item label="鍚敤鎶樻棫" prop="isDepr" border-bottom>
+ <u-switch
+ v-model="form.isDepr"
+ :activeValue="1"
+ :inactiveValue="2"
+ />
+ </u-form-item>
+ <u-form-item v-if="form.isDepr === 1" label="姣忓勾鎶樻棫閲戦" prop="annualDepreciationAmount" border-bottom>
+ <u-input
+ v-model="form.annualDepreciationAmount"
+ type="number"
+ placeholder="璇疯緭鍏ユ瘡骞存姌鏃ч噾棰�"
+ clearable
+ />
+ </u-form-item>
+ <u-form-item label="璁惧鍥剧墖" border-bottom>
+ <CommonUpload
+ v-model="imageFiles"
+ :maxCount="20"
+ accept="image"
+ />
+ </u-form-item>
+ </u-cell-group>
+
<!-- 鎻愪氦鎸夐挳 -->
<view class="footer-btns">
<u-button class="cancel-btn" @click="goBack">鍙栨秷</u-button>
@@ -148,7 +278,10 @@
import { ref, computed, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import PageHeader from '@/components/PageHeader.vue';
+import FormPicker from '@/components/FormPicker.vue';
+import CommonUpload from '@/components/CommonUpload.vue';
import { getLedgerById, addLedger, editLedger } from '@/api/equipmentManagement/ledger';
+import { toFileList } from '@/utils/file';
import dayjs from "dayjs";
import {
calculateTaxIncludeTotalPrice,
@@ -182,9 +315,8 @@
const formRules = {
deviceName: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
deviceModel: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
- deviceBrand: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ type: [{ required: true, trigger: "change", message: "璇烽�夋嫨璁惧绫诲瀷" }],
supplierName: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
- storageLocation: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
unit: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
// 鏁板瓧绫诲瀷瀛楁闇�瑕佺壒娈婂鐞嗭紝纭繚鏈夋暟鍊兼椂涓嶄細瑙﹀彂蹇呭~鏍¢獙
number: [{
@@ -227,23 +359,68 @@
}
}],
createTime: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+ status: [{ required: true, trigger: "change", message: "璇烽�夋嫨璁惧鐘舵��" }],
+ planRuntimeTime: [{ required: true, trigger: "change", message: "璇烽�夋嫨棰勮杩愯鏃堕棿" }],
};
+
+// 璁惧绫诲瀷鍥哄畾閫夐」锛堜笌绠$悊绔竴鑷达紝鏃犳硶鍛戒腑鐨勫�间細鐢� FormPicker 鐨勯�夐」灞曠ず鍏滃簳锛�
+const deviceTypeOptions = ref([
+ { name: '鐢熶骇璁惧', value: '鐢熶骇璁惧' },
+ { name: '鍔炲叕璁惧', value: '鍔炲叕璁惧' },
+ { name: '妫�娴嬭澶�', value: '妫�娴嬭澶�' },
+ { name: '杩愯緭璁惧', value: '杩愯緭璁惧' },
+ { name: '鍏朵粬璁惧', value: '鍏朵粬璁惧' },
+]);
+const statusOptions = ref([
+ { name: '鍋滅敤', value: 0 },
+ { name: '鍚敤', value: 1 },
+]);
// 浣跨敤 ref 澹版槑琛ㄥ崟鏁版嵁
const form = ref({
+ // 鍩烘湰淇℃伅
+ autoGenerateUniqueCode: true, // 鑷姩鐢熸垚缂栧彿
+ uniqueCode: undefined, // 璁惧缂栧彿
deviceName: undefined, // 璁惧鍚嶇О
deviceModel: undefined, // 瑙勬牸鍨嬪彿
deviceBrand: undefined, // 璁惧鍝佺墝
- supplierName: undefined, // 渚涘簲鍟�
+ type: undefined, // 璁惧绫诲瀷
+ factoryNumber: undefined, // 鍑哄巶缂栧彿
storageLocation: undefined, // 瀛樻斁浣嶇疆
+ responsiblePerson: undefined, // 璐d换浜�
+ filingDate: dayjs().format("YYYY-MM-DD"), // 寤烘。鏃ユ湡
+ // 閲囪喘淇℃伅
+ supplierName: undefined, // 渚涘簲鍟�
+ manufacturer: undefined, // 鍒堕�犲巶鍟�
+ purchaseDate: undefined, // 璐疆鏃ユ湡
+ originalAssetValue: undefined, // 璧勪骇鍘熷��
unit: undefined, // 鍗曚綅
- enableDepreciation: false, // 鍚敤鎶樻棫
- number: undefined, // 鏁伴噺
+ number: 1, // 鏁伴噺
taxIncludingPriceUnit: undefined, // 鍚◣鍗曚环
taxIncludingPriceTotal: undefined, // 鍚◣鎬讳环
taxRate: undefined, // 绋庣巼
unTaxIncludingPriceTotal: undefined, // 涓嶅惈绋庢�讳环
+ // 鎶�鏈弬鏁�
+ technicalParameters: undefined, // 涓昏鎶�鏈弬鏁�
+ mainPurpose: undefined, // 涓昏鐢ㄩ��
+ // 杩愯绠$悊
+ status: 1, // 璁惧鐘舵�� 0鍋滅敤 1鍚敤
+ activationDate: undefined, // 鍚敤鏃ユ湡
+ planRuntimeTime: dayjs().format("YYYY-MM-DD"), // 棰勮杩愯鏃堕棿
+ isDepr: 2, // 鏄惁鍚敤鎶樻棫 1鏄� 2鍚�
+ annualDepreciationAmount: undefined, // 姣忓勾鎶樻棫閲戦
+ storageBlobDTOs: undefined, // 璁惧鍥剧墖锛堟彁浜わ級
+ storageBlobVOs: undefined, // 璁惧鍥剧墖锛堝洖鏄撅級
createTime: dayjs().format("YYYY-MM-DD"), // 褰曞叆鏃ユ湡
+});
+
+// 璁惧鍥剧墖锛欳ommonUpload 缁� modelValue锛屾敼鍔ㄦ椂鍚屾椂鍐欏洖鎻愪氦/鍥炴樉涓や釜瀛楁
+const imageFiles = computed({
+ get: () => form.value.storageBlobVOs || [],
+ set: val => {
+ form.value.storageBlobDTOs = val;
+ form.value.storageBlobVOs = val;
+ },
});
@@ -258,20 +435,53 @@
console.log(data);
if (code == 200) {
+ // 鍩烘湰淇℃伅
+ form.value.autoGenerateUniqueCode = false;
+ form.value.uniqueCode = data.uniqueCode;
form.value.deviceName = data.deviceName;
form.value.deviceModel = data.deviceModel;
form.value.deviceBrand = data.deviceBrand || '';
- form.value.supplierName = data.supplierName;
+ form.value.type = data.type;
+ form.value.factoryNumber = data.factoryNumber;
form.value.storageLocation = data.storageLocation || '';
+ form.value.responsiblePerson = data.responsiblePerson;
+ form.value.filingDate = data.filingDate
+ ? dayjs(data.filingDate).format("YYYY-MM-DD")
+ : undefined;
+
+ // 閲囪喘淇℃伅
+ form.value.supplierName = data.supplierName;
+ form.value.manufacturer = data.manufacturer;
+ form.value.purchaseDate = data.purchaseDate
+ ? dayjs(data.purchaseDate).format("YYYY-MM-DD")
+ : undefined;
+ form.value.originalAssetValue = data.originalAssetValue;
form.value.unit = data.unit;
- form.value.enableDepreciation = !!data.enableDepreciation;
form.value.number = data.number;
form.value.taxIncludingPriceUnit = data.taxIncludingPriceUnit;
form.value.taxIncludingPriceTotal = data.taxIncludingPriceTotal;
form.value.taxRate = data.taxRate;
form.value.unTaxIncludingPriceTotal = data.unTaxIncludingPriceTotal;
+
+ // 鎶�鏈弬鏁�
+ form.value.technicalParameters = data.technicalParameters;
+ form.value.mainPurpose = data.mainPurpose;
+
+ // 杩愯绠$悊
+ form.value.status = data.status != null ? data.status : 1;
+ form.value.activationDate = data.activationDate
+ ? dayjs(data.activationDate).format("YYYY-MM-DD")
+ : undefined;
+ form.value.planRuntimeTime = data.planRuntimeTime
+ ? dayjs(data.planRuntimeTime).format("YYYY-MM-DD")
+ : undefined;
+ form.value.isDepr = data.isDepr != null ? data.isDepr : 2;
+ form.value.annualDepreciationAmount = data.annualDepreciationAmount;
+ form.value.storageBlobVOs = toFileList(data.storageBlobVOs);
+ form.value.storageBlobDTOs = form.value.storageBlobVOs;
+
form.value.createTime = data.createTime;
-
+
// 鏁版嵁鍔犺浇瀹屾垚鍚庯紝閲嶇疆琛ㄥ崟楠岃瘉鐘舵��
setTimeout(() => {
clearValidate();
@@ -284,12 +494,11 @@
// 鏁板璁$畻
const mathNum = () => {
- // 鍙湁鍦ㄦ柊澧炴ā寮忔垨鑰呭瓧娈电‘瀹炰负绌烘椂鎵嶆樉绀烘彁绀�
- if (operationType.value !== 'edit' || (form.value.taxIncludingPriceUnit === undefined || form.value.taxIncludingPriceUnit === '')) {
+ if (!form.value.taxIncludingPriceUnit) {
showToast("璇疯緭鍏ュ崟浠�");
return;
}
- if (operationType.value !== 'edit' || (form.value.number === undefined || form.value.number === '')) {
+ if (!form.value.number) {
showToast("璇疯緭鍏ユ暟閲�");
return;
}
@@ -318,18 +527,33 @@
let errorMessage = '';
// 妫�鏌ユ枃鏈被鍨嬪繀濉瓧娈�
- if (!form.value.deviceName) {
+ if (!form.value.autoGenerateUniqueCode && !form.value.uniqueCode) {
+ isValid = false;
+ errorMessage = '璇疯緭鍏ヨ澶囩紪鍙�';
+ } else if (!form.value.deviceName) {
isValid = false;
errorMessage = '璇疯緭鍏ヨ澶囧悕绉�';
} else if (!form.value.deviceModel) {
isValid = false;
errorMessage = '璇疯緭鍏ヨ鏍煎瀷鍙�';
+ } else if (!form.value.type) {
+ isValid = false;
+ errorMessage = '璇烽�夋嫨璁惧绫诲瀷';
} else if (!form.value.supplierName) {
isValid = false;
errorMessage = '璇疯緭鍏ヤ緵搴斿晢';
} else if (!form.value.unit) {
isValid = false;
errorMessage = '璇疯緭鍏ュ崟浣�';
+ } else if (form.value.status === undefined || form.value.status === null || form.value.status === '') {
+ isValid = false;
+ errorMessage = '璇烽�夋嫨璁惧鐘舵��';
+ } else if (form.value.isDepr === 1 && !form.value.annualDepreciationAmount) {
+ isValid = false;
+ errorMessage = '璇疯緭鍏ユ瘡骞存姌鏃ч噾棰�';
+ } else if (!form.value.planRuntimeTime) {
+ isValid = false;
+ errorMessage = '璇烽�夋嫨棰勮杩愯鏃堕棿';
}
// 妫�鏌ユ暟瀛楃被鍨嬪繀濉瓧娈�
diff --git a/src/pages/equipmentManagement/ledger/index.vue b/src/pages/equipmentManagement/ledger/index.vue
index fbe02e0..123e4a8 100644
--- a/src/pages/equipmentManagement/ledger/index.vue
+++ b/src/pages/equipmentManagement/ledger/index.vue
@@ -37,8 +37,29 @@
<view class="item-details">
<view class="detail-row">
+ <text class="detail-label">璁惧缂栧彿</text>
+ <text class="detail-value">{{ item.uniqueCode || '-' }}</text>
+ </view>
+ <view class="detail-row">
<text class="detail-label">瑙勬牸鍨嬪彿</text>
<text class="detail-value">{{ item.deviceModel || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁惧绫诲瀷</text>
+ <text class="detail-value">{{ item.type || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璐d换浜�</text>
+ <text class="detail-value">{{ item.responsiblePerson || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁惧鐘舵��</text>
+ <text
+ class="detail-value"
+ :class="item.status === 1 ? 'status-enabled' : 'status-disabled'"
+ >
+ {{ item.status === 1 ? '鍚敤' : '鍋滅敤' }}
+ </text>
</view>
<view class="detail-row">
<text class="detail-label">渚涘簲鍟�</text>
@@ -221,4 +242,12 @@
@import '@/styles/sales-common.scss';
// 璁惧鍙拌处鐗规湁鏍峰紡锛堟墍鏈夋牱寮忛兘宸插寘鍚湪鍏叡鏍峰紡涓級
+
+.status-enabled {
+ color: #67c23a;
+}
+
+.status-disabled {
+ color: #f56c6c;
+}
</style>
\ No newline at end of file
diff --git a/src/pages/inventoryManagement/fuelCard/detail.vue b/src/pages/inventoryManagement/fuelCard/detail.vue
new file mode 100644
index 0000000..3cd08cc
--- /dev/null
+++ b/src/pages/inventoryManagement/fuelCard/detail.vue
@@ -0,0 +1,273 @@
+<template>
+ <view class="fuel-card-detail">
+ <PageHeader title="鍔犳补鍗¤鎯�"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <text class="hero-title">{{ detail.cardNumber || "-" }}</text>
+ <up-tag :text="statusLabel(detail.status)"
+ :type="detail.status === 'CANCELLED' ? 'error' : 'success'"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <u-cell-group title="鍔犳补鍗′俊鎭�"
+ class="detail-section">
+ <u-cell title="鍔犳补鍗″彿"
+ :value="detail.cardNumber || '-'" />
+ <u-cell title="鎵�灞為儴闂�"
+ :value="detail.deptName || '-'" />
+ <u-cell title="鍙戝崱鍗曚綅"
+ :value="detail.issuer || '-'" />
+ <u-cell title="鏈熷垵浣欓"
+ :value="formatAmount(detail.initialBalance)" />
+ <u-cell title="褰撳墠浣欓"
+ :value="formatAmount(detail.currentBalance)" />
+ <u-cell title="鐘舵��"
+ :value="statusLabel(detail.status)" />
+ <u-cell title="澶囨敞"
+ :value="detail.remark || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鍏呭�艰褰�"
+ class="detail-section">
+ <view class="section-tools">
+ <up-button type="primary"
+ size="small"
+ text="鍏呭�肩鐞�"
+ @click="goRecharge" />
+ </view>
+ <view v-if="rechargeList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤鍏呭�艰褰�</text>
+ </view>
+ <view v-else
+ class="recharge-list">
+ <view v-for="row in rechargeList"
+ :key="row.id"
+ class="recharge-item">
+ <view class="recharge-main">
+ <text class="recharge-amount">+{{ Number(row.rechargeAmount || 0).toFixed(2) }}</text>
+ <text class="recharge-date">{{ row.rechargeDate || "-" }}</text>
+ </view>
+ <view class="recharge-meta">
+ <text v-if="row.createUserName"
+ class="recharge-user">{{ row.createUserName }}</text>
+ <text v-if="row.remark"
+ class="recharge-remark">{{ row.remark }}</text>
+ </view>
+ </view>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { unwrapList } from "@/utils/list";
+ import {
+ listFuelCardPage,
+ listRecharge,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const loading = ref(true);
+ const cardId = ref("");
+ const detail = ref({});
+ const rechargeList = ref([]);
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("fuelCardDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelCard/edit?id=${cardId.value}`,
+ });
+ };
+
+ const goRecharge = () => {
+ uni.setStorageSync("fuelCardDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelCard/recharge?id=${cardId.value}`,
+ });
+ };
+
+ const statusLabel = status => (status === "CANCELLED" ? "娉ㄩ攢" : "姝e父");
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ // 鍔犳补鍗℃病鏈夊崟鏉¤鎯呮帴鍙o紝浠庡垪琛ㄩ噷鎸� id 鎹�
+ const loadCard = async () => {
+ const cached = uni.getStorageSync("fuelCardDetail");
+ if (cached && String(cached.id) === String(cardId.value)) {
+ detail.value = cached;
+ }
+ try {
+ const res = await listFuelCardPage({ current: -1, size: -1 });
+ const found = unwrapList(res).find(
+ item => String(item.id) === String(cardId.value)
+ );
+ if (found) detail.value = found;
+ } catch (error) {
+ // 鎷変笉鍒板氨娌跨敤缂撳瓨
+ }
+ };
+
+ const loadRecharges = () => {
+ listRecharge({ current: -1, size: -1, cardId: cardId.value })
+ .then(res => {
+ rechargeList.value = unwrapList(res);
+ })
+ .catch(() => {
+ rechargeList.value = [];
+ });
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ await loadCard();
+ loadRecharges();
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ cardId.value = options?.id || "";
+ if (!cardId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .fuel-card-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .section-tools {
+ display: flex;
+ justify-content: flex-end;
+ padding: 12px 12px 0;
+ }
+
+ .empty-text {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .recharge-list {
+ padding: 6px 12px 12px;
+ }
+
+ .recharge-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .recharge-item:last-child {
+ border-bottom: none;
+ }
+
+ .recharge-main {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .recharge-amount {
+ font-size: 15px;
+ font-weight: 600;
+ color: #19be6b;
+ }
+
+ .recharge-date {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .recharge-meta {
+ display: flex;
+ gap: 12px;
+ margin-top: 4px;
+ }
+
+ .recharge-user,
+ .recharge-remark {
+ font-size: 12px;
+ color: #909399;
+ word-break: break-all;
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/fuelCard/edit.vue b/src/pages/inventoryManagement/fuelCard/edit.vue
new file mode 100644
index 0000000..207d77b
--- /dev/null
+++ b/src/pages/inventoryManagement/fuelCard/edit.vue
@@ -0,0 +1,223 @@
+<template>
+ <view class="account-detail">
+ <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="cardNumber"
+ required>
+ <up-input v-model="form.cardNumber"
+ placeholder="璇疯緭鍏ュ姞娌瑰崱鍙�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎵�灞為儴闂�"
+ prop="deptId"
+ required>
+ <FormPicker v-model="form.deptId"
+ type="select"
+ title="閫夋嫨鎵�灞為儴闂�"
+ :options="deptOptions"
+ placeholder="璇烽�夋嫨鎵�灞為儴闂�"
+ @change="onDeptChange" />
+ </up-form-item>
+ <up-form-item label="鍙戝崱鍗曚綅"
+ prop="issuer"
+ required>
+ <up-input v-model="form.issuer"
+ placeholder="璇疯緭鍏ュ彂鍗″崟浣�"
+ clearable />
+ </up-form-item>
+ <!-- 鏈熷垵浣欓鍙湪鏂板鏃跺彲濉紝缂栬緫鏃朵綑棰濈敱鍏呭��/鍔犳补璁板綍缁存姢 -->
+ <up-form-item v-if="!cardId"
+ label="鏈熷垵浣欓"
+ prop="initialBalance">
+ <up-input v-model="form.initialBalance"
+ type="number"
+ placeholder="璇疯緭鍏ユ湡鍒濅綑棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鐘舵��"
+ prop="status"
+ required>
+ <FormPicker v-model="form.status"
+ type="select"
+ title="閫夋嫨鐘舵��"
+ :options="STATUS_OPTIONS"
+ placeholder="璇烽�夋嫨鐘舵��" />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ auto-height />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FormPicker from "@/components/FormPicker.vue";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess";
+ import { loadDetailData } from "@/utils/detailCache";
+ import {
+ addFuelCard,
+ listFuelCardPage,
+ updateFuelCard,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const STATUS_OPTIONS = [
+ { name: "姝e父", value: "ACTIVE" },
+ { name: "娉ㄩ攢", value: "CANCELLED" },
+ ];
+
+ const formRef = ref();
+ const loading = ref(false);
+ const cardId = ref("");
+ const deptOptions = ref([]);
+
+ const form = ref({
+ id: undefined,
+ cardNumber: "",
+ deptId: "",
+ deptName: "",
+ issuer: "",
+ initialBalance: 0,
+ status: "ACTIVE",
+ remark: "",
+ });
+
+ const rules = {
+ cardNumber: [{ required: true, message: "璇疯緭鍏ュ姞娌瑰崱鍙�", trigger: "blur" }],
+ deptId: [{ required: true, message: "璇烽�夋嫨鎵�灞為儴闂�", trigger: "change" }],
+ issuer: [{ required: true, message: "璇疯緭鍏ュ彂鍗″崟浣�", trigger: "blur" }],
+ status: [{ required: true, message: "璇烽�夋嫨鐘舵��", trigger: "change" }],
+ };
+
+ const pageTitle = computed(() => (cardId.value ? "缂栬緫鍔犳补鍗�" : "鏂板鍔犳补鍗�"));
+
+ const goBack = () => uni.navigateBack();
+
+ const onDeptChange = (value, item) => {
+ form.value.deptName = item?.name || "";
+ };
+
+ const loadDepts = async () => {
+ try {
+ const res = await getDept();
+ deptOptions.value = (res?.data || []).map(item => ({
+ name: item.deptName,
+ value: item.deptId,
+ }));
+ } catch (error) {
+ deptOptions.value = [];
+ }
+ };
+
+ // 鍔犳补鍗℃病鏈夊崟鏉¤鎯呮帴鍙o紝浠庡垪琛ㄩ噷鎸� id 鎹�
+ const fetchCard = async id => {
+ const res = await listFuelCardPage({ current: -1, size: -1 });
+ const rows = res?.data?.records || res?.records || res?.data || [];
+ const found = (Array.isArray(rows) ? rows : []).find(
+ item => String(item.id) === String(id)
+ );
+ return { data: found };
+ };
+
+ const loadDetail = async () => {
+ if (!cardId.value) return;
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "fuelCardDetail",
+ id: cardId.value,
+ fetcher: fetchCard,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鑾峰彇鍔犳补鍗¤鎯呭け璐�", icon: "none" });
+ return;
+ }
+ form.value = { ...form.value, ...data, id: data.id };
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ loading.value = true;
+ const action = cardId.value ? updateFuelCard : addFuelCard;
+ action({ ...form.value, id: cardId.value || undefined })
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ cardId.value = options.id;
+ form.value.id = options.id;
+ }
+ });
+
+ onMounted(async () => {
+ await loadDepts();
+ if (cardId.value) await loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/fuelCard/index.vue b/src/pages/inventoryManagement/fuelCard/index.vue
new file mode 100644
index 0000000..8e184c0
--- /dev/null
+++ b/src/pages/inventoryManagement/fuelCard/index.vue
@@ -0,0 +1,282 @@
+<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="query.cardNumber"
+ placeholder="璇疯緭鍏ュ姞娌瑰崱鍙锋悳绱�"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="cardList.length > 0"
+ class="ledger-list">
+ <view v-for="item in cardList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="rmb-circle"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.cardNumber || "-" }}</text>
+ </view>
+ <up-tag :text="statusLabel(item.status)"
+ :type="item.status === 'CANCELLED' ? 'error' : 'success'"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鎵�灞為儴闂�</text>
+ <text class="detail-value">{{ item.deptName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙戝崱鍗曚綅</text>
+ <text class="detail-value">{{ item.issuer || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏈熷垵浣欓</text>
+ <text class="detail-value">{{ formatAmount(item.initialBalance) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">褰撳墠浣欓</text>
+ <text class="detail-value highlight">{{ formatAmount(item.currentBalance) }}</text>
+ </view>
+ <view class="detail-row"
+ v-if="item.remark">
+ <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="success"
+ @click="goRecharge(item)">鍏呭�肩鐞�</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="goEdit(item)">缂栬緫</up-button>
+ <up-button class="action-btn"
+ size="small"
+ @click="goDetail(item)">璇︽儏</up-button>
+ <up-button v-if="item.status !== 'CANCELLED'"
+ class="action-btn"
+ size="small"
+ type="warning"
+ plain
+ @click="handleCancelCard(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">
+ <text>鏆傛棤鍔犳补鍗℃暟鎹�</text>
+ </view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鍔犳补鍗$瓫閫�"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess";
+ import { unwrapList } from "@/utils/list";
+ import {
+ delFuelCard,
+ listFuelCardPage,
+ updateFuelCard,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const cardList = ref([]);
+ const showFilter = ref(false);
+ const deptOptions = ref([]);
+
+ const query = reactive({
+ cardNumber: "",
+ deptId: "",
+ });
+
+ const filterFields = computed(() => [
+ {
+ prop: "cardNumber",
+ label: "鍔犳补鍗″彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ュ姞娌瑰崱鍙�",
+ },
+ {
+ prop: "deptId",
+ label: "鎵�灞為儴闂�",
+ type: "select",
+ options: deptOptions.value,
+ },
+ ]);
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("fuelCardDetail");
+ uni.navigateTo({ url: "/pages/inventoryManagement/fuelCard/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("fuelCardDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelCard/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("fuelCardDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelCard/detail?id=${item.id}`,
+ });
+ };
+
+ const goRecharge = item => {
+ uni.setStorageSync("fuelCardDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelCard/recharge?id=${item.id}`,
+ });
+ };
+
+ const statusLabel = status => (status === "CANCELLED" ? "娉ㄩ攢" : "姝e父");
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const getList = () => {
+ listFuelCardPage({
+ current: -1,
+ size: -1,
+ cardNumber: query.cardNumber || undefined,
+ deptId: query.deptId || undefined,
+ })
+ .then(res => {
+ cardList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleCancelCard = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "娉ㄩ攢纭",
+ content: `纭娉ㄩ攢鍔犳补鍗°��${item.cardNumber || ""}銆嶅悧锛熸敞閿�鍚庤鍗′笉鑳界敤浜庢柊澧炲姞娌硅褰曪紝浣欓灏嗕繚鐣欍�俙,
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ updateFuelCard({ id: item.id, status: "CANCELLED" })
+ .then(() => {
+ uni.showToast({ title: "娉ㄩ攢鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "娉ㄩ攢澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: `纭鍒犻櫎鍔犳补鍗°��${item.cardNumber || ""}銆嶅悧锛焋,
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delFuelCard(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ let deptsLoaded = false;
+
+ const loadDepts = () => {
+ if (deptsLoaded) return;
+ deptsLoaded = true;
+ getDept()
+ .then(res => {
+ deptOptions.value = (res?.data || []).map(item => ({
+ name: item.deptName,
+ value: item.deptId,
+ }));
+ })
+ .catch(() => {
+ deptsLoaded = false;
+ });
+ };
+
+ onShow(() => {
+ getList();
+ loadDepts();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/fuelCard/recharge.vue b/src/pages/inventoryManagement/fuelCard/recharge.vue
new file mode 100644
index 0000000..46482a4
--- /dev/null
+++ b/src/pages/inventoryManagement/fuelCard/recharge.vue
@@ -0,0 +1,401 @@
+<template>
+ <view class="recharge-page">
+ <PageHeader title="鍏呭�肩鐞�"
+ @back="goBack" />
+
+ <view class="hero">
+ <text class="hero-title">{{ card.cardNumber || "-" }}</text>
+ <view class="hero-balance">
+ <text class="hero-label">褰撳墠浣欓</text>
+ <text class="hero-value">{{ formatAmount(currentBalance) }}</text>
+ </view>
+ </view>
+
+ <view class="section">
+ <view class="section-header">
+ <text class="section-title">鍏呭�艰褰�</text>
+ <up-button type="primary"
+ size="small"
+ text="鏂板鍏呭��"
+ @click="openForm" />
+ </view>
+
+ <view v-if="rechargeList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤鍏呭�艰褰�</text>
+ </view>
+ <view v-else
+ class="recharge-list">
+ <view v-for="row in rechargeList"
+ :key="row.id"
+ class="recharge-item">
+ <view class="recharge-main">
+ <text class="recharge-amount">+{{ Number(row.rechargeAmount || 0).toFixed(2) }}</text>
+ <text class="recharge-date">{{ row.rechargeDate || "-" }}</text>
+ </view>
+ <view class="recharge-meta">
+ <text v-if="row.createUserName"
+ class="recharge-user">{{ row.createUserName }}</text>
+ <text v-if="row.remark"
+ class="recharge-remark">{{ row.remark }}</text>
+ </view>
+ <view class="recharge-actions">
+ <up-button size="mini"
+ type="error"
+ plain
+ text="鍒犻櫎"
+ @click="handleDelete(row)" />
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <up-popup :show="showForm"
+ mode="bottom"
+ :round="16"
+ @close="showForm = false">
+ <view class="form-panel">
+ <view class="form-panel__header">
+ <text class="form-panel__title">鏂板鍏呭��</text>
+ <u-icon name="close"
+ size="20"
+ color="#909399"
+ @click="showForm = false"></u-icon>
+ </view>
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="90"
+ input-align="right">
+ <up-form-item label="鍏呭�奸噾棰�"
+ prop="rechargeAmount"
+ required>
+ <up-input v-model="form.rechargeAmount"
+ type="number"
+ placeholder="璇疯緭鍏ュ厖鍊奸噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍏呭�兼棩鏈�"
+ prop="rechargeDate"
+ required>
+ <up-input v-model="form.rechargeDate"
+ placeholder="璇烽�夋嫨鍏呭�兼棩鏈�"
+ readonly
+ @click="showDatePicker = true" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="showDatePicker = true"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-input v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable />
+ </up-form-item>
+ </up-form>
+ <view class="form-panel__footer">
+ <up-button class="form-panel__btn"
+ text="鍙栨秷"
+ @click="showForm = false" />
+ <up-button class="form-panel__btn"
+ type="primary"
+ text="纭畾"
+ :loading="submitting"
+ @click="submitForm" />
+ </view>
+ </view>
+ </up-popup>
+
+ <up-datetime-picker :show="showDatePicker"
+ v-model="dateValue"
+ mode="datetime"
+ @confirm="onDateConfirm"
+ @cancel="showDatePicker = false" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { unwrapList } from "@/utils/list";
+ import { parseTime } from "@/utils/ruoyi";
+ import {
+ addRecharge,
+ delRecharge,
+ listAllFuelCard,
+ listRecharge,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const cardId = ref("");
+ const card = ref({});
+ const rechargeList = ref([]);
+ const showForm = ref(false);
+ const showDatePicker = ref(false);
+ const submitting = ref(false);
+ const formRef = ref();
+ const dateValue = ref(Date.now());
+
+ const form = ref({
+ cardId: "",
+ rechargeAmount: "",
+ rechargeDate: "",
+ remark: "",
+ });
+
+ const rules = {
+ rechargeAmount: [
+ { required: true, message: "璇疯緭鍏ュ厖鍊奸噾棰�", trigger: "blur" },
+ ],
+ rechargeDate: [
+ { required: true, message: "璇烽�夋嫨鍏呭�兼棩鏈�", trigger: "change" },
+ ],
+ };
+
+ // 浣欓鐢卞厖鍊�/鍔犳补璁板綍鍦ㄥ悗绔淮鎶わ紝鍓嶇鍙睍绀�
+ const currentBalance = computed(() =>
+ card.value.currentBalance === undefined ? null : card.value.currentBalance
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const loadCard = async () => {
+ const cached = uni.getStorageSync("fuelCardDetail");
+ if (cached && String(cached.id) === String(cardId.value)) {
+ card.value = cached;
+ }
+ try {
+ const res = await listAllFuelCard();
+ const rows = unwrapList(res);
+ const found = rows.find(item => String(item.id) === String(cardId.value));
+ if (found) card.value = found;
+ } catch (error) {
+ // 鎷変笉鍒板氨娌跨敤缂撳瓨
+ }
+ };
+
+ const loadRecharges = () => {
+ listRecharge({ current: -1, size: -1, cardId: cardId.value })
+ .then(res => {
+ rechargeList.value = unwrapList(res);
+ })
+ .catch(() => {
+ rechargeList.value = [];
+ });
+ };
+
+ const refresh = () => {
+ loadCard();
+ loadRecharges();
+ };
+
+ const openForm = () => {
+ form.value = {
+ cardId: cardId.value,
+ rechargeAmount: "",
+ rechargeDate: parseTime(Date.now()),
+ remark: "",
+ };
+ showForm.value = true;
+ };
+
+ const onDateConfirm = e => {
+ form.value.rechargeDate = parseTime(e.value);
+ showDatePicker.value = false;
+ };
+
+ const submitForm = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+ submitting.value = true;
+ addRecharge({
+ ...form.value,
+ rechargeAmount: Number(form.value.rechargeAmount || 0),
+ })
+ .then(() => {
+ uni.showToast({ title: "鍏呭�兼垚鍔�", icon: "success" });
+ showForm.value = false;
+ refresh();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍏呭�煎け璐�", icon: "none" });
+ })
+ .finally(() => {
+ submitting.value = false;
+ });
+ };
+
+ const handleDelete = row => {
+ if (!row?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ョ瑪鍏呭�艰褰曞悧锛燂紙鍒犻櫎鍚庝綑棰濆皢鑷姩鎵e洖锛�",
+ success: res => {
+ if (!res.confirm) return;
+ delRecharge(row.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ refresh();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ });
+ },
+ });
+ };
+
+ onLoad(options => {
+ cardId.value = options?.id || "";
+ refresh();
+ });
+</script>
+
+<style scoped lang="scss">
+ .recharge-page {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding: 12px 12px 24px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 55%;
+ word-break: break-all;
+ }
+
+ .hero-balance {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ }
+
+ .hero-label {
+ font-size: 12px;
+ color: #7a8599;
+ }
+
+ .hero-value {
+ font-size: 18px;
+ font-weight: 600;
+ color: #2979ff;
+ }
+
+ .section {
+ margin-top: 12px;
+ padding: 12px;
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .section-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .empty-text {
+ padding: 24px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .recharge-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .recharge-item:last-child {
+ border-bottom: none;
+ }
+
+ .recharge-main {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .recharge-amount {
+ font-size: 15px;
+ font-weight: 600;
+ color: #19be6b;
+ }
+
+ .recharge-date {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .recharge-meta {
+ display: flex;
+ gap: 12px;
+ margin-top: 4px;
+ }
+
+ .recharge-user,
+ .recharge-remark {
+ font-size: 12px;
+ color: #909399;
+ word-break: break-all;
+ }
+
+ .recharge-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 8px;
+ }
+
+ .form-panel {
+ padding: 16px;
+ padding-bottom: calc(16px + env(safe-area-inset-bottom));
+ }
+
+ .form-panel__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .form-panel__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .form-panel__footer {
+ display: flex;
+ gap: 12px;
+ padding-top: 16px;
+ }
+
+ .form-panel__btn {
+ flex: 1;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/fuelRecord/detail.vue b/src/pages/inventoryManagement/fuelRecord/detail.vue
new file mode 100644
index 0000000..46cafcd
--- /dev/null
+++ b/src/pages/inventoryManagement/fuelRecord/detail.vue
@@ -0,0 +1,255 @@
+<template>
+ <view class="fuel-record-detail">
+ <PageHeader title="鍔犳补璁板綍璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text v-if="isDraft"
+ class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <text class="hero-title">{{ detail.plateNumber || "-" }}</text>
+ <up-tag :text="statusLabel"
+ :type="isDraft ? 'info' : 'success'"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <u-cell-group title="鍔犳补淇℃伅"
+ class="detail-section">
+ <u-cell title="鍔犳补鏃ユ湡"
+ :value="detail.fuelDate || '-'" />
+ <u-cell title="鎵�灞為儴闂�"
+ :value="detail.deptName || '-'" />
+ <u-cell title="鍔犳补杞﹁締"
+ :value="detail.plateNumber || '-'" />
+ <u-cell title="鍔犳补鍗″彿"
+ :value="detail.cardNumber || '-'" />
+ <u-cell title="鍙戠エ鍙风爜"
+ :value="detail.invoiceNo || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="閲戦涓庨噷绋�"
+ class="detail-section">
+ <u-cell title="閲戦"
+ :value="formatAmount(detail.amount)" />
+ <u-cell title="鍗囨暟(L)"
+ :value="detail.liters ?? '-'" />
+ <u-cell title="璧峰閲岀▼(km)"
+ :value="detail.startMileage ?? '-'" />
+ <u-cell title="缁撴潫閲岀▼(km)"
+ :value="detail.endMileage ?? '-'" />
+ <u-cell title="鏈琛岄┒(km)"
+ :value="travelDistance" />
+ </u-cell-group>
+
+ <u-cell-group title="澶囨敞"
+ class="detail-section">
+ <u-cell title="澶囨敞"
+ :value="detail.remark || '-'" />
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+
+ <view v-if="!loading"
+ class="footer-btns">
+ <up-button class="footer-btn"
+ text="杩斿洖"
+ @click="goBack" />
+ <up-button class="footer-btn"
+ :type="isDraft ? 'primary' : 'warning'"
+ :text="isDraft ? '鎻愪氦' : '鎾ゅ洖'"
+ @click="handleStatusAction" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { unwrapList } from "@/utils/list";
+ import {
+ listFuelRecord,
+ revokeFuelRecord,
+ submitFuelRecord,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const loading = ref(true);
+ const recordId = ref("");
+ const detail = ref({});
+
+ // status 鍚庣杩斿洖瀛楃涓� '0' 鑽夌 / '1' 宸叉彁浜�
+ const isDraft = computed(
+ () => detail.value.status === "0" || !detail.value.status
+ );
+ const statusLabel = computed(() => (isDraft.value ? "鑽夌" : "宸叉彁浜�"));
+
+ const travelDistance = computed(() => {
+ const start = Number(detail.value.startMileage);
+ const end = Number(detail.value.endMileage);
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) {
+ return "-";
+ }
+ return String(end - start);
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("fuelRecordDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelRecord/edit?id=${recordId.value}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ // 鍔犳补璁板綍娌℃湁鍗曟潯璇︽儏鎺ュ彛锛屼粠鍒楄〃閲屾寜 id 鎹�
+ const loadDetail = async () => {
+ loading.value = true;
+ const cached = uni.getStorageSync("fuelRecordDetail");
+ if (cached && String(cached.id) === String(recordId.value)) {
+ detail.value = cached;
+ }
+ try {
+ const res = await listFuelRecord({ current: -1, size: -1 });
+ const found = unwrapList(res).find(
+ item => String(item.id) === String(recordId.value)
+ );
+ if (found) detail.value = found;
+ } catch (error) {
+ // 鎷変笉鍒板氨娌跨敤缂撳瓨
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const handleStatusAction = () => {
+ if (!recordId.value) return;
+ const submitting = isDraft.value;
+ uni.showModal({
+ title: submitting ? "鎻愪氦纭" : "鎾ゅ洖纭",
+ content: submitting
+ ? "纭鎻愪氦璇ョ瑪鍔犳补璁板綍鍚楋紵鎻愪氦鍚庡皢浠庡姞娌瑰崱涓墸闄や綑棰濄��"
+ : "纭鎾ゅ洖璇ョ瑪鍔犳补璁板綍鍚楋紵鎾ゅ洖鍚庡皢閫�杩樺姞娌瑰崱浣欓銆�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ const action = submitting ? submitFuelRecord : revokeFuelRecord;
+ action(recordId.value)
+ .then(() => {
+ uni.showToast({
+ title: submitting ? "鎻愪氦鎴愬姛" : "鎾ゅ洖鎴愬姛",
+ icon: "success",
+ });
+ loadDetail();
+ })
+ .catch(() => {
+ uni.showToast({
+ title: submitting ? "鎻愪氦澶辫触" : "鎾ゅ洖澶辫触",
+ icon: "none",
+ });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onLoad(options => {
+ recordId.value = options?.id || "";
+ if (!recordId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .fuel-record-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 90px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .footer-btns {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ gap: 12px;
+ padding: 12px 16px;
+ padding-bottom: calc(12px + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
+ z-index: 1000;
+ }
+
+ .footer-btn {
+ flex: 1;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/fuelRecord/edit.vue b/src/pages/inventoryManagement/fuelRecord/edit.vue
new file mode 100644
index 0000000..4c13b91
--- /dev/null
+++ b/src/pages/inventoryManagement/fuelRecord/edit.vue
@@ -0,0 +1,295 @@
+<template>
+ <view class="account-detail">
+ <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="vehicleId"
+ required>
+ <FormPicker v-model="form.vehicleId"
+ type="select"
+ title="閫夋嫨鍔犳补杞﹁締"
+ :options="vehicleOptions"
+ placeholder="璇烽�夋嫨鍔犳补杞﹁締" />
+ </up-form-item>
+ <up-form-item label="鍏宠仈鍔犳补鍗�"
+ prop="fuelCardId"
+ required>
+ <FormPicker v-model="form.fuelCardId"
+ type="select"
+ title="閫夋嫨鍔犳补鍗�"
+ :options="cardOptions"
+ placeholder="璇烽�夋嫨鍔犳补鍗�" />
+ <view v-if="currentBalanceText"
+ class="field-tip">
+ <text>{{ currentBalanceText }}</text>
+ </view>
+ </up-form-item>
+ <up-form-item label="鍔犳补鏃ユ湡"
+ prop="fuelDate"
+ required>
+ <FormPicker v-model="form.fuelDate"
+ type="date"
+ placeholder="璇烽�夋嫨鍔犳补鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="鍙戠エ鍙风爜"
+ prop="invoiceNo">
+ <up-input v-model="form.invoiceNo"
+ placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�"
+ clearable />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="閲戦涓庨噷绋�"
+ class="form-section">
+ <up-form-item label="閲戦(鍏�)"
+ prop="amount"
+ required>
+ <up-input v-model="form.amount"
+ type="number"
+ placeholder="璇疯緭鍏ラ噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍗囨暟(L)"
+ prop="liters">
+ <up-input v-model="form.liters"
+ type="number"
+ placeholder="璇疯緭鍏ュ崌鏁�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璧峰閲岀▼"
+ prop="startMileage">
+ <up-input v-model="form.startMileage"
+ type="number"
+ placeholder="璇疯緭鍏ヨ捣濮嬮噷绋�(km)"
+ clearable />
+ </up-form-item>
+ <up-form-item label="缁撴潫閲岀▼"
+ prop="endMileage">
+ <up-input v-model="form.endMileage"
+ type="number"
+ placeholder="璇疯緭鍏ョ粨鏉熼噷绋�(km)"
+ clearable />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ auto-height />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FormPicker from "@/components/FormPicker.vue";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { formatDateToYMD } from "@/utils/ruoyi";
+ import {
+ addFuelRecord,
+ listAllFuelCard,
+ listFuelRecord,
+ listVehicleInfo,
+ updateFuelRecord,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const formRef = ref();
+ const loading = ref(false);
+ const recordId = ref("");
+ const vehicleOptions = ref([]);
+ const allCards = ref([]);
+
+ const form = ref({
+ id: undefined,
+ vehicleId: "",
+ fuelCardId: "",
+ fuelDate: "",
+ invoiceNo: "",
+ amount: "",
+ liters: "",
+ startMileage: "",
+ endMileage: "",
+ remark: "",
+ });
+
+ const rules = {
+ vehicleId: [{ required: true, message: "璇烽�夋嫨鍔犳补杞﹁締", trigger: "change" }],
+ fuelCardId: [{ required: true, message: "璇烽�夋嫨鍔犳补鍗�", trigger: "change" }],
+ fuelDate: [{ required: true, message: "璇烽�夋嫨鍔犳补鏃ユ湡", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+ };
+
+ const pageTitle = computed(() =>
+ recordId.value ? "缂栬緫鍔犳补璁板綍" : "鏂板鍔犳补璁板綍"
+ );
+
+ // 宸叉敞閿�鐨勫崱涓嶈兘鐢ㄤ簬鏂板鍔犳补璁板綍
+ const cardOptions = computed(() =>
+ allCards.value
+ .filter(item => item.status !== "CANCELLED")
+ .map(item => ({
+ name: `${item.cardNumber || ""} - ${item.issuer || ""}`,
+ value: item.id,
+ }))
+ );
+
+ const currentBalanceText = computed(() => {
+ if (!form.value.fuelCardId) return "";
+ const card = allCards.value.find(
+ item => String(item.id) === String(form.value.fuelCardId)
+ );
+ if (!card) return "";
+ return `褰撳墠浣欓: ${Number(card.currentBalance || 0).toFixed(2)} 鍏僠;
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ const loadOptions = async () => {
+ const [vehicles, cards] = await Promise.all([
+ listVehicleInfo({ current: -1, size: -1 }).catch(() => ({})),
+ listAllFuelCard().catch(() => ({})),
+ ]);
+ const vehicleRows =
+ vehicles?.data?.records || vehicles?.records || vehicles?.data || [];
+ vehicleOptions.value = (Array.isArray(vehicleRows) ? vehicleRows : []).map(
+ item => ({ name: item.plateNumber, value: item.id })
+ );
+ const cardRows = cards?.data || [];
+ allCards.value = Array.isArray(cardRows) ? cardRows : [];
+ };
+
+ // 鍔犳补璁板綍娌℃湁鍗曟潯璇︽儏鎺ュ彛锛屼粠鍒楄〃閲屾寜 id 鎹�
+ const fetchRecord = async id => {
+ const res = await listFuelRecord({ current: -1, size: -1 });
+ const rows = res?.data?.records || res?.records || res?.data || [];
+ const found = (Array.isArray(rows) ? rows : []).find(
+ item => String(item.id) === String(id)
+ );
+ return { data: found };
+ };
+
+ const loadDetail = async () => {
+ if (!recordId.value) return;
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "fuelRecordDetail",
+ id: recordId.value,
+ fetcher: fetchRecord,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鑾峰彇鍔犳补璁板綍澶辫触", icon: "none" });
+ return;
+ }
+ form.value = { ...form.value, ...data, id: data.id };
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ const card = allCards.value.find(
+ item => String(item.id) === String(form.value.fuelCardId)
+ );
+ const balance = Number(card?.currentBalance || 0);
+ const amount = Number(form.value.amount || 0);
+ if (!recordId.value && balance < amount) {
+ uni.showToast({
+ title: `璇ュ姞娌瑰崱浣欓涓嶈冻锛屽綋鍓嶄綑棰� ${balance} 鍏僠,
+ icon: "none",
+ });
+ }
+
+ loading.value = true;
+ const action = recordId.value ? updateFuelRecord : addFuelRecord;
+ action({
+ ...form.value,
+ id: recordId.value || undefined,
+ amount: amount,
+ liters: Number(form.value.liters || 0),
+ startMileage: Number(form.value.startMileage || 0),
+ endMileage: Number(form.value.endMileage || 0),
+ })
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ recordId.value = options.id;
+ form.value.id = options.id;
+ } else {
+ form.value.fuelDate = formatDateToYMD(Date.now());
+ }
+ });
+
+ onMounted(async () => {
+ await loadOptions();
+ if (recordId.value) await loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .field-tip {
+ padding-top: 4px;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/fuelRecord/index.vue b/src/pages/inventoryManagement/fuelRecord/index.vue
new file mode 100644
index 0000000..4acf6a5
--- /dev/null
+++ b/src/pages/inventoryManagement/fuelRecord/index.vue
@@ -0,0 +1,377 @@
+<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="query.plateNumber"
+ placeholder="璇疯緭鍏ヨ溅鐗屽彿鐮佹悳绱�"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="recordList.length > 0"
+ class="ledger-list">
+ <view v-for="item in recordList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="car-fill"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.plateNumber || "-" }}</text>
+ </view>
+ <up-tag :text="statusLabel(item.status)"
+ :type="isDraft(item) ? 'info' : 'success'"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍔犳补鏃ユ湡</text>
+ <text class="detail-value">{{ item.fuelDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵�灞為儴闂�</text>
+ <text class="detail-value">{{ item.deptName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍔犳补鍗″彿</text>
+ <text class="detail-value">{{ item.cardNumber || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閲戦</text>
+ <text class="detail-value highlight">{{ formatAmount(item.amount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗囨暟</text>
+ <text class="detail-value">{{ item.liters ?? "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璧峰閲岀▼</text>
+ <text class="detail-value">{{ item.startMileage ?? "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">缁撴潫閲岀▼</text>
+ <text class="detail-value">{{ item.endMileage ?? "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙戠エ鍙风爜</text>
+ <text class="detail-value">{{ item.invoiceNo || "-" }}</text>
+ </view>
+ <view class="detail-row"
+ v-if="item.remark">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ item.remark }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button v-if="isDraft(item)"
+ class="action-btn"
+ size="small"
+ type="success"
+ @click="handleSubmitRow(item)">鎻愪氦</up-button>
+ <up-button v-if="isDraft(item)"
+ class="action-btn"
+ size="small"
+ type="primary"
+ @click="goEdit(item)">缂栬緫</up-button>
+ <up-button class="action-btn"
+ size="small"
+ @click="goDetail(item)">璇︽儏</up-button>
+ <up-button v-if="isDraft(item)"
+ class="action-btn"
+ size="small"
+ type="error"
+ plain
+ @click="handleDelete(item)">鍒犻櫎</up-button>
+ <up-button v-if="!isDraft(item)"
+ class="action-btn"
+ size="small"
+ type="warning"
+ plain
+ @click="handleRevokeRow(item)">鎾ゅ洖</up-button>
+ </view>
+ </view>
+ </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>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鍔犳补璁板綍绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess";
+ import { unwrapList } from "@/utils/list";
+ import {
+ delFuelRecord,
+ listAllFuelCard,
+ listFuelRecord,
+ listVehicleInfo,
+ revokeFuelRecord,
+ submitFuelRecord,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const STATUS_OPTIONS = [
+ { name: "鑽夌", value: "0" },
+ { name: "宸叉彁浜�", value: "1" },
+ ];
+
+ const recordList = ref([]);
+ const showFilter = ref(false);
+ const deptOptions = ref([]);
+ const vehicleOptions = ref([]);
+ const cardOptions = ref([]);
+
+ const query = reactive({
+ plateNumber: "",
+ deptId: "",
+ vehicleId: "",
+ fuelCardId: "",
+ status: "",
+ fuelDateRange: [],
+ });
+
+ const filterFields = computed(() => [
+ {
+ prop: "plateNumber",
+ label: "杞︾墝鍙风爜",
+ type: "input",
+ placeholder: "璇疯緭鍏ヨ溅鐗屽彿鐮�",
+ },
+ {
+ prop: "deptId",
+ label: "鎵�灞為儴闂�",
+ type: "select",
+ options: deptOptions.value,
+ },
+ {
+ prop: "vehicleId",
+ label: "鍔犳补杞﹁締",
+ type: "select",
+ options: vehicleOptions.value,
+ },
+ {
+ prop: "fuelCardId",
+ label: "鍔犳补鍗�",
+ type: "select",
+ options: cardOptions.value,
+ },
+ {
+ prop: "status",
+ label: "鐘舵��",
+ type: "select",
+ options: STATUS_OPTIONS,
+ },
+ { prop: "fuelDateRange", label: "鍔犳补鏃ユ湡", type: "daterange" },
+ ]);
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("fuelRecordDetail");
+ uni.navigateTo({ url: "/pages/inventoryManagement/fuelRecord/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("fuelRecordDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelRecord/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("fuelRecordDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/fuelRecord/detail?id=${item.id}`,
+ });
+ };
+
+ // status 鍚庣杩斿洖瀛楃涓� '0' 鑽夌 / '1' 宸叉彁浜�
+ const isDraft = item => item?.status === "0" || !item?.status;
+ const statusLabel = status => (status === "1" ? "宸叉彁浜�" : "鑽夌");
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const getList = () => {
+ const [beginDate, endDate] = Array.isArray(query.fuelDateRange)
+ ? query.fuelDateRange
+ : [];
+ listFuelRecord({
+ current: -1,
+ size: -1,
+ deptId: query.deptId || undefined,
+ vehicleId: query.vehicleId || undefined,
+ fuelCardId: query.fuelCardId || undefined,
+ status: query.status || undefined,
+ beginDate: beginDate || undefined,
+ endDate: endDate || undefined,
+ })
+ .then(res => {
+ const rows = unwrapList(res);
+ // 杞︾墝鎼滅储妗嗗湪绛涢�夋潯浠堕噷娌℃湁瀵瑰簲鍙傛暟锛屾湰鍦拌繃婊ゅ厹搴�
+ recordList.value = query.plateNumber
+ ? rows.filter(item =>
+ String(item.plateNumber || "").includes(query.plateNumber)
+ )
+ : rows;
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleSubmitRow = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鎻愪氦纭",
+ content: "纭鎻愪氦璇ョ瑪鍔犳补璁板綍鍚楋紵鎻愪氦鍚庡皢浠庡姞娌瑰崱涓墸闄や綑棰濄��",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ submitFuelRecord(item.id)
+ .then(() => {
+ uni.showToast({ title: "鎻愪氦鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鎻愪氦澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ const handleRevokeRow = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鎾ゅ洖纭",
+ content: "纭鎾ゅ洖璇ョ瑪鍔犳补璁板綍鍚楋紵鎾ゅ洖鍚庡皢閫�杩樺姞娌瑰崱浣欓銆�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ revokeFuelRecord(item.id)
+ .then(() => {
+ uni.showToast({ title: "鎾ゅ洖鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鎾ゅ洖澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ヨ崏绋胯褰曞悧锛�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delFuelRecord(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ let optionsLoaded = false;
+
+ const loadOptions = () => {
+ if (optionsLoaded) return;
+ optionsLoaded = true;
+ Promise.all([
+ getDept().catch(() => ({})),
+ listVehicleInfo({ current: -1, size: -1 }).catch(() => ({})),
+ listAllFuelCard().catch(() => ({})),
+ ])
+ .then(([depts, vehicles, cards]) => {
+ deptOptions.value = (depts?.data || []).map(item => ({
+ name: item.deptName,
+ value: item.deptId,
+ }));
+ vehicleOptions.value = unwrapList(vehicles).map(item => ({
+ name: item.plateNumber,
+ value: item.id,
+ }));
+ cardOptions.value = unwrapList(cards).map(item => ({
+ name: `${item.cardNumber || ""} - ${item.issuer || ""}`,
+ value: item.id,
+ }));
+ })
+ .catch(() => {
+ optionsLoaded = false;
+ });
+ };
+
+ onShow(() => {
+ getList();
+ loadOptions();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/vehicleInfo/detail.vue b/src/pages/inventoryManagement/vehicleInfo/detail.vue
new file mode 100644
index 0000000..23a9416
--- /dev/null
+++ b/src/pages/inventoryManagement/vehicleInfo/detail.vue
@@ -0,0 +1,510 @@
+<template>
+ <view class="vehicle-detail">
+ <PageHeader title="杞﹁締璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <text class="hero-title">{{ detail.plateNumber || "-" }}</text>
+ <up-tag :text="statusLabel(detail.vehicleStatus)"
+ :type="statusTagType(detail.vehicleStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <view v-if="warnItems.length"
+ class="warn-banner">
+ <up-icon name="error-circle"
+ size="16"
+ color="#ff9900"></up-icon>
+ <text class="warn-text">{{ warnItems.join("锛�") }}</text>
+ </view>
+
+ <u-cell-group title="杞﹁締淇℃伅"
+ class="detail-section">
+ <u-cell title="杞︾墝鍙风爜"
+ :value="detail.plateNumber || '-'" />
+ <u-cell title="浜ф潈鎵�鏈�"
+ :value="detail.propertyOwnership || '-'" />
+ <u-cell title="鎵�灞為儴闂�"
+ :value="detail.deptName || '-'" />
+ <u-cell title="璐d换浜�"
+ :value="detail.userName || '-'" />
+ <u-cell title="鐘舵��"
+ :value="statusLabel(detail.vehicleStatus)" />
+ <u-cell title="骞存鍒版湡"
+ :value="detail.inspectionExpireDate || '-'" />
+ <u-cell title="淇濋櫓鍒版湡"
+ :value="detail.insuranceExpireDate || '-'" />
+ <u-cell title="澶囨敞"
+ :value="detail.remark || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="璐圭敤绠$悊"
+ class="detail-section">
+ <view class="section-tools">
+ <up-button type="primary"
+ size="small"
+ text="鏂板璐圭敤"
+ @click="openCostForm" />
+ </view>
+ <view v-if="costList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤璐圭敤璁板綍</text>
+ </view>
+ <view v-else
+ class="cost-list">
+ <view v-for="cost in costList"
+ :key="cost.id"
+ class="cost-item">
+ <view class="cost-main">
+ <text class="cost-type">{{ costTypeLabel(cost.costType) }}</text>
+ <text class="cost-amount">{{ formatAmount(cost.amount) }}</text>
+ </view>
+ <view class="cost-meta">
+ <text class="cost-date">{{ cost.costDate || "-" }}</text>
+ <text v-if="cost.remark"
+ class="cost-remark">{{ cost.remark }}</text>
+ </view>
+ <view class="cost-actions">
+ <up-button size="mini"
+ type="error"
+ plain
+ text="鍒犻櫎"
+ @click="handleDeleteCost(cost)" />
+ </view>
+ </view>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+
+ <up-popup :show="showCostForm"
+ mode="bottom"
+ :round="16"
+ @close="showCostForm = false">
+ <view class="cost-form">
+ <view class="cost-form__header">
+ <text class="cost-form__title">鏂板璐圭敤</text>
+ <u-icon name="close"
+ size="20"
+ color="#909399"
+ @click="showCostForm = false"></u-icon>
+ </view>
+ <up-form ref="costFormRef"
+ :model="costForm"
+ :rules="costRules"
+ label-width="90"
+ input-align="right">
+ <up-form-item label="璐圭敤绫诲瀷"
+ prop="costType"
+ required>
+ <FormPicker v-model="costForm.costType"
+ type="select"
+ title="閫夋嫨璐圭敤绫诲瀷"
+ :options="COST_TYPE_OPTIONS"
+ placeholder="璇烽�夋嫨璐圭敤绫诲瀷" />
+ </up-form-item>
+ <up-form-item label="閲戦(鍏�)"
+ prop="amount"
+ required>
+ <up-input v-model="costForm.amount"
+ type="number"
+ placeholder="璇疯緭鍏ラ噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍙戠敓鏃ユ湡"
+ prop="costDate"
+ required>
+ <FormPicker v-model="costForm.costDate"
+ type="date"
+ placeholder="璇烽�夋嫨鍙戠敓鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-input v-model="costForm.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable />
+ </up-form-item>
+ </up-form>
+ <view class="cost-form__footer">
+ <up-button class="cost-form__btn"
+ text="鍙栨秷"
+ @click="showCostForm = false" />
+ <up-button class="cost-form__btn"
+ type="primary"
+ text="纭畾"
+ :loading="costSubmitting"
+ @click="submitCost" />
+ </view>
+ </view>
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FormPicker from "@/components/FormPicker.vue";
+ import { unwrapList } from "@/utils/list";
+ import { formatDateToYMD } from "@/utils/ruoyi";
+ import { getWarnInfo } from "@/utils/warning";
+ import {
+ addVehicleCost,
+ delVehicleCost,
+ listVehicleCost,
+ listVehicleInfo,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const COST_TYPE_OPTIONS = [
+ { name: "淇濆吇", value: "MAINTENANCE" },
+ { name: "骞存", value: "INSPECTION" },
+ { name: "淇濋櫓", value: "INSURANCE" },
+ { name: "缁翠慨", value: "REPAIR" },
+ ];
+
+ const loading = ref(true);
+ const vehicleId = ref("");
+ const detail = ref({});
+ const costList = ref([]);
+ const showCostForm = ref(false);
+ const costSubmitting = ref(false);
+ const costFormRef = ref();
+
+ const costForm = ref({
+ vehicleId: "",
+ costType: "",
+ amount: "",
+ costDate: "",
+ remark: "",
+ });
+
+ const costRules = {
+ costType: [{ required: true, message: "璇烽�夋嫨璐圭敤绫诲瀷", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+ costDate: [{ required: true, message: "璇烽�夋嫨鍙戠敓鏃ユ湡", trigger: "change" }],
+ };
+
+ // 闇�姹傚崄涓�锛氬勾妫�/淇濋櫓鍒版湡鍓� 30 澶╅璀�
+ const warnItems = computed(() => {
+ const items = [];
+ const inspection = getWarnInfo(detail.value.inspectionExpireDate, 30);
+ if (inspection.text && inspection.level !== "normal") {
+ items.push(`骞存${inspection.text}`);
+ }
+ const insurance = getWarnInfo(detail.value.insuranceExpireDate, 30);
+ if (insurance.text && insurance.level !== "normal") {
+ items.push(`淇濋櫓${insurance.text}`);
+ }
+ return items;
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("vehicleInfoDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/vehicleInfo/edit?id=${vehicleId.value}`,
+ });
+ };
+
+ const statusLabel = status => {
+ if (status === "NORMAL") return "鍦ㄧ敤";
+ if (status === "IDLE") return "闂茬疆";
+ if (status === "REPAIR") return "缁翠慨";
+ return status || "鏈~鐘舵��";
+ };
+
+ const statusTagType = status => {
+ if (status === "NORMAL") return "success";
+ if (status === "IDLE") return "info";
+ if (status === "REPAIR") return "warning";
+ return "info";
+ };
+
+ const costTypeLabel = type =>
+ COST_TYPE_OPTIONS.find(item => item.value === type)?.name || type || "-";
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ // 杞﹁締妯″潡娌℃湁鍗曟潯璇︽儏鎺ュ彛锛屼粠鍒楄〃閲屾寜 id 鎹�
+ const loadVehicle = async () => {
+ const cached = uni.getStorageSync("vehicleInfoDetail");
+ if (cached && String(cached.id) === String(vehicleId.value)) {
+ detail.value = cached;
+ }
+ try {
+ const res = await listVehicleInfo({ current: -1, size: -1 });
+ const rows = unwrapList(res);
+ const found = rows.find(
+ item => String(item.id) === String(vehicleId.value)
+ );
+ if (found) detail.value = found;
+ } catch (error) {
+ // 鎷変笉鍒板氨娌跨敤缂撳瓨锛屼笅闈㈣繕鏈夌┖鎬佸厹搴�
+ }
+ };
+
+ const loadCosts = () => {
+ listVehicleCost({ current: -1, size: -1, vehicleId: vehicleId.value })
+ .then(res => {
+ costList.value = unwrapList(res);
+ })
+ .catch(() => {
+ costList.value = [];
+ });
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ await loadVehicle();
+ loadCosts();
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const openCostForm = () => {
+ costForm.value = {
+ vehicleId: vehicleId.value,
+ costType: "",
+ amount: "",
+ costDate: formatDateToYMD(Date.now()),
+ remark: "",
+ };
+ showCostForm.value = true;
+ };
+
+ const submitCost = async () => {
+ const valid = await costFormRef.value.validate().catch(() => false);
+ if (!valid) return;
+ costSubmitting.value = true;
+ addVehicleCost({
+ ...costForm.value,
+ amount: Number(costForm.value.amount || 0),
+ })
+ .then(() => {
+ uni.showToast({ title: "娣诲姞鎴愬姛", icon: "success" });
+ showCostForm.value = false;
+ loadCosts();
+ })
+ .catch(() => {
+ uni.showToast({ title: "娣诲姞澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ costSubmitting.value = false;
+ });
+ };
+
+ const handleDeleteCost = cost => {
+ if (!cost?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ヨ垂鐢ㄨ褰曞悧锛�",
+ success: res => {
+ if (!res.confirm) return;
+ delVehicleCost(cost.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ loadCosts();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ });
+ },
+ });
+ };
+
+ onLoad(options => {
+ vehicleId.value = options?.id || "";
+ if (!vehicleId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .vehicle-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .warn-banner {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 12px;
+ padding: 10px 14px;
+ border-radius: 10px;
+ background: #fff7ea;
+ }
+
+ .warn-text {
+ flex: 1;
+ font-size: 13px;
+ color: #e6a23c;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .section-tools {
+ display: flex;
+ justify-content: flex-end;
+ padding: 12px 12px 0;
+ }
+
+ .empty-text {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .cost-list {
+ padding: 6px 12px 12px;
+ }
+
+ .cost-item {
+ padding: 12px 0;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .cost-item:last-child {
+ border-bottom: none;
+ }
+
+ .cost-main {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .cost-type {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .cost-amount {
+ font-size: 14px;
+ font-weight: 600;
+ color: #2979ff;
+ }
+
+ .cost-meta {
+ display: flex;
+ gap: 12px;
+ margin-top: 4px;
+ }
+
+ .cost-date {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .cost-remark {
+ flex: 1;
+ font-size: 12px;
+ color: #909399;
+ word-break: break-all;
+ }
+
+ .cost-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 8px;
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .cost-form {
+ padding: 16px;
+ padding-bottom: calc(16px + env(safe-area-inset-bottom));
+ }
+
+ .cost-form__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-bottom: 12px;
+ border-bottom: 1px solid #f0f0f0;
+ }
+
+ .cost-form__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .cost-form__footer {
+ display: flex;
+ gap: 12px;
+ padding-top: 16px;
+ }
+
+ .cost-form__btn {
+ flex: 1;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/vehicleInfo/edit.vue b/src/pages/inventoryManagement/vehicleInfo/edit.vue
new file mode 100644
index 0000000..abacf46
--- /dev/null
+++ b/src/pages/inventoryManagement/vehicleInfo/edit.vue
@@ -0,0 +1,249 @@
+<template>
+ <view class="account-detail">
+ <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="plateNumber"
+ required>
+ <up-input v-model="form.plateNumber"
+ placeholder="璇疯緭鍏ヨ溅鐗屽彿鐮�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="浜ф潈鎵�鏈�"
+ prop="propertyOwnership">
+ <up-input v-model="form.propertyOwnership"
+ placeholder="濡傦細鑷湁銆佺璧佺瓑"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎵�灞為儴闂�"
+ prop="deptId"
+ required>
+ <FormPicker v-model="form.deptId"
+ type="select"
+ title="閫夋嫨鎵�灞為儴闂�"
+ :options="deptOptions"
+ placeholder="璇烽�夋嫨鎵�灞為儴闂�"
+ @change="onDeptChange" />
+ </up-form-item>
+ <up-form-item label="璐d换浜�"
+ prop="userId">
+ <FormPicker v-model="form.userId"
+ type="select"
+ title="閫夋嫨璐d换浜�"
+ :options="userOptions"
+ placeholder="璇烽�夋嫨璐d换浜�"
+ @change="onUserChange" />
+ </up-form-item>
+ <up-form-item label="鐘舵��"
+ prop="vehicleStatus"
+ required>
+ <FormPicker v-model="form.vehicleStatus"
+ type="select"
+ title="閫夋嫨鐘舵��"
+ :options="VEHICLE_STATUS_OPTIONS"
+ placeholder="璇烽�夋嫨鐘舵��" />
+ </up-form-item>
+ <up-form-item label="骞存鍒版湡"
+ prop="inspectionExpireDate">
+ <FormPicker v-model="form.inspectionExpireDate"
+ type="date"
+ placeholder="璇烽�夋嫨骞存鍒版湡鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="淇濋櫓鍒版湡"
+ prop="insuranceExpireDate">
+ <FormPicker v-model="form.insuranceExpireDate"
+ type="date"
+ placeholder="璇烽�夋嫨淇濋櫓鍒版湡鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ auto-height />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FormPicker from "@/components/FormPicker.vue";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import { loadDetailData } from "@/utils/detailCache";
+ import {
+ addVehicleInfo,
+ listVehicleInfo,
+ updateVehicleInfo,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const VEHICLE_STATUS_OPTIONS = [
+ { name: "鍦ㄧ敤", value: "NORMAL" },
+ { name: "闂茬疆", value: "IDLE" },
+ { name: "缁翠慨", value: "REPAIR" },
+ ];
+
+ const formRef = ref();
+ const loading = ref(false);
+ const vehicleId = ref("");
+ const deptOptions = ref([]);
+ const userOptions = ref([]);
+
+ const form = ref({
+ id: undefined,
+ plateNumber: "",
+ propertyOwnership: "",
+ deptId: "",
+ deptName: "",
+ userId: "",
+ userName: "",
+ vehicleStatus: "NORMAL",
+ inspectionExpireDate: "",
+ insuranceExpireDate: "",
+ remark: "",
+ });
+
+ const rules = {
+ plateNumber: [
+ { required: true, message: "璇疯緭鍏ヨ溅鐗屽彿鐮�", trigger: "blur" },
+ ],
+ deptId: [{ required: true, message: "璇烽�夋嫨鎵�灞為儴闂�", trigger: "change" }],
+ vehicleStatus: [{ required: true, message: "璇烽�夋嫨鐘舵��", trigger: "change" }],
+ };
+
+ const pageTitle = computed(() => (vehicleId.value ? "缂栬緫杞﹁締" : "鏂板杞﹁締"));
+
+ const goBack = () => uni.navigateBack();
+
+ const onDeptChange = (value, item) => {
+ form.value.deptName = item?.name || "";
+ };
+
+ const onUserChange = (value, item) => {
+ form.value.userName = item?.name || "";
+ };
+
+ const loadOptions = async () => {
+ const [depts, users] = await Promise.all([
+ getDept().catch(() => ({})),
+ userListNoPageByTenantId().catch(() => ({})),
+ ]);
+ deptOptions.value = (depts?.data || []).map(item => ({
+ name: item.deptName,
+ value: item.deptId,
+ }));
+ const rows = users?.data || [];
+ userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({
+ name: item.nickName || item.userName || `鐢ㄦ埛${item.userId}`,
+ value: item.userId,
+ }));
+ };
+
+ // 杞﹁締妯″潡娌℃湁鍗曟潯璇︽儏鎺ュ彛锛屼粠鍒楄〃閲屾寜 id 鎹烇紝鍖呰鎴� loadDetailData 鏈熸湜鐨勫搷搴斿舰鐘�
+ const fetchVehicle = async id => {
+ const res = await listVehicleInfo({ current: -1, size: -1 });
+ const rows = res?.data?.records || res?.records || res?.data || [];
+ const found = (Array.isArray(rows) ? rows : []).find(
+ item => String(item.id) === String(id)
+ );
+ return { data: found };
+ };
+
+ const loadDetail = async () => {
+ if (!vehicleId.value) return;
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "vehicleInfoDetail",
+ id: vehicleId.value,
+ fetcher: fetchVehicle,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鑾峰彇杞﹁締璇︽儏澶辫触", icon: "none" });
+ return;
+ }
+ form.value = { ...form.value, ...data, id: data.id };
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ loading.value = true;
+ const action = vehicleId.value ? updateVehicleInfo : addVehicleInfo;
+ action({ ...form.value, id: vehicleId.value || undefined })
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ vehicleId.value = options.id;
+ form.value.id = options.id;
+ }
+ });
+
+ onMounted(async () => {
+ await loadOptions();
+ if (vehicleId.value) await loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/inventoryManagement/vehicleInfo/index.vue b/src/pages/inventoryManagement/vehicleInfo/index.vue
new file mode 100644
index 0000000..e5e2696
--- /dev/null
+++ b/src/pages/inventoryManagement/vehicleInfo/index.vue
@@ -0,0 +1,331 @@
+<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="query.plateNumber"
+ placeholder="璇疯緭鍏ヨ溅鐗屽彿鐮佹悳绱�"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view class="warn-bar">
+ <up-button :type="showWarning ? 'warning' : 'primary'"
+ size="small"
+ :plain="!showWarning"
+ :text="showWarning ? '杩斿洖鍏ㄩ儴杞﹁締' : '鏌ョ湅 30 澶╁唴鍒版湡棰勮'"
+ @click="toggleWarning" />
+ </view>
+
+ <view v-if="vehicleList.length > 0"
+ class="ledger-list">
+ <view v-for="item in vehicleList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="car"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.plateNumber || "-" }}</text>
+ </view>
+ <up-tag :text="statusLabel(item.vehicleStatus)"
+ :type="statusTagType(item.vehicleStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鎵�灞為儴闂�</text>
+ <text class="detail-value">{{ item.deptName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璐d换浜�</text>
+ <text class="detail-value">{{ item.userName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜ф潈鎵�鏈�</text>
+ <text class="detail-value">{{ item.propertyOwnership || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">骞存鍒版湡</text>
+ <text class="detail-value">{{ item.inspectionExpireDate || "-" }}</text>
+ </view>
+ <view class="detail-row"
+ v-if="inspectionWarn(item).text">
+ <text class="detail-label">骞存棰勮</text>
+ <text class="detail-value"
+ :style="{ color: inspectionWarn(item).color }">
+ {{ inspectionWarn(item).text }}
+ </text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">淇濋櫓鍒版湡</text>
+ <text class="detail-value">{{ item.insuranceExpireDate || "-" }}</text>
+ </view>
+ <view class="detail-row"
+ v-if="insuranceWarn(item).text">
+ <text class="detail-label">淇濋櫓棰勮</text>
+ <text class="detail-value"
+ :style="{ color: insuranceWarn(item).color }">
+ {{ insuranceWarn(item).text }}
+ </text>
+ </view>
+ <view class="detail-row"
+ v-if="item.remark">
+ <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="success"
+ @click="goCost(item)">璐圭敤绠$悊</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @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">
+ <text>{{ showWarning ? "鏆傛棤 30 澶╁唴鍒版湡鐨勮溅杈�" : "鏆傛棤杞﹁締鏁版嵁" }}</text>
+ </view>
+
+ <view v-if="!showWarning"
+ class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="杞﹁締绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess";
+ import { unwrapList } from "@/utils/list";
+ import { getWarnInfo } from "@/utils/warning";
+ import {
+ delVehicleInfo,
+ getVehicleWarningList,
+ listVehicleInfo,
+ } from "@/api/inventoryManagement/vehicle";
+
+ const VEHICLE_STATUS_OPTIONS = [
+ { name: "鍦ㄧ敤", value: "NORMAL" },
+ { name: "闂茬疆", value: "IDLE" },
+ { name: "缁翠慨", value: "REPAIR" },
+ ];
+
+ const vehicleList = ref([]);
+ const showFilter = ref(false);
+ const showWarning = ref(false);
+ const deptOptions = ref([]);
+
+ const query = reactive({
+ plateNumber: "",
+ deptId: "",
+ vehicleStatus: "",
+ });
+
+ const filterFields = computed(() => [
+ {
+ prop: "plateNumber",
+ label: "杞︾墝鍙风爜",
+ type: "input",
+ placeholder: "璇疯緭鍏ヨ溅鐗屽彿鐮�",
+ },
+ {
+ prop: "deptId",
+ label: "鎵�灞為儴闂�",
+ type: "select",
+ options: deptOptions.value,
+ },
+ {
+ prop: "vehicleStatus",
+ label: "鐘舵��",
+ type: "select",
+ options: VEHICLE_STATUS_OPTIONS,
+ },
+ ]);
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("vehicleInfoDetail");
+ uni.navigateTo({ url: "/pages/inventoryManagement/vehicleInfo/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("vehicleInfoDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/vehicleInfo/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("vehicleInfoDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/vehicleInfo/detail?id=${item.id}`,
+ });
+ };
+
+ const goCost = item => {
+ uni.setStorageSync("vehicleInfoDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/inventoryManagement/vehicleInfo/detail?id=${item.id}&tab=cost`,
+ });
+ };
+
+ const statusLabel = status => {
+ if (status === "NORMAL") return "鍦ㄧ敤";
+ if (status === "IDLE") return "闂茬疆";
+ if (status === "REPAIR") return "缁翠慨";
+ return status || "鏈~鐘舵��";
+ };
+
+ const statusTagType = status => {
+ if (status === "NORMAL") return "success";
+ if (status === "IDLE") return "info";
+ if (status === "REPAIR") return "warning";
+ return "info";
+ };
+
+ // 闇�姹傚崄涓�锛氳溅杈嗗勾妫�/淇濋櫓鍒版湡鍓� 30 澶╅璀�
+ const inspectionWarn = item => getWarnInfo(item?.inspectionExpireDate, 30);
+ const insuranceWarn = item => getWarnInfo(item?.insuranceExpireDate, 30);
+
+ const getList = () => {
+ if (showWarning.value) return;
+ listVehicleInfo({
+ current: -1,
+ size: -1,
+ plateNumber: query.plateNumber || undefined,
+ deptId: query.deptId || undefined,
+ vehicleStatus: query.vehicleStatus || undefined,
+ })
+ .then(res => {
+ vehicleList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ const toggleWarning = () => {
+ if (showWarning.value) {
+ showWarning.value = false;
+ getList();
+ return;
+ }
+ getVehicleWarningList()
+ .then(res => {
+ vehicleList.value = unwrapList(res);
+ showWarning.value = true;
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇棰勮杞﹁締澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ showWarning.value = false;
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: `纭鍒犻櫎杞﹁締銆�${item.plateNumber || ""}銆嶅悧锛焋,
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delVehicleInfo(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ let deptsLoaded = false;
+
+ const loadDepts = () => {
+ if (deptsLoaded) return;
+ deptsLoaded = true;
+ getDept()
+ .then(res => {
+ deptOptions.value = (res?.data || []).map(item => ({
+ name: item.deptName,
+ value: item.deptId,
+ }));
+ })
+ .catch(() => {
+ deptsLoaded = false;
+ });
+ };
+
+ onShow(() => {
+ getList();
+ loadDepts();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .warn-bar {
+ padding: 0 12px 8px;
+ }
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue b/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue
index 4270e90..e60a0c9 100644
--- a/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue
+++ b/src/pages/oa/ReimburseManage/_components/ReimburseInstanceDetailBody.vue
@@ -109,6 +109,11 @@
<text class="rd-section-title">鏀舵淇℃伅</text>
</view>
<view class="rd-group">
+ <view v-if="!isTravel"
+ class="rd-cell">
+ <text class="rd-label">浠樻鏂�</text>
+ <text class="rd-value">{{ r.payer || "鈥�" }}</text>
+ </view>
<view class="rd-cell">
<text class="rd-label">鏀舵浜�</text>
<text class="rd-value">{{ r.payeeName || r.payee || "鈥�" }}</text>
diff --git a/src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js b/src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js
index 118b353..66b0213 100644
--- a/src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js
+++ b/src/pages/oa/ReimburseManage/_utils/costReimburseUtils.js
@@ -95,6 +95,7 @@
expenseCategory: "other",
reimburseReason: "",
applyAmount: "",
+ payer: "",
payee: "",
payeeAccount: "",
bankBranch: "",
diff --git a/src/pages/oa/ReimburseManage/reimburse-form/index.vue b/src/pages/oa/ReimburseManage/reimburse-form/index.vue
index cfb9dd1..c7b68b6 100644
--- a/src/pages/oa/ReimburseManage/reimburse-form/index.vue
+++ b/src/pages/oa/ReimburseManage/reimburse-form/index.vue
@@ -215,6 +215,16 @@
input-align="right" />
</view>
</view>
+ <view v-if="!isTravel"
+ class="rf-cell">
+ <text class="rf-label">浠樻鏂�</text>
+ <view class="rf-input-body">
+ <up-input v-model="form.payer"
+ placeholder="閫夊~"
+ border="none"
+ input-align="right" />
+ </view>
+ </view>
<view class="rf-cell">
<text class="rf-label required">鏀舵浜�</text>
<view class="rf-input-body">
diff --git a/src/pages/oa/_utils/finReimbursementMappers.js b/src/pages/oa/_utils/finReimbursementMappers.js
index 147e20c..d2695ba 100644
--- a/src/pages/oa/_utils/finReimbursementMappers.js
+++ b/src/pages/oa/_utils/finReimbursementMappers.js
@@ -778,6 +778,7 @@
applyAmount: row.applyAmount,
applyTime: formatReimbursementDateTime(row.createTime),
createTime: formatReimbursementDateTime(row.createTime),
+ payer: row.payer || "",
payee: row.payeeName || "",
payeeAccount: row.payeeAccount || "",
bankBranch: row.payeeBank || "",
@@ -892,6 +893,7 @@
reason: (form.reimburseReason || "").trim(),
applyAmount,
detailTotalAmount: detailTotal,
+ payer: form.payer || "",
payeeName: form.payee || "",
payeeAccount: form.payeeAccount || "",
payeeBank: form.bankBranch || form.payeeBank || "",
diff --git a/src/pages/projectManagement/project/_components/AddressRow.vue b/src/pages/projectManagement/project/_components/AddressRow.vue
new file mode 100644
index 0000000..a0a70ad
--- /dev/null
+++ b/src/pages/projectManagement/project/_components/AddressRow.vue
@@ -0,0 +1,94 @@
+<!--
+ 鏀惰揣鍦板潃瀛愯〃鐨勪竴琛�
+
+ 鍚庣 shippingAddress 鍙瓨涓�鏉★紙consignee=鏀惰揣浜恒�乧ontract=鑱旂郴鐢佃瘽銆乤ddress=鍦板潃锛夛紝
+ 鎵�浠ヨ繖涓瓙琛ㄥ湪琛ㄥ崟閲屽彧浼氭湁涓�琛岋紝鎻愪氦鏃舵寜 mapper 鍙栫涓�琛屻��
+-->
+<template>
+ <view class="address-card">
+ <view class="address-header">
+ <text class="address-title">鏀惰揣鍦板潃</text>
+ </view>
+ <up-divider></up-divider>
+
+ <view class="address-body">
+ <view class="field-row">
+ <text class="field-label">鏀惰揣浜�</text>
+ <u-input v-model="row.receiver"
+ placeholder="璇疯緭鍏ユ敹璐т汉"
+ border="none"
+ inputAlign="right" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">鑱旂郴鏂瑰紡</text>
+ <u-input v-model="row.phone"
+ type="number"
+ placeholder="璇疯緭鍏ヨ仈绯绘柟寮�"
+ border="none"
+ inputAlign="right" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">鏀惰揣鍦板潃</text>
+ <u-input v-model="row.address"
+ placeholder="璇疯緭鍏ユ敹璐у湴鍧�"
+ border="none"
+ inputAlign="right" />
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ defineProps({
+ row: {
+ type: Object,
+ required: true,
+ },
+ });
+</script>
+
+<style scoped lang="scss">
+ .address-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 0 12px 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .address-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 0;
+ }
+
+ .address-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .address-body {
+ padding-top: 4px;
+ }
+
+ .field-row {
+ display: flex;
+ align-items: center;
+ min-height: 44px;
+ border-bottom: 1px solid #f5f6fa;
+ }
+
+ .field-row:last-child {
+ border-bottom: none;
+ }
+
+ .field-label {
+ width: 96px;
+ flex-shrink: 0;
+ font-size: 14px;
+ color: #606266;
+ }
+</style>
diff --git a/src/pages/projectManagement/project/_components/ProductRow.vue b/src/pages/projectManagement/project/_components/ProductRow.vue
new file mode 100644
index 0000000..83e6fbe
--- /dev/null
+++ b/src/pages/projectManagement/project/_components/ProductRow.vue
@@ -0,0 +1,363 @@
+<!--
+ 椤圭洰浜у搧淇℃伅瀛愯〃鐨勪竴琛�
+
+ row 鐢辩埗绾� productList 鐨勫厓绱犵洿鎺ヤ紶鍏ワ紙鍚屼竴涓搷搴斿紡瀵硅薄锛夛紝
+ 缁勪欢鍐呯洿鎺ユ敼 row.xxx 鍗冲彲锛屼笉闇�瑕� emit 鍥炵埗绾у悎骞躲��
+ 浜у搧澶х被 / 瑙勬牸鍨嬪彿璧� up-action-sheet锛岄�夐」鐢辨湰缁勪欢鑷繁鎷夊彇銆�
+-->
+<template>
+ <view class="product-card">
+ <view class="product-header">
+ <text class="product-title">浜у搧 {{ index + 1 }}</text>
+ <up-icon name="trash"
+ color="#ee0a24"
+ size="18"
+ @click="$emit('remove')"></up-icon>
+ </view>
+ <up-divider></up-divider>
+
+ <view class="product-body">
+ <view class="field-row"
+ @click="openCategory">
+ <text class="field-label">浜у搧澶х被</text>
+ <text :class="['field-value', row.productCategory ? '' : 'placeholder']">
+ {{ row.productCategory || "璇烽�夋嫨浜у搧澶х被" }}
+ </text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc"></up-icon>
+ </view>
+
+ <view class="field-row"
+ @click="openModel">
+ <text class="field-label">瑙勬牸鍨嬪彿</text>
+ <text :class="['field-value', row.specificationModel ? '' : 'placeholder']">
+ {{ row.specificationModel || "璇烽�夋嫨瑙勬牸鍨嬪彿" }}
+ </text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc"></up-icon>
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">鍗曚綅</text>
+ <u-input v-model="row.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ border="none"
+ inputAlign="right" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">鏁伴噺</text>
+ <u-input v-model="row.quantity"
+ type="number"
+ placeholder="璇疯緭鍏ユ暟閲�"
+ border="none"
+ inputAlign="right" />
+ </view>
+
+ <view class="field-row"
+ @click="openTaxRate">
+ <text class="field-label">绋庣巼(%)</text>
+ <text :class="['field-value', row.taxRate ? '' : 'placeholder']">
+ {{ taxRateText || "璇烽�夋嫨绋庣巼" }}
+ </text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc"></up-icon>
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">鍚◣鍗曚环</text>
+ <u-input v-model="row.taxInclusiveUnitPrice"
+ type="number"
+ placeholder="璇疯緭鍏ュ惈绋庡崟浠�"
+ border="none"
+ inputAlign="right" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">鍚◣鎬讳环</text>
+ <text class="field-value readonly">{{ formatAmount(inclusiveTotal) }}</text>
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">涓嶅惈绋庢�讳环</text>
+ <u-input v-model="row.taxExclusiveTotalPrice"
+ type="number"
+ placeholder="璇疯緭鍏ヤ笉鍚◣鎬讳环"
+ border="none"
+ inputAlign="right" />
+ </view>
+
+ <view class="field-row"
+ @click="openInvoiceType">
+ <text class="field-label">鍙戠エ绫诲瀷</text>
+ <text :class="['field-value', row.invoiceType ? '' : 'placeholder']">
+ {{ row.invoiceType || "璇烽�夋嫨鍙戠エ绫诲瀷" }}
+ </text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc"></up-icon>
+ </view>
+ </view>
+
+ <up-action-sheet :show="showCategorySheet"
+ title="閫夋嫨浜у搧澶х被"
+ :actions="categoryActions"
+ @select="onSelectCategory"
+ @close="showCategorySheet = false"></up-action-sheet>
+
+ <up-action-sheet :show="showModelSheet"
+ title="閫夋嫨瑙勬牸鍨嬪彿"
+ :actions="modelActions"
+ @select="onSelectModel"
+ @close="showModelSheet = false"></up-action-sheet>
+
+ <up-action-sheet :show="showTaxRateSheet"
+ title="閫夋嫨绋庣巼"
+ :actions="taxRateActions"
+ @select="onSelectTaxRate"
+ @close="showTaxRateSheet = false"></up-action-sheet>
+
+ <up-action-sheet :show="showInvoiceSheet"
+ title="閫夋嫨鍙戠エ绫诲瀷"
+ :actions="INVOICE_TYPE_ACTIONS"
+ @select="onSelectInvoiceType"
+ @close="showInvoiceSheet = false"></up-action-sheet>
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from "vue";
+ import { modelList, productTreeList } from "@/api/basicData/product";
+ import { productInclusiveTotal } from "../_utils/mapper";
+
+ const INVOICE_TYPE_ACTIONS = [
+ { name: "澧炴櫘绁�", value: "澧炴櫘绁�" },
+ { name: "澧炰笓绁�", value: "澧炰笓绁�" },
+ ];
+
+ const props = defineProps({
+ row: {
+ type: Object,
+ required: true,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ },
+ // 绋庣巼瀛楀吀锛坲seDict('tax_rate') 鐨勮繑鍥炲�硷級
+ taxRateOptions: {
+ type: Array,
+ default: () => [],
+ },
+ });
+
+ defineEmits(["remove"]);
+
+ const categoryActions = ref([]);
+ const modelActions = ref([]);
+ const showCategorySheet = ref(false);
+ const showModelSheet = ref(false);
+ const showTaxRateSheet = ref(false);
+ const showInvoiceSheet = ref(false);
+
+ const inclusiveTotal = computed(() => productInclusiveTotal(props.row));
+
+ const taxRateActions = computed(() =>
+ (props.taxRateOptions || []).map(item => ({
+ name: item.label,
+ value: item.value,
+ }))
+ );
+
+ const taxRateText = computed(() => {
+ const matched = (props.taxRateOptions || []).find(
+ item => String(item.value) === String(props.row.taxRate)
+ );
+ return matched ? matched.label : "";
+ });
+
+ const formatAmount = value => Number(value || 0).toFixed(2);
+
+ const flattenTree = nodes => {
+ const result = [];
+ const walk = list => {
+ (list || []).forEach(item => {
+ if (item.children && item.children.length) {
+ walk(item.children);
+ } else {
+ result.push({
+ name: item.label || item.productName || "",
+ value: item.id || item.value,
+ });
+ }
+ });
+ };
+ walk(nodes);
+ return result;
+ };
+
+ const loadCategories = () => {
+ productTreeList()
+ .then(res => {
+ const nodes = Array.isArray(res) ? res : res?.data || [];
+ categoryActions.value = flattenTree(nodes);
+ })
+ .catch(() => {
+ categoryActions.value = [];
+ });
+ };
+
+ const openCategory = () => {
+ if (!categoryActions.value.length) loadCategories();
+ showCategorySheet.value = true;
+ };
+
+ const openModel = () => {
+ if (!props.row.productCategoryId) {
+ uni.showToast({ title: "璇峰厛閫夋嫨浜у搧澶х被", icon: "none" });
+ return;
+ }
+ if (!modelActions.value.length) {
+ uni.showToast({ title: "鏆傛棤瑙勬牸鍨嬪彿", icon: "none" });
+ return;
+ }
+ showModelSheet.value = true;
+ };
+
+ const openTaxRate = () => {
+ if (!taxRateActions.value.length) {
+ uni.showToast({ title: "鏆傛棤绋庣巼閫夐」", icon: "none" });
+ return;
+ }
+ showTaxRateSheet.value = true;
+ };
+
+ const openInvoiceType = () => {
+ showInvoiceSheet.value = true;
+ };
+
+ const onSelectCategory = action => {
+ showCategorySheet.value = false;
+ props.row.productCategoryId = action.value;
+ props.row.productCategory = action.name;
+ props.row.productModelId = "";
+ props.row.specificationModel = "";
+ props.row.unit = "";
+ modelActions.value = [];
+ modelList({ id: action.value })
+ .then(res => {
+ const rows = res?.data?.records || res?.data || res?.records || res || [];
+ modelActions.value = (Array.isArray(rows) ? rows : []).map(item => ({
+ name: item.model || item.specification || "",
+ value: item.id,
+ unit: item.unit,
+ }));
+ })
+ .catch(() => {
+ modelActions.value = [];
+ });
+ };
+
+ const onSelectModel = action => {
+ showModelSheet.value = false;
+ props.row.productModelId = action.value;
+ props.row.specificationModel = action.name;
+ if (action.unit) props.row.unit = action.unit;
+ };
+
+ const onSelectTaxRate = action => {
+ showTaxRateSheet.value = false;
+ props.row.taxRate = action.value;
+ };
+
+ const onSelectInvoiceType = action => {
+ showInvoiceSheet.value = false;
+ props.row.invoiceType = action.value;
+ };
+
+ // 鎵撳紑鍨嬪彿闈㈡澘鍓嶅厛鎶婂凡鏈夊ぇ绫诲搴旂殑鍨嬪彿鎹炲洖鏉ワ紙缂栬緫鍥炴樉鍦烘櫙锛�
+ onMounted(() => {
+ if (!props.row.productCategoryId) return;
+ modelList({ id: props.row.productCategoryId })
+ .then(res => {
+ const rows = res?.data?.records || res?.data || res?.records || res || [];
+ modelActions.value = (Array.isArray(rows) ? rows : []).map(item => ({
+ name: item.model || item.specification || "",
+ value: item.id,
+ unit: item.unit,
+ }));
+ // 鍙瓨浜嗗瀷鍙峰悕绉版椂鍙嶆煡 id锛屼繚璇侀�夋嫨鍣ㄨ兘鍛戒腑
+ if (!props.row.productModelId && props.row.specificationModel) {
+ const matched = modelActions.value.find(
+ item => item.name === props.row.specificationModel
+ );
+ if (matched) props.row.productModelId = matched.value;
+ }
+ })
+ .catch(() => {});
+ });
+</script>
+
+<style scoped lang="scss">
+ .product-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 0 12px 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .product-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 0;
+ }
+
+ .product-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .product-body {
+ padding-top: 4px;
+ }
+
+ .field-row {
+ display: flex;
+ align-items: center;
+ min-height: 44px;
+ border-bottom: 1px solid #f5f6fa;
+ }
+
+ .field-row:last-child {
+ border-bottom: none;
+ }
+
+ .field-label {
+ width: 96px;
+ flex-shrink: 0;
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .field-value {
+ flex: 1;
+ min-width: 0;
+ font-size: 14px;
+ color: #303133;
+ text-align: right;
+ word-break: break-all;
+ }
+
+ .field-value.placeholder {
+ color: #c0c4cc;
+ }
+
+ .field-value.readonly {
+ color: #909399;
+ }
+</style>
diff --git a/src/pages/projectManagement/project/_components/TeamRow.vue b/src/pages/projectManagement/project/_components/TeamRow.vue
new file mode 100644
index 0000000..31fb7e7
--- /dev/null
+++ b/src/pages/projectManagement/project/_components/TeamRow.vue
@@ -0,0 +1,148 @@
+<!--
+ 椤圭洰鍥㈤槦瀛愯〃鐨勪竴琛�
+
+ 鎴愬憳 / 瑙掕壊涓嬫媺鐢辩埗绾х粺涓�鎷夊彇鍚庝紶鍏ワ紙澶氫釜琛屽叡鐢ㄤ竴浠介�夐」锛屼笉閲嶅璇锋眰锛夈��
+ 鎻愪氦鏃跺悗绔笉鍙嶆煡 userName / userRoleName锛屾墍浠ラ�変腑鍚庤鎶� label 涓�璧疯鍦� row 涓娿��
+-->
+<template>
+ <view class="team-card">
+ <view class="team-header">
+ <text class="team-title">鎴愬憳 {{ index + 1 }}</text>
+ <up-icon name="trash"
+ color="#ee0a24"
+ size="18"
+ @click="$emit('remove')"></up-icon>
+ </view>
+ <up-divider></up-divider>
+
+ <view class="team-body">
+ <view class="field-row">
+ <text class="field-label">濮撳悕</text>
+ <FormPicker v-model="row.memberId"
+ type="select"
+ title="閫夋嫨椤圭洰鎴愬憳"
+ :options="userOptions"
+ placeholder="璇烽�夋嫨鎴愬憳"
+ @change="onMemberChange" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">椤圭洰缁勮鑹�</text>
+ <FormPicker v-model="row.roleId"
+ type="select"
+ title="閫夋嫨椤圭洰缁勮鑹�"
+ :options="roleOptions"
+ placeholder="璇烽�夋嫨瑙掕壊"
+ @change="onRoleChange" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">杩涘叆鏃ユ湡</text>
+ <FormPicker v-model="row.enterDate"
+ type="date"
+ placeholder="璇烽�夋嫨杩涘叆鏃ユ湡" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">绂诲紑鏃ユ湡</text>
+ <FormPicker v-model="row.leaveDate"
+ type="date"
+ placeholder="璇烽�夋嫨绂诲紑鏃ユ湡" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">鑱旂郴鏂瑰紡</text>
+ <u-input v-model="row.phone"
+ placeholder="璇疯緭鍏ヨ仈绯绘柟寮�"
+ border="none"
+ inputAlign="right" />
+ </view>
+
+ <view class="field-row">
+ <text class="field-label">澶囨敞</text>
+ <u-input v-model="row.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ border="none"
+ inputAlign="right" />
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import FormPicker from "@/components/FormPicker.vue";
+
+ const props = defineProps({
+ row: {
+ type: Object,
+ required: true,
+ },
+ index: {
+ type: Number,
+ default: 0,
+ },
+ userOptions: {
+ type: Array,
+ default: () => [],
+ },
+ roleOptions: {
+ type: Array,
+ default: () => [],
+ },
+ });
+
+ defineEmits(["remove"]);
+
+ // 閫変腑鍚庢妸 label 璁板埌琛屼笂锛屾彁浜ゆ椂闅� payload 涓�璧峰彂缁欏悗绔�
+ const onMemberChange = (value, item) => {
+ props.row.memberName = item?.name || props.row.memberName || "";
+ };
+
+ const onRoleChange = (value, item) => {
+ props.row.roleName = item?.name || props.row.roleName || "";
+ };
+</script>
+
+<style scoped lang="scss">
+ .team-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 0 12px 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .team-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 0;
+ }
+
+ .team-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .team-body {
+ padding-top: 4px;
+ }
+
+ .field-row {
+ display: flex;
+ align-items: center;
+ min-height: 44px;
+ border-bottom: 1px solid #f5f6fa;
+ }
+
+ .field-row:last-child {
+ border-bottom: none;
+ }
+
+ .field-label {
+ width: 96px;
+ flex-shrink: 0;
+ font-size: 14px;
+ color: #606266;
+ }
+</style>
diff --git a/src/pages/projectManagement/project/_utils/dict.js b/src/pages/projectManagement/project/_utils/dict.js
new file mode 100644
index 0000000..4e5b254
--- /dev/null
+++ b/src/pages/projectManagement/project/_utils/dict.js
@@ -0,0 +1,43 @@
+/**
+ * 瀛楀吀娓叉煋灏忓伐鍏�
+ *
+ * 椤圭洰绠$悊鐨勩�屽崟鎹姸鎬� / 璁″垝鐘舵�� / 瀹℃牳鐘舵�併�嶉兘鏉ヨ嚜鍚庣瀛楀吀
+ * 锛坆ill_status / plan_status / project_management锛夛紝useDict 鎷垮埌鐨勯」褰㈠
+ * { label, value, elTagType, elTagClass }銆傚垪琛ㄤ笌璇︽儏閮借鎶� value 娓叉煋鎴�
+ * 鏍囩鏂囨 + 棰滆壊锛岃繖閲屾敹涓�澶勩��
+ *
+ * up-tag 鍙 info/primary/success/warning/error锛屽悗绔� listClass 鍙兘鏄�
+ * danger锛圗lement Plus 璇嶈〃锛夛紝蹇呴』杞崲锛屽惁鍒欐爣绛句笉鏄剧ず棰滆壊銆�
+ */
+
+const TAG_TYPE_MAP = {
+ danger: "error",
+ "": "info",
+ default: "info",
+};
+
+/** 瀛楀吀鍊� 鈫� 鏍囩鏂囨锛屾壘涓嶅埌杩斿洖鍘熷�� */
+export function dictLabel(options, value) {
+ if (value === undefined || value === null || value === "") return "";
+ const matched = (options || []).find(
+ item => String(item.value) === String(value)
+ );
+ return matched ? matched.label : String(value);
+}
+
+/** 瀛楀吀鍊� 鈫� up-tag 鍙敤鐨� type */
+export function dictTagType(options, value) {
+ const matched = (options || []).find(
+ item => String(item.value) === String(value)
+ );
+ const raw = matched?.elTagType ?? "";
+ return TAG_TYPE_MAP[raw] || raw || "info";
+}
+
+/** useDict 鐨勫瓧鍏搁」 鈫� FormPicker / FilterPanel 璁よ瘑鐨勪笅鎷夐�夐」 */
+export function toPickerOptions(options) {
+ return (options || []).map(item => ({
+ name: item.label,
+ value: item.value,
+ }));
+}
diff --git a/src/pages/projectManagement/project/_utils/mapper.js b/src/pages/projectManagement/project/_utils/mapper.js
new file mode 100644
index 0000000..14a33f3
--- /dev/null
+++ b/src/pages/projectManagement/project/_utils/mapper.js
@@ -0,0 +1,318 @@
+/**
+ * 椤圭洰绠$悊瀛楁鏄犲皠
+ *
+ * 鍚庣 projectManagement 鐨勫瓧娈靛悕涓庝笟鍔″惈涔夊樊寮傚緢澶э紝鏄犲皠鍏崇郴鍙栬嚜绠$悊绔�
+ * views/projectManagement/Management/components/formDia.vue 涓� projectDetail.vue锛�
+ *
+ * info 灞� no=鍗曟嵁缂栧彿銆乼itle=椤圭洰鍚嶇О銆乧lientName=瀹㈡埛鍚嶇О銆�
+ * establishTime=绔嬮」鏃ユ湡銆乻ource=椤圭洰鏉ユ簮銆乵anagerName=绔嬮」浜恒��
+ * orderAmount=椤圭洰閲戦銆乺eviewStatus=瀹℃牳鐘舵�併��
+ * status=鍗曟嵁鐘舵�併�乻tage=璁″垝鐘舵�併�乸lanStartTime/planEndTime=璁″垝璧锋
+ * contractInfo 灞� name=鑱旂郴浜恒�乻ex='鐢�'/'濂�'銆乴ineaFissa=浼佷笟寰俊銆亀x=寰俊銆�
+ * origineEtnica=鍦板潃銆乺appresentanteLegale=澶囨敞
+ * shippingAddress 灞� consignee=鏀惰揣浜恒�乧ontract=鑱旂郴鐢佃瘽銆乤ddress=鍦板潃
+ * teamList 椤� userId/userName銆乽serRoleId/userRoleName銆乯oinTime/departTime銆�
+ * contact=鑱旂郴鏂瑰紡
+ *
+ * 鍙屽悜杞崲鍏ㄩ儴鏀舵暃鍦ㄨ繖閲岋紝椤甸潰閲屼笉鍑虹幇瑁稿瓧娈靛悕銆�
+ * 璇︽儏杩斿洖锛歞etail.info / detail.shippingAddress / detail.contractInfo /
+ * detail.salesLedgerProductList / detail.info.teamList / detail.info.attachmentList
+ * 鎻愪氦缁撴瀯锛歿 info, shippingAddress, contractInfo, salesLedgerProductList }
+ */
+
+export const GENDER_OPTIONS = [
+ { name: "鐢�", value: "1" },
+ { name: "濂�", value: "2" },
+];
+
+let uidSeed = 1;
+const nextUid = () => `p_${uidSeed++}`;
+
+const toStr = value =>
+ value === undefined || value === null ? "" : String(value);
+
+// 鍚庣瀛樼殑鏄腑鏂囨�у埆锛岃〃鍗曢�夌殑鏄� '1'/'2'
+const genderToValue = sex => (sex === "鐢�" ? "1" : sex === "濂�" ? "2" : "");
+const genderFromValue = value =>
+ value === "1" ? "鐢�" : value === "2" ? "濂�" : "";
+
+// 瀛楀吀鍊煎悗绔彲鑳借繑鍥炴暟瀛楋紝閫夋嫨鍣ㄦ寜瀛楃涓叉瘮瀵�
+const dictValue = value =>
+ value === undefined || value === null || value === "" ? "" : String(value);
+
+const idOrNull = value =>
+ value === undefined || value === null || value === "" ? null : value;
+
+// 鏀惰揣鍦板潃銆佷骇鍝佸瓙琛ㄩ兘鍙兘涓虹┖瀵硅薄鎴栫己澶憋紝缁熶竴鍙栨暟缁�
+const asArray = value => (Array.isArray(value) ? value : []);
+
+/** 鍒楄〃琛岋細鍒楄〃鎺ュ彛瀛楁 鈫� 灞曠ず瀛楁 */
+export function toListRow(record = {}) {
+ return {
+ id: record.id,
+ billNo: record.no ?? record.billNo ?? "",
+ projectName: record.title ?? record.projectName ?? "",
+ billStatus: dictValue(record.billStatus ?? record.status),
+ auditStatus: dictValue(record.reviewStatus ?? record.auditStatus),
+ projectStage: dictValue(record.stage ?? record.projectStage),
+ customerName: record.clientName ?? record.customerName ?? "",
+ setupDate: record.establishTime ?? record.setupDate ?? "",
+ projectSource: record.source ?? record.projectSource ?? "",
+ projectClassification:
+ record.departmentName ?? record.projectClassification ?? "",
+ projectAmount: record.orderAmount,
+ salesContractCount: record.salesContractCount,
+ raw: record,
+ };
+}
+
+/** 璇︽儏鎺ュ彛杩斿洖 鈫� 璇︽儏椤靛悇鍒嗗尯 */
+export function toDetail(detail = {}) {
+ const info = detail?.info || {};
+ const shippingAddress = detail?.shippingAddress || {};
+ const contractInfo = detail?.contractInfo || {};
+
+ const rawPhase =
+ detail?.phaseList ||
+ detail?.projectPhaseList ||
+ detail?.projectStageList ||
+ info?.phaseList ||
+ info?.projectPhaseList;
+
+ return {
+ info,
+ shippingAddress: {
+ consignee: shippingAddress.consignee ?? "",
+ contract: shippingAddress.contract ?? "",
+ address: shippingAddress.address ?? "",
+ },
+ contractInfo: {
+ name: contractInfo.name ?? "",
+ sex: contractInfo.sex ?? "",
+ birthday: contractInfo.birthday ?? "",
+ department: contractInfo.department ?? "",
+ job: contractInfo.job ?? "",
+ phoneNumber: contractInfo.phoneNumber ?? "",
+ email: contractInfo.email ?? "",
+ qq: contractInfo.qq ?? "",
+ wx: contractInfo.wx ?? "",
+ // 鍚庣杩欎袱涓瓧娈靛悕涓庝笟鍔″惈涔夊畬鍏ㄦ棤鍏筹紝UI 涓婃寜涓氬姟鍚箟灞曠ず
+ workWechat: contractInfo.lineaFissa ?? "",
+ address: contractInfo.origineEtnica ?? "",
+ remark: contractInfo.rappresentanteLegale ?? "",
+ },
+ productList: asArray(detail?.salesLedgerProductList),
+ teamList: asArray(info.teamList),
+ attachments: asArray(info.attachmentList).map(a => ({
+ id: a.id ?? a.fileId,
+ name: a.fileName ?? a.name ?? "",
+ url: a.url ?? a.fileUrl ?? a.path ?? "",
+ })),
+ // 琛ㄥ崟閲屾病鏈夊彲缂栬緫鐨勯樁娈靛瓙琛紙鍚庣 /save 涓嶅悆闃舵锛夛紝杩欓噷鍙粰璇︽儏椤靛彧璇诲睍绀虹敤
+ phaseList: asArray(rawPhase),
+ };
+}
+
+/** 璇︽儏鎺ュ彛杩斿洖 鈫� 琛ㄥ崟锛堝惈瀛愯〃锛� */
+export function toForm(detail = {}) {
+ const d = toDetail(detail);
+ const info = d.info;
+ const contractInfo = detail?.contractInfo || {};
+ const shippingAddress = detail?.shippingAddress || {};
+
+ const form = {
+ id: info.id,
+ billNo: info.no ?? "",
+ projectName: info.title ?? "",
+ clientId: info.clientId ?? "",
+ customerName: info.clientName ?? "",
+ parentProjectId: info.projectManagementInfoParentId ?? "",
+ parentProjectName: info.projectManagementInfoParentName ?? "",
+ projectManagementPlanId: info.projectManagementPlanId ?? "",
+ projectSource: info.source ?? "",
+ managerId: info.managerId ?? "",
+ creatorName: info.managerName ?? "",
+ salesmanId: info.salesmanId ?? "",
+ salesmanName: info.salesmanName ?? "",
+ departmentId: info.departmentId ?? "",
+ departmentName: info.departmentName ?? "",
+ setupDate: info.establishTime ?? "",
+ planStartDate: info.planStartTime ?? "",
+ planEndDate: info.planEndTime ?? "",
+ actualStartDate: info.actualStartTime ?? "",
+ actualEndDate: info.actualEndTime ?? "",
+ orderDate: info.orderDate ?? "",
+ projectAmount: info.orderAmount ?? "",
+ billStatus: dictValue(info.status),
+ auditStatus: dictValue(info.reviewStatus),
+ projectStage: dictValue(info.stage ?? info.projectStage),
+ remark: info.remark ?? "",
+ contactName: contractInfo.name ?? "",
+ contactGender: genderToValue(contractInfo.sex),
+ contactBirthday: contractInfo.birthday ?? "",
+ contactDept: contractInfo.department ?? "",
+ contactJob: contractInfo.job ?? "",
+ contactMobile: contractInfo.phoneNumber ?? "",
+ contactEmail: contractInfo.email ?? "",
+ contactQq: contractInfo.qq ?? "",
+ contactWechat: contractInfo.wx ?? "",
+ contactWorkWechat: contractInfo.lineaFissa ?? "",
+ contactAddress: contractInfo.origineEtnica ?? "",
+ contactRemark: contractInfo.rappresentanteLegale ?? "",
+ };
+
+ // 鏀惰揣鍦板潃鍚庣鍙瓨涓�鏉★紝杩欓噷鐢ㄥ崟琛岀紪杈戯紝鎻愪氦鏃跺彇绗竴琛�
+ const addressList = shippingAddress?.address
+ ? [
+ {
+ receiver: shippingAddress.consignee ?? "",
+ phone: shippingAddress.contract ?? "",
+ address: shippingAddress.address ?? "",
+ },
+ ]
+ : [];
+
+ const teamList = d.teamList.map(t => ({
+ memberId: t.userId ?? "",
+ memberName: t.userName ?? "",
+ roleId: t.userRoleId ?? "",
+ roleName: t.userRoleName ?? "",
+ enterDate: t.joinTime ?? "",
+ leaveDate: t.departTime ?? "",
+ phone: t.contact ?? "",
+ remark: t.remark ?? "",
+ }));
+
+ const productList = d.productList.map(p => ({
+ uid: nextUid(),
+ productCategoryId: p.productCategoryId ?? "",
+ productCategory: p.productCategory ?? "",
+ productModelId: p.productModelId ?? "",
+ specificationModel: p.specificationModel ?? "",
+ unit: p.unit ?? "",
+ quantity: p.quantity ?? "",
+ taxRate: p.taxRate ?? "",
+ taxInclusiveUnitPrice: p.taxInclusiveUnitPrice ?? "",
+ taxExclusiveTotalPrice: p.taxExclusiveTotalPrice ?? "",
+ invoiceType: p.invoiceType ?? "",
+ }));
+
+ return { form, teamList, addressList, productList, attachments: d.attachments };
+}
+
+/** 琛ㄥ崟 鈫� 鎻愪氦缁撴瀯 */
+export function toPayload({
+ form,
+ teamList,
+ addressList,
+ productList,
+ attachmentIds,
+}) {
+ const shippingRow = asArray(addressList)[0] || {};
+
+ const info = {
+ id: form.id ?? null,
+ no: form.billNo,
+ title: form.projectName,
+ clientId: idOrNull(form.clientId),
+ clientName: form.customerName,
+ projectManagementInfoParentId: idOrNull(form.parentProjectId),
+ projectManagementPlanId: idOrNull(form.projectManagementPlanId),
+ establishTime: form.setupDate,
+ source: form.projectSource,
+ managerId: idOrNull(form.managerId),
+ managerName: form.creatorName,
+ salesmanId: idOrNull(form.salesmanId),
+ salesmanName: form.salesmanName || "",
+ departmentId: idOrNull(form.departmentId),
+ departmentName: form.departmentName || "",
+ planStartTime: form.planStartDate,
+ planEndTime: form.planEndDate,
+ actualStartTime: form.actualStartDate,
+ actualEndTime: form.actualEndDate,
+ orderDate: form.orderDate,
+ orderAmount: Number(form.projectAmount) || 0,
+ status: form.billStatus === "" ? null : Number(form.billStatus),
+ reviewStatus: form.auditStatus === "" ? null : Number(form.auditStatus),
+ stage: form.projectStage === "" ? null : Number(form.projectStage),
+ remark: form.remark,
+ attachmentIds: asArray(attachmentIds),
+ // userName / userRoleName 鏄笅鎷� label锛屽悗绔笉鍙嶆煡锛屽繀椤诲墠绔甫涓�
+ teamList: asArray(teamList).map(t => ({
+ userId: t.memberId,
+ userName: t.memberName,
+ userRoleId: t.roleId,
+ userRoleName: t.roleName,
+ joinTime: t.enterDate,
+ departTime: t.leaveDate,
+ contact: t.phone,
+ remark: t.remark,
+ })),
+ };
+
+ const shippingAddress = {
+ id: undefined,
+ consignee: shippingRow.receiver,
+ contract: shippingRow.phone,
+ address: shippingRow.address,
+ };
+
+ const contractInfo = {
+ id: undefined,
+ name: form.contactName,
+ sex: genderFromValue(form.contactGender),
+ birthday: form.contactBirthday,
+ department: form.contactDept,
+ job: form.contactJob,
+ phoneNumber: form.contactMobile,
+ email: form.contactEmail,
+ qq: form.contactQq,
+ wx: form.contactWechat,
+ lineaFissa: form.contactWorkWechat,
+ origineEtnica: form.contactAddress,
+ rappresentanteLegale: form.contactRemark,
+ };
+
+ return {
+ info,
+ shippingAddress,
+ contractInfo,
+ salesLedgerProductList: asArray(productList).map(p => ({
+ productCategoryId: p.productCategoryId,
+ productCategory: p.productCategory,
+ productModelId: p.productModelId,
+ specificationModel: p.specificationModel,
+ unit: p.unit,
+ quantity: Number(p.quantity) || 0,
+ taxRate: p.taxRate,
+ taxInclusiveUnitPrice: Number(p.taxInclusiveUnitPrice) || 0,
+ taxInclusiveTotalPrice: productInclusiveTotal(p),
+ taxExclusiveTotalPrice: Number(p.taxExclusiveTotalPrice) || 0,
+ invoiceType: p.invoiceType,
+ })),
+ };
+}
+
+/** 鍚◣鎬讳环 = 鍚◣鍗曚环 脳 鏁伴噺 */
+export function productInclusiveTotal(row) {
+ const total =
+ Number(row?.taxInclusiveUnitPrice || 0) * Number(row?.quantity || 0);
+ return Number(total.toFixed(2));
+}
+
+/** 璇︽儏椤靛睍绀虹敤鐨勭┖浜у搧琛� */
+export function createEmptyProduct() {
+ return {
+ uid: nextUid(),
+ productCategoryId: "",
+ productCategory: "",
+ productModelId: "",
+ specificationModel: "",
+ unit: "",
+ quantity: "",
+ taxRate: "",
+ taxInclusiveUnitPrice: "",
+ taxExclusiveTotalPrice: "",
+ invoiceType: "",
+ };
+}
diff --git a/src/pages/projectManagement/project/_utils/stage.js b/src/pages/projectManagement/project/_utils/stage.js
new file mode 100644
index 0000000..e363a91
--- /dev/null
+++ b/src/pages/projectManagement/project/_utils/stage.js
@@ -0,0 +1,131 @@
+/**
+ * 椤圭洰闃舵 / 杩涘害姹囨姤鍏辩敤閫昏緫
+ *
+ * 闃舵鏁版嵁鏈変袱涓潵婧愶紝鍚箟涓嶅悓锛屽埆娣凤細
+ * listPlan() 鈫� 椤圭洰绫诲瀷涓嬬殑銆岃鍒掕妭鐐广�嶏紙planNodeList锛夛紝
+ * 鏄ā鏉匡紝鍐冲畾浜嗛樁娈佃繘搴︽潯涓庛�岃繘搴︽眹鎶ャ�嶇殑鍙�夐」
+ * listStage(projectId) 鈫� 璇ラ」鐩凡姹囨姤杩囩殑闃舵璁板綍锛岃鎯呴〉銆岄」鐩樁娈点�峊ab 灞曠ず
+ *
+ * saveStage 鐨勫叆鍙傚瓧娈靛悕涓庤〃鍗曞瓧娈靛悕涓嶄竴鑷达紙琛ㄥ崟鐢� phaseName/planDays锛�
+ * 鎺ュ彛鐢� projectManagementPlanNodeId/estimatedDuration锛夛紝杞崲涔熸敹鏁涘湪杩欓噷銆�
+ */
+import { listPlan } from "@/api/projectManagement/projectType";
+import { listStage } from "@/api/projectManagement/project";
+import { unwrapList } from "@/utils/list";
+
+/** 鍙栨煇涓」鐩被鍨嬩笅鐨勮鍒掕妭鐐瑰垪琛� */
+export async function fetchPlanNodeList(planId) {
+ if (planId === undefined || planId === null || planId === "") return [];
+ try {
+ const res = await listPlan({ current: 1, size: 999 });
+ const records = unwrapList(res);
+ const plan = records.find(p => String(p.id) === String(planId)) || {};
+ const nodes = Array.isArray(plan.planNodeList) ? plan.planNodeList : [];
+ // 杩涘害鏉′笌涓嬫媺閮芥寜 sort 鎺掑簭锛屽悗绔笉淇濊瘉椤哄簭
+ return [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0));
+ } catch (error) {
+ return [];
+ }
+}
+
+/** 璁″垝鑺傜偣 鈫� 涓嬫媺閫夐」锛堝垪琛ㄩ〉涓庤繘搴︽眹鎶ュ叡鐢ㄤ竴浠� label 瑙勫垯锛� */
+export function planNodeOptions(nodes) {
+ return (nodes || [])
+ .map(node => ({
+ name: node.name || node.workContent || "",
+ value: node.id,
+ estimatedDuration: node.estimatedDuration,
+ }))
+ .filter(item => item.value !== undefined && item.value !== null);
+}
+
+/** 璁″垝鑺傜偣 id 鈫� 闃舵鍚嶇О锛岃鎯呴〉銆岄」鐩樁娈点�峊ab 閲屽悗绔彧鍥� id锛岄渶瑕佸墠绔弽鏌� */
+export function stageNameFromNodes(nodes, nodeId) {
+ const matched = (nodes || []).find(n => String(n.id) === String(nodeId));
+ return matched?.name || matched?.workContent || "";
+}
+
+/**
+ * 鍙栭」鐩殑宸叉眹鎶ラ樁娈�
+ * 杩斿洖缁撴瀯鍚庣鏈� data.data / data.records 绛夊绉嶅舰鎬侊紝鐢� unwrapList 鍏滃簳
+ */
+export async function fetchProjectStages(projectId) {
+ if (!projectId) return [];
+ try {
+ const res = await listStage(projectId);
+ const inner = res?.data?.data ?? res?.data ?? res;
+ return unwrapList({ data: inner });
+ } catch (error) {
+ return [];
+ }
+}
+
+/**
+ * 鏍规嵁褰撳墠璁″垝鐘舵�佹帹绠楅粯璁ら�変腑鐨勮鍒掕妭鐐�
+ * 鍚庣 stage 瀛樼殑鏄瓧鍏稿�硷紝鍙兘鍛戒腑鑺傜偣 id锛屼篃鍙兘鏄簭鍙凤紝閮藉厹涓�涓�
+ */
+export function defaultPlanNodeId(stageValue, nodes) {
+ const list = Array.isArray(nodes) ? nodes : [];
+ if (list.length === 0) return "";
+ const direct = list.find(n => String(n.id) === String(stageValue));
+ if (direct) return direct.id;
+ const index = Number(stageValue);
+ if (Number.isFinite(index)) {
+ const byIndex = list[index - 1] || list[index] || list[0];
+ if (byIndex) return byIndex.id;
+ }
+ return list[0].id;
+}
+
+/**
+ * 杩涘害姹囨姤琛ㄥ崟 鈫� saveStage 鍏ュ弬
+ * @param {object} options
+ * @param {string|number} options.projectId
+ * @param {object} options.projectInfo 椤圭洰璇︽儏鎺ュ彛鐨� info 鑺傜偣
+ * @param {Array} options.planNodes fetchPlanNodeList 鐨勭粨鏋�
+ * @param {object} options.form 杩涘害姹囨姤琛ㄥ崟
+ * @param {Array} options.attachments CommonUpload 鐨勬枃浠跺璞℃暟缁�
+ * @param {string} options.leaderId 褰撳墠鐧诲綍鐢ㄦ埛 id
+ * @param {string} options.leaderName 褰撳墠鐧诲綍鐢ㄦ埛鏄电О
+ */
+export function buildStagePayload({
+ projectId,
+ projectInfo,
+ planNodes,
+ form,
+ attachments,
+ leaderId,
+ leaderName,
+}) {
+ const info = projectInfo || {};
+ const node =
+ (planNodes || []).find(n => String(n.id) === String(form.planNodeId)) || {};
+ const description = form.remark
+ ? `${form.reportDate || ""} ${form.remark}`.trim()
+ : `${form.reportDate || ""} 杩涘害姹囨姤`.trim();
+
+ // 鍚庣鍙瓨涓�涓� progress銆傜疮璁¤繘搴﹀~浜嗗氨浼樺厛鐢ㄥ畠锛岀暀绌烘墠閫�鍥炲畬鎴愯繘搴� 鈥斺��
+ // 鐢ㄧ┖鍊煎垽鏂�屼笉鏄� `??`锛屽惁鍒欒鍒濆鍖栨垚 "0" 鐨勭疮璁¤繘搴︿細鎶婂畬鎴愯繘搴﹀悶鎺夈��
+ const totalProgress = form.totalProgress;
+ const progress =
+ totalProgress === "" || totalProgress === null || totalProgress === undefined
+ ? Number(form.completionProgress || 0) || 0
+ : Number(totalProgress) || 0;
+
+ return {
+ id: null,
+ projectManagementPlanNodeId: form.planNodeId,
+ projectManagementInfoId: projectId,
+ description,
+ // 璐熻矗浜哄悗绔笉鍙嶆煡锛屽墠绔甫褰撳墠鐧诲綍浜猴紱鍙栦笉鍒板啀閫�鍥為」鐩珛椤逛汉
+ actualLeaderId: leaderId || info.managerId || null,
+ actualLeaderName: leaderName || info.managerName || "",
+ estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
+ planStartTime: form.planStartTime || info.planStartTime || null,
+ planEndTime: form.planEndTime || info.planEndTime || null,
+ actualStartTime: form.actualStartTime || null,
+ actualEndTime: form.actualEndTime || null,
+ progress,
+ storageBlobDTOs: Array.isArray(attachments) ? attachments : [],
+ };
+}
diff --git a/src/pages/projectManagement/project/archive.vue b/src/pages/projectManagement/project/archive.vue
new file mode 100644
index 0000000..1c7ba29
--- /dev/null
+++ b/src/pages/projectManagement/project/archive.vue
@@ -0,0 +1,186 @@
+<!--
+ 鏂囨。褰掓。
+
+ 鏁版嵁鏉ヨ嚜 getProjectAttachments(id)锛岃繑鍥炴寜妯″潡鍒嗙粍鐨勯檮浠讹細
+ [{ moduleName, files: [{ name, url }] }]
+ 锛堝榻愮鐞嗙 components/ArchiveDialog.vue 鐨� res.data 缁撴瀯锛�
+
+ 闄勪欢涓嬭浇澶嶇敤 utils/file.js 鐨� openFile锛歳equest.ts 鏈�忎紶 responseType锛�
+ 鍙兘 downloadFile 鍒颁复鏃舵枃浠跺啀 openDocument銆�
+-->
+<template>
+ <view class="project-archive">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <view v-if="loading"
+ class="state-text">
+ <text>鍔犺浇涓�...</text>
+ </view>
+
+ <view v-else-if="groups.length === 0"
+ class="state-text">
+ <text>鏆傛棤褰掓。闄勪欢</text>
+ </view>
+
+ <view v-else
+ class="archive-list">
+ <view v-for="(group, index) in groups"
+ :key="index"
+ class="archive-group">
+ <view class="group-header"
+ @click="toggleGroup(index)">
+ <text class="group-title">
+ {{ group.moduleName || "鏈懡鍚嶆ā鍧�" }}锛坽{ group.files.length }}锛�
+ </text>
+ <up-icon :name="isOpen(index) ? 'arrow-up' : 'arrow-down'"
+ size="16"
+ color="#909399"></up-icon>
+ </view>
+
+ <view v-if="isOpen(index)"
+ class="file-list">
+ <view v-for="(file, fileIndex) in group.files"
+ :key="fileIndex"
+ class="file-item"
+ @click="openFile(file.url)">
+ <up-icon name="attach"
+ size="18"
+ color="#5b8ff9"></up-icon>
+ <text class="file-name">{{ file.name || "鏈懡鍚嶆枃浠�" }}</text>
+ <up-icon name="download"
+ size="18"
+ color="#909399"></up-icon>
+ </view>
+ </view>
+ </view>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { openFile } from "@/utils/file";
+ import { getProjectAttachments } from "@/api/projectManagement/project";
+
+ const loading = ref(true);
+ const projectId = ref("");
+ const projectName = ref("");
+ const groups = ref([]);
+ const openIndex = ref([]);
+
+ const pageTitle = computed(() =>
+ projectName.value ? `鏂囨。褰掓。 - ${projectName.value}` : "鏂囨。褰掓。"
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const isOpen = index => openIndex.value.includes(index);
+
+ const toggleGroup = index => {
+ openIndex.value = isOpen(index)
+ ? openIndex.value.filter(item => item !== index)
+ : [...openIndex.value, index];
+ };
+
+ // 鍚庣鍙兘鍥� files 涓� null锛岀粺涓�鎴愭暟缁勶紝閬垮厤妯℃澘閲屽彇 length 鎶ラ敊
+ const normalize = list =>
+ (Array.isArray(list) ? list : []).map(group => ({
+ moduleName: group?.moduleName ?? "",
+ files: (Array.isArray(group?.files) ? group.files : []).map(file => ({
+ name: file?.name ?? "",
+ url: file?.url ?? "",
+ })),
+ }));
+
+ const loadAttachments = async () => {
+ try {
+ const res = await getProjectAttachments(projectId.value);
+ const raw = res?.data?.data ?? res?.data ?? res;
+ groups.value = normalize(raw);
+ // 鍏ㄩ儴鍒嗙粍榛樿灞曞紑锛屼笌绠$悊绔� ArchiveDialog 鐨� activeNames 琛屼负涓�鑷�
+ openIndex.value = groups.value.map((_, index) => index);
+ } catch (error) {
+ groups.value = [];
+ uni.showToast({ title: "鑾峰彇褰掓。闄勪欢澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ projectId.value = options?.id || "";
+ projectName.value = options?.name ? decodeURIComponent(options.name) : "";
+ if (projectId.value) {
+ loadAttachments();
+ } else {
+ loading.value = false;
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ .project-archive {
+ min-height: 100vh;
+ background: #f5f6fa;
+ }
+
+ .state-text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 80px 0;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .archive-list {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .archive-group {
+ background: #ffffff;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .group-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 16px;
+ background: #f8fbff;
+ }
+
+ .group-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .file-list {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .file-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 16px;
+ border-top: 1px solid #f0f2f5;
+ }
+
+ .file-name {
+ flex: 1;
+ font-size: 14px;
+ color: #606266;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/projectManagement/project/config.vue b/src/pages/projectManagement/project/config.vue
new file mode 100644
index 0000000..aead187
--- /dev/null
+++ b/src/pages/projectManagement/project/config.vue
@@ -0,0 +1,150 @@
+<!--
+ 涓氬姟鍙傛暟璁剧疆锛堝榻愮鐞嗙 views/projectManagement/config/index.vue锛�
+
+ 鐩墠鍙湁涓�涓弬鏁般�屽悎鍚屽師浠剁害瀹氬洖浼犳彁鍓嶆彁閱掑ぉ鏁般�嶏紝鐢ㄤ簬閿�鍞彴璐﹂噷
+ 銆屽悎鍚屽師浠剁害瀹氬洖浼犳棩鏈熴�嶇殑涓存湡棰勮锛歞ays = 鍥炰紶鏃ユ湡 - 浠婂ぉ锛宒ays <= N 鎶ヨ銆�
+ 鍚庣 GET /projectManagement/config 杩斿洖 { id, contractReturnReminderDays }锛�
+ PUT 鍚屽湴鍧�鏁翠綋瑕嗙洊锛屾墍浠� id 蹇呴』鍘熸牱甯﹀洖鍘汇��
+-->
+<template>
+ <view class="project-config">
+ <PageHeader title="涓氬姟鍙傛暟璁剧疆"
+ @back="goBack" />
+
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="200"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group class="form-section">
+ <up-form-item label="鍚堝悓鍘熶欢绾﹀畾鍥炰紶鎻愬墠鎻愰啋澶╂暟"
+ prop="contractReturnReminderDays"
+ required>
+ <up-input v-model="form.contractReturnReminderDays"
+ type="number"
+ placeholder="璇疯緭鍏ュぉ鏁�" />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+
+ <view class="tip-text">
+ <text>绀轰緥锛氬~ 3 琛ㄧず銆屽悎鍚屽師浠剁害瀹氬洖浼犳棩鏈熴�嶈窛浠婁笉瓒� 3 澶╂椂鎻愮ず棰勮銆�</text>
+ </view>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirm-text="淇濆瓨璁剧疆"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </view>
+</template>
+
+<script setup>
+ import { ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import {
+ getProjectConfig,
+ updateProjectConfig,
+ } from "@/api/projectManagement/config";
+
+ const MIN_DAYS = 1;
+ const MAX_DAYS = 365;
+ const DEFAULT_DAYS = 3;
+
+ const formRef = ref();
+ const loading = ref(false);
+ const form = ref({
+ id: undefined,
+ contractReturnReminderDays: DEFAULT_DAYS,
+ });
+
+ const validateDays = (rule, value, callback) => {
+ const days = Number(value);
+ if (value === "" || value === null || value === undefined) {
+ callback(new Error("鎻愬墠鎻愰啋澶╂暟涓嶈兘涓虹┖"));
+ return;
+ }
+ if (!Number.isInteger(days) || days < MIN_DAYS || days > MAX_DAYS) {
+ callback(new Error(`澶╂暟闇�涓� ${MIN_DAYS}~${MAX_DAYS} 鐨勬暣鏁癭));
+ return;
+ }
+ callback();
+ };
+
+ const rules = {
+ contractReturnReminderDays: [
+ { required: true, validator: validateDays, trigger: "blur" },
+ ],
+ };
+
+ const goBack = () => uni.navigateBack();
+
+ const loadConfig = async () => {
+ try {
+ const res = await getProjectConfig();
+ const data = res?.data?.data ?? res?.data ?? res;
+ if (!data) return;
+ form.value.id = data.id;
+ form.value.contractReturnReminderDays =
+ Number(data.contractReturnReminderDays) || DEFAULT_DAYS;
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇閰嶇疆澶辫触", icon: "none" });
+ }
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ loading.value = true;
+ updateProjectConfig({
+ id: form.value.id,
+ contractReturnReminderDays: Number(form.value.contractReturnReminderDays),
+ })
+ .then(() => {
+ uni.showToast({ title: "閰嶇疆鏇存柊鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "閰嶇疆鏇存柊澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(() => {
+ loadConfig();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .project-config {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .tip-text {
+ padding: 12px 4px;
+ font-size: 13px;
+ line-height: 1.6;
+ color: #909399;
+ }
+</style>
diff --git a/src/pages/projectManagement/project/detail.vue b/src/pages/projectManagement/project/detail.vue
new file mode 100644
index 0000000..694d831
--- /dev/null
+++ b/src/pages/projectManagement/project/detail.vue
@@ -0,0 +1,948 @@
+<!--
+ 椤圭洰璇︽儏
+
+ 鍏釜 Tab锛氬熀纭�璧勬枡 / 椤圭洰闃舵 / 璇夎淇℃伅 / 涓爣淇℃伅 / 閿�鍞悎鍚� / 鍏ㄦ祦绋嬫椂闂磋酱銆�
+ 璇夎銆佷腑鏍囥�侀攢鍞悎鍚屾病鏈夈�屾寜椤圭洰鏌ャ�嶇殑涓撶敤鎺ュ彛锛岀鐞嗙涔熸槸鎷夊叏閲忓悗鎸�
+ 椤圭洰 id / 椤圭洰鍚嶇О鍖归厤锛堣 projectDetail.vue 鐨� matchProject锛夛紝杩欓噷鐓у仛銆�
+
+ 銆屽叏娴佺▼鏃堕棿杞淬�嶆槸鍓嶇鑱氬悎锛氶樁娈垫眹鎶� + 璇夎绔嬫 + 鎷涙姇鏍� 鎸夋棩鏈熷�掑簭鎷兼帴锛�
+ 鍚庣娌℃湁 timeline 鎺ュ彛锛涘皢鏉ュ悗绔彁渚涗簡鍙渶鎹㈡暟鎹簮锛孶I 涓嶇敤鍔ㄣ��
+-->
+<template>
+ <view class="project-detail">
+ <PageHeader title="椤圭洰璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading">
+ <view class="detail-card">
+ <view class="hero">
+ <text class="hero-title">{{ info.title || "-" }}</text>
+ <up-tag :text="dictLabel(auditOptions, info.reviewStatus)"
+ :type="dictTagType(auditOptions, info.reviewStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <view class="hero-meta">
+ <text class="hero-meta-item">鍗曟嵁缂栧彿锛歿{ info.no || "-" }}</text>
+ <text class="hero-meta-item">椤圭洰閲戦锛歿{ formatAmount(info.orderAmount) }}</text>
+ </view>
+ </view>
+
+ <view v-if="planNodes.length > 0"
+ class="steps-card">
+ <up-steps :current="activeStep"
+ direction="row"
+ activeColor="#2979ff"
+ inactiveColor="#c0c4cc">
+ <up-steps-item v-for="(node, index) in planNodes"
+ :key="node.id || index"
+ :title="node.name || node.workContent || ''" />
+ </up-steps>
+ </view>
+
+ <view class="tabs-wrap">
+ <up-tabs v-model="activeTab"
+ :list="TABS"
+ :scrollable="true"
+ itemStyle="padding: 0 14px; height: 84rpx;" />
+ </view>
+
+ <!-- 鍩虹璧勬枡 -->
+ <view v-if="activeTab === 0">
+ <u-cell-group title="鍩烘湰淇℃伅"
+ class="detail-section">
+ <u-cell title="鍗曟嵁缂栧彿"
+ :value="info.no || '-'" />
+ <u-cell title="椤圭洰鍚嶇О"
+ :value="info.title || '-'" />
+ <u-cell title="瀹㈡埛鍚嶇О"
+ :value="info.clientName || '-'" />
+ <u-cell title="鐖堕」鐩�"
+ :value="detail.info.parentProjectName || '-'" />
+ <u-cell title="椤圭洰绫诲瀷"
+ :value="projectTypeLabel" />
+ <u-cell title="绔嬮」鏃ユ湡"
+ :value="info.establishTime || '-'" />
+ <u-cell title="椤圭洰鏉ユ簮"
+ :value="info.source || '-'" />
+ <u-cell title="绔嬮」浜�"
+ :value="info.managerName || '-'" />
+ <u-cell title="涓氬姟鍛�"
+ :value="info.salesmanName || '-'" />
+ <u-cell title="閮ㄩ棬"
+ :value="info.departmentName || '-'" />
+ <u-cell title="鍗曟嵁鐘舵��"
+ :value="dictLabel(billOptions, info.status) || '-'" />
+ <u-cell title="瀹℃牳鐘舵��"
+ :value="dictLabel(auditOptions, info.reviewStatus) || '-'" />
+ <u-cell title="璁″垝鐘舵��"
+ :value="dictLabel(stageOptions, info.stage) || '-'" />
+ <u-cell title="棰勮宸ユ湡(澶�)"
+ :value="estimatedDays === null ? '-' : String(estimatedDays)" />
+ <u-cell title="璁″垝寮�濮嬫棩鏈�"
+ :value="info.planStartTime || '-'" />
+ <u-cell title="璁″垝瀹屾垚鏃ユ湡"
+ :value="info.planEndTime || '-'" />
+ <u-cell title="瀹為檯寮�濮嬫棩鏈�"
+ :value="info.actualStartTime || '-'" />
+ <u-cell title="瀹為檯瀹屾垚鏃ユ湡"
+ :value="info.actualEndTime || '-'" />
+ <u-cell title="璁㈠崟鏃ユ湡"
+ :value="info.orderDate || '-'" />
+ <u-cell title="椤圭洰閲戦"
+ :value="formatAmount(info.orderAmount)" />
+ <u-cell title="澶囨敞"
+ :value="info.remark || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="浜у搧淇℃伅"
+ class="detail-section">
+ <view v-if="productList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤浜у搧淇℃伅</text>
+ </view>
+ <view v-for="(row, index) in productList"
+ v-else
+ :key="row.id || index"
+ class="sub-card">
+ <view class="sub-card-header">
+ <text class="sub-card-title">{{ row.productCategory || "浜у搧" }} {{ index + 1 }}</text>
+ <text class="sub-card-amount">{{ formatAmount(row.taxInclusiveTotalPrice) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ row.specificationModel || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍗曚綅</text>
+ <text class="detail-value">{{ row.unit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏁伴噺</text>
+ <text class="detail-value">{{ row.quantity ?? "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绋庣巼(%)</text>
+ <text class="detail-value">{{ row.taxRate ?? "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚◣鍗曚环</text>
+ <text class="detail-value">{{ formatAmount(row.taxInclusiveUnitPrice) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚◣鎬讳环</text>
+ <text class="detail-value">{{ formatAmount(row.taxInclusiveTotalPrice) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓嶅惈绋庢�讳环</text>
+ <text class="detail-value">{{ formatAmount(row.taxExclusiveTotalPrice) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙戠エ绫诲瀷</text>
+ <text class="detail-value">{{ row.invoiceType || "-" }}</text>
+ </view>
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="椤圭洰鍥㈤槦"
+ class="detail-section">
+ <view v-if="teamList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤椤圭洰鎴愬憳</text>
+ </view>
+ <view v-for="(row, index) in teamList"
+ v-else
+ :key="row.id || index"
+ class="sub-card">
+ <view class="sub-card-header">
+ <text class="sub-card-title">{{ row.userName || "-" }}</text>
+ <text class="sub-card-amount">{{ row.userRoleName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">杩涘叆鏃ユ湡</text>
+ <text class="detail-value">{{ row.joinTime || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绂诲紑鏃ユ湡</text>
+ <text class="detail-value">{{ row.departTime || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鑱旂郴鏂瑰紡</text>
+ <text class="detail-value">{{ row.contact || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">澶囨敞</text>
+ <text class="detail-value">{{ row.remark || "-" }}</text>
+ </view>
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="鏀惰揣鍦板潃"
+ class="detail-section">
+ <u-cell title="鏀惰揣浜�"
+ :value="detail.shippingAddress.consignee || '-'" />
+ <u-cell title="鑱旂郴鏂瑰紡"
+ :value="detail.shippingAddress.contract || '-'" />
+ <u-cell title="鍦板潃"
+ :value="detail.shippingAddress.address || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鑱旂郴淇℃伅"
+ class="detail-section">
+ <u-cell title="鑱旂郴浜�"
+ :value="detail.contractInfo.name || '-'" />
+ <u-cell title="鎬у埆"
+ :value="detail.contractInfo.sex || '-'" />
+ <u-cell title="鐢熸棩"
+ :value="detail.contractInfo.birthday || '-'" />
+ <u-cell title="閮ㄩ棬"
+ :value="detail.contractInfo.department || '-'" />
+ <u-cell title="鑱屽姟"
+ :value="detail.contractInfo.job || '-'" />
+ <u-cell title="鎵嬫満鍙�"
+ :value="detail.contractInfo.phoneNumber || '-'" />
+ <u-cell title="閭"
+ :value="detail.contractInfo.email || '-'" />
+ <u-cell title="QQ"
+ :value="detail.contractInfo.qq || '-'" />
+ <u-cell title="寰俊鍙�"
+ :value="detail.contractInfo.wx || '-'" />
+ <u-cell title="浼佷笟寰俊"
+ :value="detail.contractInfo.workWechat || '-'" />
+ <u-cell title="鍦板潃"
+ :value="detail.contractInfo.address || '-'" />
+ <u-cell title="澶囨敞"
+ :value="detail.contractInfo.remark || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="闄勪欢"
+ class="detail-section">
+ <view v-if="attachments.length === 0"
+ class="empty-text">
+ <text>鏆傛棤闄勪欢</text>
+ </view>
+ <view v-for="(file, index) in attachments"
+ v-else
+ :key="file.id || index"
+ class="file-item"
+ @click="previewFile(file)">
+ <up-icon name="attach"
+ size="16"
+ color="#2979ff"></up-icon>
+ <text class="file-name">{{ file.name || `闄勪欢${index + 1}` }}</text>
+ <up-icon name="arrow-right"
+ size="14"
+ color="#c0c4cc"></up-icon>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <!-- 椤圭洰闃舵 -->
+ <view v-if="activeTab === 1">
+ <u-cell-group title="璁″垝鑺傜偣锛堥」鐩被鍨嬫ā鏉匡級"
+ class="detail-section">
+ <view v-if="planNodes.length === 0"
+ class="empty-text">
+ <text>璇ラ」鐩被鍨嬫湭閰嶇疆璁″垝鑺傜偣</text>
+ </view>
+ <view v-for="(node, index) in planNodes"
+ v-else
+ :key="node.id || index"
+ class="sub-card">
+ <view class="sub-card-header">
+ <text class="sub-card-title">{{ node.name || node.workContent || "-" }}</text>
+ <text class="sub-card-amount">宸ユ湡 {{ node.estimatedDuration ?? "-" }} 澶�</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璐熻矗浜�</text>
+ <text class="detail-value">{{ node.leaderName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浣滀笟鍐呭</text>
+ <text class="detail-value">{{ node.workContent || "-" }}</text>
+ </view>
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="闃舵姹囨姤璁板綍"
+ class="detail-section">
+ <view v-if="stageList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤闃舵姹囨姤</text>
+ </view>
+ <view v-for="(row, index) in stageList"
+ v-else
+ :key="row.id || index"
+ class="sub-card">
+ <view class="sub-card-header">
+ <text class="sub-card-title">{{ row.stageName || "-" }}</text>
+ <text class="sub-card-amount">{{ row.progress ?? 0 }}%</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎻忚堪</text>
+ <text class="detail-value">{{ row.description || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹為檯璐熻矗浜�</text>
+ <text class="detail-value">{{ row.actualLeaderName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁″垝璧锋</text>
+ <text class="detail-value">{{ row.planStartTime || "-" }} ~ {{ row.planEndTime || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹為檯璧锋</text>
+ <text class="detail-value">{{ row.actualStartTime || "-" }} ~ {{ row.actualEndTime || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">棰勮宸ユ湡</text>
+ <text class="detail-value">{{ row.estimatedDuration ?? "-" }} 澶�</text>
+ </view>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <!-- 璇夎淇℃伅 -->
+ <view v-if="activeTab === 2">
+ <view v-if="lawsuitList.length === 0"
+ class="no-data">
+ <text>鏆傛棤璇夎淇℃伅</text>
+ </view>
+ <view v-else
+ class="list-wrap">
+ <view v-for="row in lawsuitList"
+ :key="row.id"
+ class="list-card">
+ <view class="list-card-header">
+ <text class="list-card-title">{{ row.caseNo || "-" }}</text>
+ <up-tag :text="row.lawsuitStatus || '-'"
+ :type="lawsuitTagType(row.lawsuitStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堝悓缂栧彿</text>
+ <text class="detail-value">{{ row.contractNo || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绔嬫鏃堕棿</text>
+ <text class="detail-value">{{ formatDate(row.filingDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">寮�搴椂闂�</text>
+ <text class="detail-value">{{ formatDate(row.hearingDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏍囩殑閲戦</text>
+ <text class="detail-value">{{ formatAmount(row.claimAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵垮姙寰嬪笀</text>
+ <text class="detail-value">{{ row.lawyer || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍒ゅ喅鎽樿</text>
+ <text class="detail-value">{{ row.judgmentSummary || "-" }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 涓爣淇℃伅 -->
+ <view v-if="activeTab === 3">
+ <view v-if="biddingList.length === 0"
+ class="no-data">
+ <text>鏆傛棤鎷涙姇鏍囦俊鎭�</text>
+ </view>
+ <view v-else
+ class="list-wrap">
+ <view v-for="row in biddingList"
+ :key="row.id"
+ class="list-card">
+ <view class="list-card-header">
+ <text class="list-card-title">{{ row.bidDocName || "-" }}</text>
+ <up-tag :text="row.bidResult || '-'"
+ :type="biddingTagType(row.bidResult)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎷涙爣鍗曚綅</text>
+ <text class="detail-value">{{ row.tenderUnit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曟爣鍗曚綅</text>
+ <text class="detail-value">{{ row.bidUnit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎷涙爣鏃ユ湡</text>
+ <text class="detail-value">{{ formatDate(row.tenderDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曟爣鏃ユ湡</text>
+ <text class="detail-value">{{ formatDate(row.bidDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏍囦功娆�</text>
+ <text class="detail-value">{{ formatAmount(row.bidDocFee) }}锛坽{ row.bidDocFeeStatus || "-" }}锛�</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曟爣淇濊瘉閲�</text>
+ <text class="detail-value">{{ formatAmount(row.bidDeposit) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 閿�鍞悎鍚� -->
+ <view v-if="activeTab === 4">
+ <view v-if="contractList.length === 0"
+ class="no-data">
+ <text>鏆傛棤閿�鍞悎鍚�</text>
+ </view>
+ <view v-else
+ class="list-wrap">
+ <view v-for="row in contractList"
+ :key="row.id"
+ class="list-card">
+ <view class="list-card-header">
+ <text class="list-card-title">{{ row.salesContractNo || "-" }}</text>
+ <text class="list-card-meta">{{ formatDate(row.executionDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹㈡埛鍚嶇О</text>
+ <text class="detail-value">{{ row.customerName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">涓氬姟鍛�</text>
+ <text class="detail-value">{{ row.salesman || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堝悓閲戦</text>
+ <text class="detail-value">{{ formatAmount(row.contractAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹為檯鍚堝悓閲戦</text>
+ <text class="detail-value">{{ formatAmount(row.netContractAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜や粯鏃ユ湡</text>
+ <text class="detail-value">{{ formatDate(row.deliveryDate) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">褰曞叆浜�</text>
+ <text class="detail-value">{{ row.entryPersonName || "-" }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍏ㄦ祦绋嬫椂闂磋酱锛堝墠绔仛鍚堬紝闈炲悗绔帴鍙o級 -->
+ <view v-if="activeTab === 5">
+ <view v-if="timeline.length === 0"
+ class="no-data">
+ <text>鏆傛棤娴佺▼璁板綍</text>
+ </view>
+ <view v-else
+ class="timeline-wrap">
+ <up-steps :current="-1"
+ direction="column"
+ activeColor="#2979ff"
+ inactiveColor="#2979ff">
+ <up-steps-item v-for="(item, index) in timeline"
+ :key="index"
+ :title="item.title"
+ :desc="item.desc" />
+ </up-steps>
+ </view>
+ </view>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+
+ <view v-if="!loading"
+ class="footer-btns">
+ <up-button class="footer-btn"
+ text="杩斿洖"
+ @click="goBack" />
+ <up-button class="footer-btn"
+ type="primary"
+ text="杩涘害姹囨姤"
+ @click="goProgress" />
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad, onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { useDict } from "@/utils/dict";
+ import { unwrapList } from "@/utils/list";
+ import { openFile } from "@/utils/file";
+ import { parseDate } from "@/utils/warning";
+ import { getProject } from "@/api/projectManagement/project";
+ import { listPlan } from "@/api/projectManagement/projectType";
+ import { lawsuitListPage } from "@/api/salesManagement/lawsuitManagement";
+ import { biddingListPage } from "@/api/salesManagement/biddingManagement";
+ import { ledgerListPage } from "@/api/salesManagement/salesLedger";
+ import { toDetail } from "./_utils/mapper";
+ import { dictLabel, dictTagType, toPickerOptions } from "./_utils/dict";
+ import {
+ fetchPlanNodeList,
+ fetchProjectStages,
+ stageNameFromNodes,
+ } from "./_utils/stage";
+
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+ const TABS = [
+ { name: "鍩虹璧勬枡" },
+ { name: "椤圭洰闃舵" },
+ { name: "璇夎淇℃伅" },
+ { name: "涓爣淇℃伅" },
+ { name: "閿�鍞悎鍚�" },
+ { name: "鍏ㄦ祦绋嬫椂闂磋酱" },
+ ];
+
+ const LAWSUIT_TAG_TYPE = {
+ 绔嬫涓�: "warning",
+ 瀹$悊涓�: "primary",
+ 宸插垽鍐�: "success",
+ 宸叉挙璇�: "info",
+ 宸插拰瑙�: "success",
+ };
+
+ const BIDDING_TAG_TYPE = {
+ 搴熸爣: "info",
+ 娴佹爣: "info",
+ 鏈腑鏍�: "warning",
+ 涓爣寰呭嚭閫氱煡涔�: "primary",
+ 宸插嚭涓爣閫氱煡涔�: "success",
+ 宸茬鍚堝悓: "success",
+ };
+
+ const { bill_status, project_management, plan_status } = useDict(
+ "bill_status",
+ "project_management",
+ "plan_status"
+ );
+
+ const billOptions = computed(() => toPickerOptions(bill_status.value));
+ const auditOptions = computed(() => toPickerOptions(project_management.value));
+ const stageOptions = computed(() => toPickerOptions(plan_status.value));
+
+ const loading = ref(true);
+ const projectId = ref("");
+ const activeTab = ref(0);
+ const detail = ref({});
+ const planNodes = ref([]);
+ const stageList = ref([]);
+ const projectTypeName = ref("");
+ const lawsuitList = ref([]);
+ const biddingList = ref([]);
+ const contractList = ref([]);
+
+ const info = computed(() => detail.value.info || {});
+ const productList = computed(() => detail.value.productList || []);
+ const teamList = computed(() => detail.value.teamList || []);
+ const attachments = computed(() => detail.value.attachments || []);
+
+ const projectTypeLabel = computed(
+ () => projectTypeName.value || String(info.value.projectManagementPlanId ?? "-")
+ );
+
+ const estimatedDays = computed(() => {
+ const raw = Number(info.value.estimatedDays);
+ if (Number.isFinite(raw) && raw > 0) return raw;
+ const start = parseDate(info.value.planStartTime);
+ const end = parseDate(info.value.planEndTime);
+ if (!start || !end || end < start) return null;
+ return Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
+ });
+
+ // 璁″垝鑺傜偣宸叉寜 sort 鎺掑簭锛岃鍒掔姸鎬佹寜搴忓彿鎴栬妭鐐� id 鍛戒腑
+ const activeStep = computed(() => {
+ const nodes = planNodes.value;
+ const stage = info.value.stage;
+ if (!nodes.length) return 0;
+ const byId = nodes.findIndex(n => String(n.id) === String(stage));
+ if (byId >= 0) return byId;
+ const byLabel = nodes.findIndex(
+ n => String(n.name || n.workContent) === String(stage)
+ );
+ if (byLabel >= 0) return byLabel;
+ const index = Number(stage);
+ if (Number.isFinite(index) && index > 0) return Math.min(index - 1, nodes.length - 1);
+ return 0;
+ });
+
+ // 鍏ㄦ祦绋嬫椂闂磋酱锛氶樁娈� + 璇夎 + 鎷涙姇鏍� 鎸夋棩鏈熷�掑簭鑱氬悎锛堝墠绔嫾鎺ワ紝鍚庣鏃� timeline 鎺ュ彛锛�
+ const timeline = computed(() => {
+ const events = [];
+ stageList.value.forEach(row => {
+ events.push({
+ date: row.planStartTime || row.actualStartTime || "",
+ title: `銆愰」鐩樁娈点��${row.stageName || "闃舵姹囨姤"}`,
+ desc: row.description || "",
+ });
+ });
+ lawsuitList.value.forEach(row => {
+ events.push({
+ date: row.filingDate || "",
+ title: `銆愯瘔璁笺��${row.caseNo || ""} ${row.lawsuitStatus || ""}`.trim(),
+ desc: row.judgmentSummary || row.progressRemark || "",
+ });
+ });
+ biddingList.value.forEach(row => {
+ events.push({
+ date: row.bidDate || "",
+ title: `銆愭嫑鎶曟爣銆�${row.bidDocName || ""} ${row.bidResult || ""}`.trim(),
+ desc: row.tenderUnit ? `鎷涙爣鍗曚綅锛�${row.tenderUnit}` : "",
+ });
+ });
+ return events
+ .filter(item => item.title)
+ .sort((a, b) => String(b.date).localeCompare(String(a.date)));
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.navigateTo({
+ url: `/pages/projectManagement/project/edit?id=${projectId.value}`,
+ });
+ };
+
+ const goProgress = () => {
+ uni.navigateTo({
+ url: `/pages/projectManagement/project/progress?id=${projectId.value}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const formatDate = value => (value ? String(value).substring(0, 10) : "-");
+
+ const lawsuitTagType = status => LAWSUIT_TAG_TYPE[status] || "info";
+ const biddingTagType = status => BIDDING_TAG_TYPE[status] || "info";
+
+ const previewFile = file => openFile(file?.url || file?.previewURL);
+
+ const loadProjectTypeName = async () => {
+ const planId = info.value.projectManagementPlanId;
+ if (planId === undefined || planId === null || planId === "") return;
+ try {
+ const res = await listPlan({ current: 1, size: 999 });
+ const matched = unwrapList(res).find(
+ item => String(item.id) === String(planId)
+ );
+ projectTypeName.value = matched?.name || "";
+ } catch (error) {
+ projectTypeName.value = "";
+ }
+ };
+
+ // 璇夎 / 鎷涙姇鏍囨病鏈夋寜椤圭洰鏌ョ殑鎺ュ彛锛屾媺鍏ㄩ噺鍚庢寜椤圭洰 id 鎴栧悕绉板尮閰嶏紙瀵归綈绠$悊绔� matchProject锛�
+ const matchProject = (record, id, title) => {
+ if (!record) return false;
+ if (
+ record.projectId !== undefined &&
+ record.projectId !== null &&
+ record.projectId !== ""
+ ) {
+ if (String(record.projectId) === String(id)) return true;
+ }
+ const name = String(record.projectName || "").trim();
+ const target = String(title || "").trim();
+ if (name && target) {
+ return name === target || name.includes(target) || target.includes(name);
+ }
+ return false;
+ };
+
+ const loadRelated = async (id, title) => {
+ if (!id) return;
+
+ try {
+ const res = await lawsuitListPage({ projectName: title, current: -1, size: -1 });
+ let matched = unwrapList(res).filter(r => matchProject(r, id, title));
+ if (matched.length === 0) {
+ const all = await lawsuitListPage({ current: -1, size: -1 });
+ matched = unwrapList(all).filter(r => matchProject(r, id, title));
+ }
+ lawsuitList.value = matched;
+ } catch (error) {
+ lawsuitList.value = [];
+ }
+
+ try {
+ const res = await biddingListPage({ projectName: title, current: -1, size: -1 });
+ let matched = unwrapList(res).filter(r => matchProject(r, id, title));
+ if (matched.length === 0) {
+ const all = await biddingListPage({ current: -1, size: -1 });
+ matched = unwrapList(all).filter(r => matchProject(r, id, title));
+ }
+ biddingList.value = matched;
+ } catch (error) {
+ biddingList.value = [];
+ }
+
+ try {
+ const res = await ledgerListPage({ projectId: id, current: -1, size: -1 });
+ contractList.value = unwrapList(res).sort((a, b) =>
+ String(b.executionDate || "").localeCompare(String(a.executionDate || ""))
+ );
+ } catch (error) {
+ contractList.value = [];
+ }
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const res = await getProject(projectId.value);
+ const raw = res?.data?.data ?? res?.data ?? res;
+ detail.value = toDetail(raw || {});
+
+ const id = info.value.id || projectId.value;
+ const title = info.value.title || "";
+
+ const [nodes, stages] = await Promise.all([
+ fetchPlanNodeList(info.value.projectManagementPlanId),
+ fetchProjectStages(id),
+ ]);
+ planNodes.value = nodes;
+ stageList.value = stages.map(row => ({
+ ...row,
+ stageName: stageNameFromNodes(nodes, row.projectManagementPlanNodeId),
+ }));
+
+ await Promise.all([loadProjectTypeName(), loadRelated(id, title)]);
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇椤圭洰璇︽儏澶辫触", icon: "none" });
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ projectId.value = options?.id || "";
+ if (!projectId.value) loading.value = false;
+ });
+
+ // 浠庛�岃繘搴︽眹鎶ャ�嶈繑鍥炴椂闃舵鍒楄〃浼氬彉锛岀敤 onShow 閲嶆媺锛坥nShow 鍦� onLoad 鍚庝篃浼氳Е鍙戯級
+ onShow(() => {
+ if (projectId.value) loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .project-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 90px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .hero-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 12px 18px 0;
+ }
+
+ .hero-meta-item {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .steps-card {
+ margin: 12px;
+ padding: 16px 8px;
+ border-radius: 12px;
+ background: #ffffff;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .tabs-wrap {
+ margin: 12px 12px 0;
+ border-radius: 12px;
+ overflow: hidden;
+ background: #ffffff;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .detail-section {
+ margin: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .sub-card {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .sub-card:last-child {
+ border-bottom: none;
+ }
+
+ .sub-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ }
+
+ .sub-card-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .sub-card-amount {
+ font-size: 13px;
+ color: #2979ff;
+ }
+
+ .list-wrap {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .list-card {
+ background: #ffffff;
+ border-radius: 12px;
+ padding: 14px 16px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .list-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ }
+
+ .list-card-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .list-card-meta {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .timeline-wrap {
+ margin: 12px;
+ padding: 20px 16px;
+ border-radius: 12px;
+ background: #ffffff;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .file-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .file-item:last-child {
+ border-bottom: none;
+ }
+
+ .file-name {
+ flex: 1;
+ font-size: 14px;
+ color: #333;
+ word-break: break-all;
+ }
+
+ .empty-text {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .footer-btns {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ gap: 12px;
+ padding: 12px 16px;
+ padding-bottom: calc(12px + env(safe-area-inset-bottom));
+ background: #ffffff;
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
+ z-index: 1000;
+ }
+
+ .footer-btn {
+ flex: 1;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/projectManagement/project/edit.vue b/src/pages/projectManagement/project/edit.vue
new file mode 100644
index 0000000..54ce91e
--- /dev/null
+++ b/src/pages/projectManagement/project/edit.vue
@@ -0,0 +1,574 @@
+<!--
+ 椤圭洰鏂板 / 缂栬緫
+
+ 琛ㄥ崟鍒嗗尯瀵归綈绠$悊绔� formDia.vue锛氬熀纭�璧勬枡 / 浜у搧淇℃伅 / 椤圭洰鍥㈤槦 / 鏀惰揣鍦板潃 / 鑱旂郴淇℃伅 / 闄勪欢銆�
+ 绠$悊绔繕鏈変竴涓�岄」鐩樁娈点�嶅瓙琛紝鏁村潡鏄敞閲婃帀鐨勪笖涓嶅湪 /save 鐨勫叆鍙傞噷锛�
+ 鎵�浠ヨ繖閲屼篃涓嶅仛鍙紪杈戦樁娈佃〃锛堝仛浜嗕細闈欓粯涓㈡暟鎹級锛涢樁娈甸�氳繃璇︽儏椤垫煡鐪嬨�侀�氳繃銆岃繘搴︽眹鎶ャ�嶅啓鍏ャ��
+
+ 鍩虹璧勬枡閲屻�岄璁″伐鏈熴�嶅湪绠$悊绔彲濉絾涓嶅湪鎻愪氦缁撴瀯閲岋紙鍚庣鎸夎鍒掕捣姝㈡棩鏈熸淳鐢燂級锛�
+ 鎵�浠ヨ繖閲屽仛鎴愬彧璇绘淳鐢熷�硷紝閬垮厤鐢ㄦ埛濉簡涓嶇敓鏁堛��
+-->
+<template>
+ <view class="project-edit">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="鍩虹璧勬枡"
+ class="form-section">
+ <up-form-item v-if="projectId"
+ label="鍗曟嵁缂栧彿">
+ <up-input :model-value="form.billNo || '绯荤粺鐢熸垚'"
+ disabled />
+ </up-form-item>
+ <up-form-item label="椤圭洰鍚嶇О"
+ prop="projectName"
+ required>
+ <up-input v-model="form.projectName"
+ placeholder="璇疯緭鍏ラ」鐩悕绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="瀹㈡埛鍚嶇О"
+ prop="customerName">
+ <up-input v-model="form.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绔嬮」鏃ユ湡"
+ prop="setupDate">
+ <FormPicker v-model="form.setupDate"
+ type="date"
+ placeholder="璇烽�夋嫨绔嬮」鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="椤圭洰鏉ユ簮"
+ prop="projectSource">
+ <up-input v-model="form.projectSource"
+ placeholder="璇疯緭鍏ラ」鐩潵婧�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绔嬮」浜�"
+ prop="creatorName">
+ <up-input v-model="form.creatorName"
+ placeholder="璇疯緭鍏ョ珛椤逛汉"
+ clearable />
+ </up-form-item>
+ <up-form-item label="椤圭洰绫诲瀷"
+ prop="projectManagementPlanId"
+ required>
+ <FormPicker v-model="form.projectManagementPlanId"
+ type="select"
+ title="閫夋嫨椤圭洰绫诲瀷"
+ :options="projectTypeOptions"
+ placeholder="璇烽�夋嫨椤圭洰绫诲瀷" />
+ </up-form-item>
+ <up-form-item label="璁″垝寮�濮嬫棩鏈�"
+ prop="planStartDate">
+ <FormPicker v-model="form.planStartDate"
+ type="date"
+ placeholder="璇烽�夋嫨璁″垝寮�濮嬫棩鏈�" />
+ </up-form-item>
+ <up-form-item label="璁″垝瀹屾垚鏃ユ湡"
+ prop="planEndDate">
+ <FormPicker v-model="form.planEndDate"
+ type="date"
+ placeholder="璇烽�夋嫨璁″垝瀹屾垚鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="棰勮宸ユ湡(澶�)">
+ <up-input :model-value="estimatedDays === null ? '鎸夎鍒掓棩鏈熻嚜鍔ㄨ绠�' : String(estimatedDays)"
+ disabled />
+ </up-form-item>
+ <up-form-item label="椤圭洰閲戦"
+ prop="projectAmount">
+ <up-input v-model="form.projectAmount"
+ type="number"
+ placeholder="璇疯緭鍏ラ」鐩噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="瀹℃牳鐘舵��"
+ prop="auditStatus">
+ <FormPicker v-model="form.auditStatus"
+ type="select"
+ title="閫夋嫨瀹℃牳鐘舵��"
+ :options="auditOptions"
+ placeholder="璇烽�夋嫨瀹℃牳鐘舵��" />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ :maxlength="100"
+ count
+ auto-height />
+ </up-form-item>
+ </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="productList.length === 0"
+ class="empty-text">
+ <text>璇疯嚦灏戞坊鍔犱竴鏉′骇鍝佷俊鎭�</text>
+ </view>
+ <view v-else
+ class="row-list">
+ <ProductRow v-for="(row, index) in productList"
+ :key="row.uid"
+ :row="row"
+ :index="index"
+ :tax-rate-options="taxRateOptions"
+ @remove="removeProduct(index)" />
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="椤圭洰鍥㈤槦"
+ class="form-section">
+ <view class="section-tools">
+ <up-button type="primary"
+ size="small"
+ text="娣诲姞鎴愬憳"
+ @click="addTeamRow" />
+ </view>
+ <view v-if="teamList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤椤圭洰鎴愬憳</text>
+ </view>
+ <view v-else
+ class="row-list">
+ <TeamRow v-for="(row, index) in teamList"
+ :key="index"
+ :row="row"
+ :index="index"
+ :user-options="userOptions"
+ :role-options="roleOptions"
+ @remove="removeTeamRow(index)" />
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="鏀惰揣鍦板潃"
+ class="form-section">
+ <view v-if="addressList.length === 0"
+ class="section-tools">
+ <up-button type="primary"
+ size="small"
+ text="娣诲姞鍦板潃"
+ @click="addAddressRow" />
+ </view>
+ <view v-else
+ class="row-list">
+ <AddressRow v-for="(row, index) in addressList"
+ :key="index"
+ :row="row" />
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="鑱旂郴淇℃伅"
+ class="form-section">
+ <up-form-item label="鑱旂郴浜哄鍚�"
+ prop="contactName">
+ <up-input v-model="form.contactName"
+ placeholder="璇疯緭鍏ヨ仈绯讳汉濮撳悕"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎬у埆"
+ prop="contactGender">
+ <FormPicker v-model="form.contactGender"
+ type="select"
+ title="閫夋嫨鎬у埆"
+ :options="GENDER_OPTIONS"
+ placeholder="璇烽�夋嫨鎬у埆" />
+ </up-form-item>
+ <up-form-item label="鐢熸棩"
+ prop="contactBirthday">
+ <FormPicker v-model="form.contactBirthday"
+ type="date"
+ placeholder="璇烽�夋嫨鐢熸棩" />
+ </up-form-item>
+ <up-form-item label="閮ㄩ棬"
+ prop="contactDept">
+ <up-input v-model="form.contactDept"
+ placeholder="璇疯緭鍏ラ儴闂�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鑱屽姟"
+ prop="contactJob">
+ <up-input v-model="form.contactJob"
+ placeholder="璇疯緭鍏ヨ亴鍔�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎵嬫満鍙风爜"
+ prop="contactMobile">
+ <up-input v-model="form.contactMobile"
+ type="number"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="閭"
+ prop="contactEmail">
+ <up-input v-model="form.contactEmail"
+ placeholder="璇疯緭鍏ラ偖绠�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="QQ"
+ prop="contactQq">
+ <up-input v-model="form.contactQq"
+ placeholder="璇疯緭鍏� QQ"
+ clearable />
+ </up-form-item>
+ <up-form-item label="寰俊鍙风爜"
+ prop="contactWechat">
+ <up-input v-model="form.contactWechat"
+ placeholder="璇疯緭鍏ュ井淇″彿鐮�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="浼佷笟寰俊"
+ prop="contactWorkWechat">
+ <up-input v-model="form.contactWorkWechat"
+ placeholder="璇疯緭鍏ヤ紒涓氬井淇�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍦板潃"
+ prop="contactAddress">
+ <up-input v-model="form.contactAddress"
+ placeholder="璇疯緭鍏ュ湴鍧�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鑱旂郴淇℃伅澶囨敞"
+ prop="contactRemark">
+ <up-textarea v-model="form.contactRemark"
+ placeholder="璇疯緭鍏ュ娉�"
+ :maxlength="200"
+ count
+ auto-height />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="闄勪欢"
+ class="form-section">
+ <view class="upload-block">
+ <CommonUpload v-model="attachmentFiles"
+ accept="all" />
+ </view>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </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 FormPicker from "@/components/FormPicker.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
+ import ProductRow from "./_components/ProductRow.vue";
+ import TeamRow from "./_components/TeamRow.vue";
+ import AddressRow from "./_components/AddressRow.vue";
+ import { useDict } from "@/utils/dict";
+ import { parseDate } from "@/utils/warning";
+ import { unwrapList } from "@/utils/list";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import { findRoleListPage } from "@/api/projectManagement/role";
+ import { listPlan } from "@/api/projectManagement/projectType";
+ import { getProject, saveProject } from "@/api/projectManagement/project";
+ import {
+ GENDER_OPTIONS,
+ createEmptyProduct,
+ toForm,
+ toPayload,
+ } from "./_utils/mapper";
+ import { toPickerOptions } from "./_utils/dict";
+
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+ const { project_management, tax_rate } = useDict(
+ "project_management",
+ "tax_rate"
+ );
+ const auditOptions = computed(() => toPickerOptions(project_management.value));
+ const taxRateOptions = computed(() =>
+ (tax_rate.value || []).map(item => ({ label: item.label, value: item.value }))
+ );
+
+ const formRef = ref();
+ const loading = ref(false);
+ const projectId = ref("");
+ const projectTypeOptions = ref([]);
+ const userOptions = ref([]);
+ const roleOptions = ref([]);
+
+ const form = ref({
+ id: undefined,
+ billNo: "",
+ projectName: "",
+ clientId: "",
+ customerName: "",
+ parentProjectId: "",
+ parentProjectName: "",
+ projectManagementPlanId: "",
+ projectSource: "",
+ managerId: "",
+ creatorName: "",
+ salesmanId: "",
+ salesmanName: "",
+ departmentId: "",
+ departmentName: "",
+ setupDate: "",
+ planStartDate: "",
+ planEndDate: "",
+ actualStartDate: "",
+ actualEndDate: "",
+ orderDate: "",
+ projectAmount: "",
+ billStatus: "",
+ auditStatus: "",
+ projectStage: "",
+ remark: "",
+ contactName: "",
+ contactGender: "",
+ contactBirthday: "",
+ contactDept: "",
+ contactJob: "",
+ contactMobile: "",
+ contactEmail: "",
+ contactQq: "",
+ contactWechat: "",
+ contactWorkWechat: "",
+ contactAddress: "",
+ contactRemark: "",
+ });
+
+ const teamList = ref([]);
+ const addressList = ref([]);
+ const productList = ref([]);
+ const attachmentFiles = ref([]);
+
+ const rules = {
+ projectName: [{ required: true, message: "璇疯緭鍏ラ」鐩悕绉�", trigger: "blur" }],
+ projectManagementPlanId: [
+ { required: true, message: "璇烽�夋嫨椤圭洰绫诲瀷", trigger: "change" },
+ ],
+ };
+
+ const pageTitle = computed(() => (projectId.value ? "缂栬緫椤圭洰" : "鏂板椤圭洰"));
+
+ // 鍚庣鎸夎鍒掕捣姝㈡棩鏈熸淳鐢� estimatedDays锛岃繖閲屽彧鍋氬睍绀猴紝涓嶅弬涓庢彁浜�
+ const estimatedDays = computed(() => {
+ const start = parseDate(form.value.planStartDate);
+ const end = parseDate(form.value.planEndDate);
+ if (!start || !end || end < start) return null;
+ return Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
+ });
+
+ const goBack = () => uni.navigateBack();
+
+ const addProduct = () => productList.value.push(createEmptyProduct());
+
+ const removeProduct = index => {
+ productList.value.splice(index, 1);
+ };
+
+ const addTeamRow = () =>
+ teamList.value.push({
+ memberId: "",
+ memberName: "",
+ roleId: "",
+ roleName: "",
+ enterDate: "",
+ leaveDate: "",
+ phone: "",
+ remark: "",
+ });
+
+ const removeTeamRow = index => {
+ teamList.value.splice(index, 1);
+ };
+
+ const addAddressRow = () =>
+ addressList.value.push({ receiver: "", phone: "", address: "" });
+
+ const loadProjectTypes = () => {
+ listPlan({ current: 1, size: 999 })
+ .then(res => {
+ projectTypeOptions.value = unwrapList(res).map(item => ({
+ name: item.name || item.planName || "",
+ value: item.id,
+ }));
+ })
+ .catch(() => {
+ projectTypeOptions.value = [];
+ });
+ };
+
+ const loadUsers = () => {
+ userListNoPageByTenantId()
+ .then(res => {
+ const rows = unwrapList(res);
+ userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({
+ name: item.nickName || item.userName || "",
+ value: item.userId ?? item.id,
+ }));
+ })
+ .catch(() => {
+ userOptions.value = [];
+ });
+ };
+
+ const loadRoles = () => {
+ findRoleListPage({ pageNum: 1, pageSize: 999 })
+ .then(res => {
+ roleOptions.value = unwrapList(res).map(item => ({
+ name: item.roleName || item.name || "",
+ value: item.id,
+ }));
+ })
+ .catch(() => {
+ roleOptions.value = [];
+ });
+ };
+
+ const loadDetail = async () => {
+ try {
+ const res = await getProject(projectId.value);
+ const detail = res?.data?.data ?? res?.data ?? res;
+ const mapped = toForm(detail || {});
+ form.value = { ...form.value, ...mapped.form };
+ teamList.value = mapped.teamList;
+ addressList.value = mapped.addressList;
+ productList.value = mapped.productList;
+ attachmentFiles.value = mapped.attachments;
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇椤圭洰璇︽儏澶辫触", icon: "none" });
+ }
+ };
+
+ const validateProducts = () => {
+ if (productList.value.length === 0) {
+ uni.showToast({ title: "璇锋坊鍔犱骇鍝佷俊鎭�", icon: "none" });
+ return false;
+ }
+ const invalid = productList.value.some(row => !row.productCategoryId);
+ if (invalid) {
+ uni.showToast({ title: "璇蜂负姣忔潯浜у搧閫夋嫨浜у搧澶х被", icon: "none" });
+ return false;
+ }
+ return true;
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+ if (!validateProducts()) return;
+
+ // CommonUpload 瀛樼殑鏄笂浼犳帴鍙h繑鍥炵殑瀵硅薄锛屾彁浜ゅ彧瑕� id
+ const attachmentIds = attachmentFiles.value
+ .map(item => item?.id ?? item?.attachmentId)
+ .filter(Boolean);
+
+ loading.value = true;
+ saveProject(
+ toPayload({
+ form: form.value,
+ teamList: teamList.value,
+ addressList: addressList.value,
+ productList: productList.value,
+ attachmentIds,
+ })
+ )
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(async options => {
+ projectId.value = options?.id || "";
+ if (projectId.value) form.value.id = projectId.value;
+
+ loadProjectTypes();
+ loadUsers();
+ loadRoles();
+
+ if (projectId.value) {
+ await loadDetail();
+ } else {
+ // 鏂板鏃跺厛閾轰竴鏉$┖琛岋紝鍑忓皯涓�娆$偣鍑伙紱浜у搧蹇呴』鑷冲皯涓�鏉★紝鍚庣浼氭牎楠�
+ addProduct();
+ addAddressRow();
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .project-edit {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .section-tools {
+ display: flex;
+ justify-content: flex-end;
+ padding: 12px 12px 0;
+ }
+
+ .row-list {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .empty-text {
+ padding: 16px 12px;
+ color: #999;
+ font-size: 14px;
+ }
+
+ .upload-block {
+ padding: 12px;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/projectManagement/project/index.vue b/src/pages/projectManagement/project/index.vue
new file mode 100644
index 0000000..981fc18
--- /dev/null
+++ b/src/pages/projectManagement/project/index.vue
@@ -0,0 +1,357 @@
+<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="query.projectNameOrCode"
+ placeholder="璇疯緭鍏ラ」鐩悕绉�/缂栧彿"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="projectList.length > 0"
+ class="ledger-list">
+ <view v-for="item in projectList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="grid"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.billNo || "-" }}</text>
+ </view>
+ <up-tag :text="dictLabel(auditOptions, item.auditStatus)"
+ :type="dictTagType(auditOptions, item.auditStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">椤圭洰鍚嶇О</text>
+ <text class="detail-value highlight">{{ item.projectName || "-" }}</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.setupDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">椤圭洰鏉ユ簮</text>
+ <text class="detail-value">{{ item.projectSource || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">椤圭洰鍒嗙被</text>
+ <text class="detail-value">{{ item.projectClassification || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璁″垝鐘舵��</text>
+ <text class="detail-value">{{ dictLabel(stageOptions, item.projectStage) || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">椤圭洰閲戦</text>
+ <text class="detail-value">{{ formatAmount(item.projectAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閿�鍞悎鍚屾�绘暟</text>
+ <text class="detail-value">{{ item.salesContractCount ?? 0 }}</text>
+ </view>
+ </view>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click="goDetail(item)">璇︽儏</up-button>
+ <up-button class="action-btn"
+ size="small"
+ @click="goEdit(item)">缂栬緫</up-button>
+ <up-button class="action-btn"
+ size="small"
+ @click="goProgress(item)">杩涘害姹囨姤</up-button>
+ <up-button class="action-btn"
+ size="small"
+ @click="goArchive(item)">鏂囨。褰掓。</up-button>
+ <up-button v-if="canSubmit(item)"
+ class="action-btn"
+ size="small"
+ type="success"
+ @click="handleSubmitRow(item)">鎻愪氦</up-button>
+ <up-button v-if="canAudit(item)"
+ class="action-btn"
+ size="small"
+ type="warning"
+ @click="handleAuditRow(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">
+ <text>鏆傛棤椤圭洰鏁版嵁</text>
+ </view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="椤圭洰绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import { useDict } from "@/utils/dict";
+ import { unwrapList } from "@/utils/list";
+ import {
+ auditProject,
+ delProject,
+ listProject,
+ submitProject,
+ } from "@/api/projectManagement/project";
+ import { toListRow } from "./_utils/mapper";
+ import { dictLabel, dictTagType, toPickerOptions } from "./_utils/dict";
+
+ const { bill_status, project_management, plan_status } = useDict(
+ "bill_status",
+ "project_management",
+ "plan_status"
+ );
+
+ const billOptions = computed(() => toPickerOptions(bill_status.value));
+ const auditOptions = computed(() => toPickerOptions(project_management.value));
+ const stageOptions = computed(() => toPickerOptions(plan_status.value));
+
+ const projectList = ref([]);
+ const showFilter = ref(false);
+
+ // 鍚庣娌℃湁鍗曟嵁鐘舵�侊紙billStatus锛夌殑鏌ヨ鍙傛暟锛岀鐞嗙涔熸槸鎷夊洖鍒楄〃鍚庢湰鍦拌繃婊�
+ const query = reactive({
+ projectNameOrCode: "",
+ customerName: "",
+ salesperson: "",
+ billStatus: "",
+ projectStage: "",
+ auditStatus: "",
+ });
+
+ const filterFields = computed(() => [
+ {
+ prop: "projectNameOrCode",
+ label: "椤圭洰鍚嶇О/缂栧彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ラ」鐩悕绉�/缂栧彿",
+ },
+ {
+ prop: "customerName",
+ label: "瀹㈡埛鍚嶇О",
+ type: "input",
+ placeholder: "璇疯緭鍏ュ鎴峰悕绉�",
+ },
+ {
+ prop: "salesperson",
+ label: "涓氬姟浜哄憳",
+ type: "input",
+ placeholder: "璇疯緭鍏ヤ笟鍔′汉鍛�",
+ },
+ {
+ prop: "billStatus",
+ label: "鍗曟嵁鐘舵��",
+ type: "select",
+ options: billOptions.value,
+ },
+ {
+ prop: "projectStage",
+ label: "璁″垝鐘舵��",
+ type: "select",
+ options: stageOptions.value,
+ },
+ {
+ prop: "auditStatus",
+ label: "瀹℃牳鐘舵��",
+ type: "select",
+ options: auditOptions.value,
+ },
+ ]);
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("projectDetail");
+ uni.navigateTo({ url: "/pages/projectManagement/project/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("projectDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/projectManagement/project/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("projectDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/projectManagement/project/detail?id=${item.id}`,
+ });
+ };
+
+ const goProgress = item => {
+ uni.setStorageSync("projectDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/projectManagement/project/progress?id=${item.id}`,
+ });
+ };
+
+ const goArchive = item => {
+ uni.navigateTo({
+ url: `/pages/projectManagement/project/archive?id=${item.id}&name=${encodeURIComponent(item.projectName || "")}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ // 鎻愪氦 / 瀹℃牳鏄嚎鎬ф祦绋嬶細鏂板缓(鏃犵姸鎬�) 鈫� 鎻愪氦(reviewStatus 0) 鈫� 瀹℃牳(reviewStatus 1)銆�
+ // 绠$悊绔彟鏈夊弽瀹℃牳锛屾墜鏈虹鎸夌害瀹氫笉鍋氥��
+ const canSubmit = item => String(item?.auditStatus ?? "") === "";
+ const canAudit = item => String(item?.auditStatus) === "0";
+
+ const getList = () => {
+ listProject({
+ noOrName: query.projectNameOrCode || undefined,
+ clientName: query.customerName || undefined,
+ salesmanName: query.salesperson || undefined,
+ reviewStatus: query.auditStatus || undefined,
+ stage: query.projectStage || undefined,
+ current: -1,
+ size: -1,
+ })
+ .then(res => {
+ const rows = unwrapList(res).map(toListRow);
+ projectList.value = query.billStatus
+ ? rows.filter(
+ item => String(item.billStatus) === String(query.billStatus)
+ )
+ : rows;
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const confirmStatusAction = (item, label, content, action) => {
+ uni.showModal({
+ title: `${label}纭`,
+ content,
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ action({ id: item.id })
+ .then(() => {
+ uni.showToast({ title: `${label}鎴愬姛`, icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: `${label}澶辫触`, icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ const handleSubmitRow = item => {
+ if (!item?.id) return;
+ confirmStatusAction(item, "鎻愪氦", "纭鎻愪氦璇ラ」鐩悧锛�", submitProject);
+ };
+
+ const handleAuditRow = item => {
+ if (!item?.id) return;
+ confirmStatusAction(item, "瀹℃牳", "纭瀹℃牳閫氳繃璇ラ」鐩悧锛�", auditProject);
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: `纭鍒犻櫎椤圭洰銆�${item.projectName || ""}銆嶅悧锛焋,
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delProject([item.id])
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .action-buttons {
+ flex-wrap: wrap;
+ }
+
+ .action-btn {
+ flex: 0 0 calc(33.33% - 8px);
+ }
+</style>
diff --git a/src/pages/projectManagement/project/progress.vue b/src/pages/projectManagement/project/progress.vue
new file mode 100644
index 0000000..6931be4
--- /dev/null
+++ b/src/pages/projectManagement/project/progress.vue
@@ -0,0 +1,326 @@
+<!--
+ 杩涘害姹囨姤锛堚啋 POST /projectManagement/info/saveStage锛�
+
+ 瀛楁浠ョ鐞嗙 projectDetail.vue 鐨� handleProgressSubmitted 缁勮鍑虹殑璇锋眰浣撲负鍑嗐��
+ 绠$悊绔� ProgressReportDialog 閲岃繕鏈夈�屼笂娆¤繘搴� / 璐熻矗浜� / 閮ㄩ棬銆嶄笁涓緭鍏ユ锛屼絾瀹冧滑
+ 骞朵笉鍦ㄨ姹備綋閲岋紙璐熻矗浜哄彇鐨勬槸褰撳墠鐧诲綍浜猴紝涓嶆槸琛ㄥ崟鍊硷級锛屾墍浠ヨ繖閲屼笉鍋氬彲缂栬緫鎺т欢锛�
+ 銆屼笂娆¤繘搴︺�嶇敱宸叉湁姹囨姤璁板綍鎺ㄧ畻鍚庡彧璇诲睍绀猴紝銆岃礋璐d汉銆嶆樉绀哄皢琚褰曠殑褰撳墠鐧诲綍浜猴紝
+ 閬垮厤鍑虹幇濉簡涓嶇敓鏁堢殑瀛楁銆�
+-->
+<template>
+ <view class="project-progress">
+ <PageHeader title="杩涘害姹囨姤"
+ @back="goBack" />
+
+ <view v-if="projectName"
+ class="project-banner">
+ <text class="banner-label">椤圭洰鍚嶇О</text>
+ <text class="banner-value">{{ projectName }}</text>
+ </view>
+
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="闃舵淇℃伅"
+ class="form-section">
+ <up-form-item label="椤圭洰闃舵"
+ prop="planNodeId"
+ required>
+ <FormPicker v-model="form.planNodeId"
+ type="select"
+ title="閫夋嫨椤圭洰闃舵"
+ :options="stageOptions"
+ placeholder="璇烽�夋嫨椤圭洰闃舵" />
+ </up-form-item>
+ <up-form-item label="璁″垝寮�濮嬫椂闂�"
+ prop="planStartTime"
+ required>
+ <FormPicker v-model="form.planStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨璁″垝寮�濮嬫椂闂�" />
+ </up-form-item>
+ <up-form-item label="璁″垝瀹屽伐鏃堕棿"
+ prop="planEndTime"
+ required>
+ <FormPicker v-model="form.planEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨璁″垝瀹屽伐鏃堕棿" />
+ </up-form-item>
+ <up-form-item label="瀹為檯寮�宸ユ棩鏈�"
+ prop="actualStartTime">
+ <FormPicker v-model="form.actualStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨瀹為檯寮�宸ユ棩鏈�" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="杩涘害鎯呭喌"
+ class="form-section">
+ <up-form-item label="鏈杩涘害鏃ユ湡"
+ prop="reportDate"
+ required>
+ <FormPicker v-model="form.reportDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏈杩涘害鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="涓婃杩涘害(%)">
+ <up-input :model-value="String(lastProgress)"
+ disabled />
+ </up-form-item>
+ <up-form-item label="瀹屾垚杩涘害(%)"
+ prop="completionProgress"
+ required>
+ <view class="progress-input">
+ <up-input v-model="form.completionProgress"
+ type="number"
+ placeholder="璇疯緭鍏ュ畬鎴愯繘搴�" />
+ <up-button type="primary"
+ size="small"
+ text="瀹屾垚"
+ @click="markDone" />
+ </view>
+ </up-form-item>
+ <up-form-item label="绱杩涘害(%)"
+ prop="totalProgress">
+ <up-input v-model="form.totalProgress"
+ type="number"
+ placeholder="鐣欑┖鍒欏彇瀹屾垚杩涘害" />
+ </up-form-item>
+ <up-form-item label="瀹為檯瀹屽伐鏃ユ湡"
+ prop="actualEndTime">
+ <FormPicker v-model="form.actualEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨瀹為檯瀹屽伐鏃ユ湡" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="姹囨姤淇℃伅"
+ class="form-section">
+ <up-form-item label="璐熻矗浜�">
+ <up-input :model-value="leaderName || '褰撳墠鐧诲綍浜�'"
+ disabled />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ :maxlength="200"
+ count
+ auto-height />
+ </up-form-item>
+ <view class="upload-block">
+ <CommonUpload v-model="attachmentFiles"
+ accept="all" />
+ </view>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirm-text="鎻愪氦姹囨姤"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </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 FormPicker from "@/components/FormPicker.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
+ import useUserStore from "@/store/modules/user";
+ import { getProject, saveStage } from "@/api/projectManagement/project";
+ import {
+ buildStagePayload,
+ defaultPlanNodeId,
+ fetchPlanNodeList,
+ fetchProjectStages,
+ planNodeOptions,
+ } from "./_utils/stage";
+
+ const userStore = useUserStore();
+
+ const formRef = ref();
+ const loading = ref(false);
+ const projectId = ref("");
+ const info = ref({});
+ const projectName = ref("");
+ const planNodes = ref([]);
+ const lastProgress = ref(0);
+ const attachmentFiles = ref([]);
+
+ const leaderName = computed(() => userStore.nickName || "");
+
+ const stageOptions = computed(() => planNodeOptions(planNodes.value));
+
+ const form = ref({
+ planNodeId: "",
+ planStartTime: "",
+ planEndTime: "",
+ actualStartTime: "",
+ actualEndTime: "",
+ reportDate: "",
+ completionProgress: "",
+ totalProgress: "",
+ remark: "",
+ });
+
+ const rules = {
+ planNodeId: [{ required: true, message: "璇烽�夋嫨椤圭洰闃舵", trigger: "change" }],
+ planStartTime: [
+ { required: true, message: "璇烽�夋嫨璁″垝寮�濮嬫椂闂�", trigger: "change" },
+ ],
+ planEndTime: [
+ { required: true, message: "璇烽�夋嫨璁″垝瀹屽伐鏃堕棿", trigger: "change" },
+ ],
+ reportDate: [
+ { required: true, message: "璇烽�夋嫨鏈杩涘害鏃ユ湡", trigger: "change" },
+ ],
+ completionProgress: [
+ { required: true, message: "璇疯緭鍏ュ畬鎴愯繘搴�", trigger: "blur" },
+ ],
+ };
+
+ const goBack = () => uni.navigateBack();
+
+ // 鍚庣涓嶆帴鏀躲�屼笂娆¤繘搴︺�嶏紝鐢卞凡姹囨姤璁板綍閲岀殑鏈�澶� progress 鎺ㄧ畻锛屽彧浣滃~鍐欏弬鑰�
+ const deriveLastProgress = stages =>
+ (Array.isArray(stages) ? stages : []).reduce((max, row) => {
+ const value = Number(row?.progress ?? 0) || 0;
+ return value > max ? value : max;
+ }, 0);
+
+ const markDone = () => {
+ form.value.completionProgress = "100";
+ form.value.totalProgress = "100";
+ if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate;
+ };
+
+ const loadDetail = async () => {
+ try {
+ const res = await getProject(projectId.value);
+ const raw = res?.data?.data ?? res?.data ?? res;
+ info.value = raw?.info || {};
+ projectName.value = info.value.title || "";
+
+ const [nodes, stages] = await Promise.all([
+ fetchPlanNodeList(info.value.projectManagementPlanId),
+ fetchProjectStages(projectId.value),
+ ]);
+ planNodes.value = nodes;
+ lastProgress.value = deriveLastProgress(stages);
+
+ form.value.planNodeId = defaultPlanNodeId(info.value.stage, nodes);
+ form.value.planStartTime = info.value.planStartTime || "";
+ form.value.planEndTime = info.value.planEndTime || "";
+ form.value.actualStartTime = info.value.actualStartTime || "";
+ form.value.actualEndTime = info.value.actualEndTime || "";
+ // 鏈夊巻鍙叉眹鎶ユ墠棰勫~绱杩涘害锛涙病鏈夊垯鐣欑┖锛屾彁浜ゆ椂閫�鍥炪�屽畬鎴愯繘搴︺��
+ form.value.totalProgress = lastProgress.value
+ ? String(lastProgress.value)
+ : "";
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇椤圭洰璇︽儏澶辫触", icon: "none" });
+ }
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ loading.value = true;
+ saveStage(
+ buildStagePayload({
+ projectId: projectId.value,
+ projectInfo: info.value,
+ planNodes: planNodes.value,
+ form: form.value,
+ attachments: attachmentFiles.value,
+ leaderId: userStore.id,
+ leaderName: userStore.nickName,
+ })
+ )
+ .then(() => {
+ uni.showToast({ title: "姹囨姤鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "姹囨姤澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ projectId.value = options?.id || "";
+ if (projectId.value) loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .project-progress {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .project-banner {
+ margin: 12px 12px 0;
+ padding: 14px 16px;
+ border-radius: 12px;
+ background: #ffffff;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ .banner-label {
+ font-size: 13px;
+ color: #909399;
+ }
+
+ .banner-value {
+ font-size: 16px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .progress-input {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ }
+
+ .upload-block {
+ padding: 12px;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/qualityManagement/finalInspection/_utils/inspection.js b/src/pages/qualityManagement/finalInspection/_utils/inspection.js
new file mode 100644
index 0000000..650e9ef
--- /dev/null
+++ b/src/pages/qualityManagement/finalInspection/_utils/inspection.js
@@ -0,0 +1,61 @@
+/**
+ * 妫�楠岄」鍒ゅ畾锛堥渶姹備節.3锛�
+ *
+ * 鍒ゅ畾鍙e緞閫愬瓧澶嶅埢绠$悊绔� qualityManagement/finalInspection/components/formDia.vue
+ * 閲� watch(tableData) 鐨勯偅娈甸�昏緫锛屼笉瑕佽嚜琛屽彂鏄庯細
+ * 鏁板瓧鍨�(parameterType === 1)锛歮inValue <= 妫�楠屽�� <= maxValue
+ * 鏂囧瓧鍨�(parameterType 鍏朵綑)锛氭楠屽�煎叏绛� standardValue
+ * 鍏ㄩ儴椤归�氳繃 鈫� 鍚堟牸锛涘叏閮ㄩ」鏈�氳繃 鈫� 涓嶅悎鏍硷紱鍏朵綑 鈫� 閮ㄥ垎鍚堟牸
+ *
+ * 娉ㄦ剰鍒ゅ畾鐢ㄧ殑鏄渶澶у��/鏈�灏忓�硷紝"鍐呮帶鍊�(controlValue)"鍙綔灞曠ず銆佷笉鍙備笌鍒ゅ畾銆�
+ * 绠$悊绔湪妫�楠岄」涓虹┖鏃惰烦杩囧垽瀹氾紝杩欓噷瀵瑰簲杩斿洖绌轰覆锛岀敱璋冪敤鏂瑰喅瀹氭槸鍚︿繚鐣欏師鍊笺��
+ */
+
+export const TEXT_TYPE = 0;
+export const NUMERIC_TYPE = 1;
+
+/** 鍗曡鏄惁鍒ゅ畾閫氳繃 */
+export function isItemPass(item) {
+ const val = item?.testValue;
+ if (val === null || val === undefined || val === "") return false;
+ if (item.parameterType === NUMERIC_TYPE) {
+ const numVal = Number(val);
+ const min = Number(item.minValue);
+ const max = Number(item.maxValue);
+ return !isNaN(numVal) && numVal >= min && numVal <= max;
+ }
+ return String(val) === String(item.standardValue);
+}
+
+/** 妫�楠岄」鍒楄〃 鈫� 妫�娴嬬粨鏋滐紙鍚堟牸 / 涓嶅悎鏍� / 閮ㄥ垎鍚堟牸锛夛紝绌哄垪琛ㄨ繑鍥炵┖涓� */
+export function judgeInspectionResult(rows) {
+ const list = Array.isArray(rows) ? rows : [];
+ if (list.length === 0) return "";
+
+ let passCount = 0;
+ let failCount = 0;
+ list.forEach(item => {
+ if (isItemPass(item)) passCount++;
+ else failCount++;
+ });
+
+ if (failCount === 0 && passCount > 0) return "鍚堟牸";
+ if (passCount === 0 && failCount > 0) return "涓嶅悎鏍�";
+ return "閮ㄥ垎鍚堟牸";
+}
+
+/** 涓存椂杩藉姞鐨勬娴嬮」琛岋紙闇�姹備節.2锛夛細涓嶅彈鎸囨爣鏍囧噯绾︽潫锛屽悇瀛楁閮藉彲缂栬緫 */
+export function createTempItem() {
+ return {
+ id: undefined,
+ isTemp: true,
+ parameterItem: "",
+ parameterType: NUMERIC_TYPE,
+ unit: "",
+ standardValue: "",
+ controlValue: "",
+ maxValue: null,
+ minValue: null,
+ testValue: "",
+ };
+}
diff --git a/src/pages/qualityManagement/finalInspection/add.vue b/src/pages/qualityManagement/finalInspection/add.vue
index 94321dd..b827124 100644
--- a/src/pages/qualityManagement/finalInspection/add.vue
+++ b/src/pages/qualityManagement/finalInspection/add.vue
@@ -125,7 +125,13 @@
<view class="inspection-items-container">
<view class="steps-header">
<text class="steps-title">妫�楠岄」鐩�</text>
- <text class="steps-count">鍏� {{ tableData.length }} 涓」鐩�</text>
+ <view class="steps-header-right">
+ <text class="steps-count">鍏� {{ tableData.length }} 涓」鐩�</text>
+ <up-button type="primary"
+ size="mini"
+ text="杩藉姞妫�娴嬮」"
+ @click="addTempItem" />
+ </view>
</view>
<view class="steps-list">
<view v-for="(item, index) in tableData"
@@ -137,28 +143,106 @@
<view class="step-content">
<view class="step-row">
<text class="step-label">鎸囨爣锛�</text>
- <text class="step-value">{{ item.parameterItem }}</text>
+ <up-input v-if="item.isTemp"
+ v-model="item.parameterItem"
+ placeholder="璇疯緭鍏ユ寚鏍囧悕绉�"
+ border-bottom
+ class="step-input" />
+ <text v-else
+ class="step-value">{{ item.parameterItem }}</text>
</view>
<view class="step-row">
+ <text class="step-label">鍙傛暟绫诲瀷锛�</text>
+ <view v-if="item.isTemp"
+ class="type-switch">
+ <text class="type-option"
+ :class="{ active: item.parameterType === TEXT_TYPE }"
+ @click="setTempType(item, TEXT_TYPE)">鏂囧瓧</text>
+ <text class="type-option"
+ :class="{ active: item.parameterType === NUMERIC_TYPE }"
+ @click="setTempType(item, NUMERIC_TYPE)">鏁板瓧</text>
+ </view>
+ <text v-else
+ class="step-value">{{ isNumeric(item) ? "鏁板瓧" : "鏂囧瓧" }}</text>
+ </view>
+ <!-- 鏁板瓧鍨嬫寜涓婁笅闄愬垽瀹氾紝鎶婁笂涓嬮檺鎽嗗嚭鏉ユ墠鐪嬪緱鎳傝嚜鍔ㄥ垽瀹氱殑渚濇嵁 -->
+ <template v-if="isNumeric(item)">
+ <view class="step-row">
+ <text class="step-label">鏈�澶у�硷細</text>
+ <up-input v-if="item.isTemp"
+ v-model="item.maxValue"
+ type="number"
+ placeholder="璇疯緭鍏ユ渶澶у��"
+ border-bottom
+ class="step-input" />
+ <text v-else
+ class="step-value">{{ item.maxValue }}</text>
+ </view>
+ <view class="step-row">
+ <text class="step-label">鏈�灏忓�硷細</text>
+ <up-input v-if="item.isTemp"
+ v-model="item.minValue"
+ type="number"
+ placeholder="璇疯緭鍏ユ渶灏忓��"
+ border-bottom
+ class="step-input" />
+ <text v-else
+ class="step-value">{{ item.minValue }}</text>
+ </view>
+ </template>
+ <view class="step-row">
<text class="step-label">鍗曚綅锛�</text>
- <text class="step-value">{{ item.unit }}</text>
+ <up-input v-if="item.isTemp"
+ v-model="item.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ border-bottom
+ class="step-input" />
+ <text v-else
+ class="step-value">{{ item.unit }}</text>
</view>
<view class="step-row">
<text class="step-label">鏍囧噯鍊硷細</text>
- <text class="step-value">{{ item.standardValue }}</text>
+ <up-input v-if="item.isTemp"
+ v-model="item.standardValue"
+ placeholder="璇疯緭鍏ユ爣鍑嗗��"
+ border-bottom
+ class="step-input" />
+ <text v-else
+ class="step-value">{{ item.standardValue }}</text>
</view>
<view class="step-row">
<text class="step-label">鍐呮帶鍊硷細</text>
- <text class="step-value">{{ item.controlValue }}</text>
+ <up-input v-if="item.isTemp"
+ v-model="item.controlValue"
+ placeholder="璇疯緭鍏ュ唴鎺у��"
+ border-bottom
+ class="step-input" />
+ <text v-else
+ class="step-value">{{ item.controlValue }}</text>
</view>
<view class="step-row">
<text class="step-label">妫�楠屽�硷細</text>
<up-input v-model="item.testValue"
+ :type="isNumeric(item) ? 'number' : 'text'"
placeholder="璇疯緭鍏ユ楠屽��"
clearable
border-bottom
class="step-input" />
</view>
+ <view class="step-row">
+ <text class="step-label">鍒ゅ畾锛�</text>
+ <text class="step-value judge-text"
+ :class="itemPassClass(item)">
+ {{ judgeText(item) }}
+ </text>
+ </view>
+ </view>
+ <view v-if="item.isTemp"
+ class="delete-btn"
+ @click="removeTempItem(index)">
+ <up-icon name="close"
+ size="14"
+ color="#ffffff" />
</view>
</view>
<view v-if="tableData.length === 0"
@@ -302,7 +386,7 @@
</template>
<script setup>
- import { ref, computed, onMounted, nextTick } from "vue";
+ import { ref, computed, onMounted, nextTick, watch } from "vue";
import { onShow, onLoad } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
import dayjs from "dayjs";
@@ -317,6 +401,13 @@
list,
} from "@/api/qualityManagement/materialInspection.js";
import { userListNoPage } from "@/api/system/user.js";
+ import {
+ NUMERIC_TYPE,
+ TEXT_TYPE,
+ createTempItem,
+ isItemPass,
+ judgeInspectionResult,
+ } from "./_utils/inspection";
// 鏄剧ず鎻愮ず淇℃伅
const showToast = message => {
@@ -671,6 +762,54 @@
});
};
+ // 鏁板瓧鍨嬫墠鏈夋渶澶�/鏈�灏忓�硷紝鍒ゅ畾涔熸寜涓婁笅闄愯蛋
+ const isNumeric = item => item?.parameterType === NUMERIC_TYPE;
+
+ const judgeText = item => {
+ const val = item?.testValue;
+ if (val === null || val === undefined || val === "") return "-";
+ return isItemPass(item) ? "鍚堟牸" : "涓嶅悎鏍�";
+ };
+
+ const itemPassClass = item => {
+ const val = item?.testValue;
+ if (val === null || val === undefined || val === "") return "";
+ return isItemPass(item) ? "pass" : "fail";
+ };
+
+ // 闇�姹備節.3锛氭楠屽�间竴鍙樺氨鑷姩鍒ゅ畾锛屽彛寰勮 _utils/inspection.js銆�
+ // 涓庣鐞嗙涓�鑷达紝杩欓噷鏃犳潯浠惰鐩� form.checkResult 鈥斺�� 鎵嬪伐閫夌殑妫�娴嬬粨鏋�
+ // 鍙槸涓存椂鍏滃簳锛屼笅娆℃敼鍔ㄦ楠岄」鏃朵細琚噸鏂板垽瀹氳鐩栥��
+ watch(
+ tableData,
+ rows => {
+ const result = judgeInspectionResult(rows);
+ if (result) form.value.checkResult = result;
+ },
+ { deep: true }
+ );
+
+ // 闇�姹備節.2锛氫复鏃惰拷鍔犳娴嬮」
+ const addTempItem = () => {
+ tableData.value.push(createTempItem());
+ };
+
+ const removeTempItem = index => {
+ tableData.value.splice(index, 1);
+ };
+
+ // 鍒囩被鍨嬫椂娓呮帀涓嶅啀閫傜敤鐨勫�硷紝閬垮厤鏂囧瓧鍨嬫畫鐣欎笂涓嬮檺銆佹暟瀛楀瀷娈嬬暀鏍囧噯鍊�
+ const setTempType = (item, type) => {
+ if (item.parameterType === type) return;
+ item.parameterType = type;
+ if (type === NUMERIC_TYPE) {
+ item.standardValue = "";
+ } else {
+ item.maxValue = null;
+ item.minValue = null;
+ }
+ };
+
// 鎻愪氦琛ㄥ崟
const submitForm = async () => {
console.log("submitForm", form.value, tableData.value);
@@ -700,10 +839,19 @@
showToast("璇烽�夋嫨妫�娴嬬粨鏋�");
return;
}
+ const missingTemp = tableData.value.find(
+ item => item.isTemp && !item.parameterItem
+ );
+ if (missingTemp) {
+ showToast("璇峰~鍐欒拷鍔犳娴嬮」鐨勬寚鏍囧悕绉�");
+ return;
+ }
loading.value = true;
form.value.inspectType = 2;
+ // isTemp 鍙槸鍓嶇鐨勮鏍囪锛屼笉鑳芥彁浜ょ粰鍚庣
+ tableData.value = tableData.value.map(({ isTemp, ...rest }) => rest);
if (!isEdit.value) {
tableData.value.forEach(item => {
delete item.id;
@@ -931,6 +1079,40 @@
color: #909399;
}
+ .steps-header-right {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ // 涓存椂杩藉姞琛岀殑鍙傛暟绫诲瀷鍒囨崲
+ .type-switch {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .type-option {
+ padding: 4px 14px;
+ font-size: 13px;
+ color: #606266;
+ background: #f4f4f5;
+ border-radius: 12px;
+ }
+
+ .type-option.active {
+ color: #409eff;
+ background: #ecf5ff;
+ }
+
+ .judge-text.pass {
+ color: #67c23a;
+ }
+
+ .judge-text.fail {
+ color: #f56c6c;
+ }
+
.steps-list {
margin-bottom: 20px;
}
diff --git a/src/pages/qualityManagement/finalInspection/detail.vue b/src/pages/qualityManagement/finalInspection/detail.vue
index d274a28..4080d94 100644
--- a/src/pages/qualityManagement/finalInspection/detail.vue
+++ b/src/pages/qualityManagement/finalInspection/detail.vue
@@ -94,6 +94,12 @@
<text class="item-detail-label">鍗曚綅:</text>
<text class="item-detail-value">{{ item.unit || '-' }}</text>
</view>
+ <!-- 鏁板瓧鍨嬫寜涓婁笅闄愬垽瀹氾紝鎶婁笂涓嬮檺鎽嗗嚭鏉ユ墠鐪嬪緱鎳傚垽瀹氫緷鎹� -->
+ <view v-if="isNumeric(item)"
+ class="item-detail-row">
+ <text class="item-detail-label">涓婁笅闄�:</text>
+ <text class="item-detail-value">{{ item.minValue }} ~ {{ item.maxValue }}</text>
+ </view>
<view class="item-detail-row">
<text class="item-detail-label">鏍囧噯鍊�:</text>
<text class="item-detail-value">{{ item.standardValue || '-' }}</text>
@@ -104,23 +110,28 @@
</view>
<view class="item-detail-row">
<text class="item-detail-label">妫�楠屽��:</text>
+ <text class="item-detail-value">{{ item.testValue || '-' }}</text>
+ </view>
+ <view class="item-detail-row">
+ <text class="item-detail-label">鍒ゅ畾:</text>
<text class="item-detail-value result"
- :class="getResultClass(item.testValue, item.standardValue)">
- {{ item.testValue || '-' }}
+ :class="judgeClass(item)">
+ {{ judgeText(item) }}
</text>
</view>
</view>
</view>
</view>
</view>
- <!-- 鎿嶄綔鎸夐挳 -->
- <!-- <view class="action-buttons">
+ <!-- 闇�姹備節.4锛氫笅杞� PDF 鍚堟牸璇� -->
+ <view class="action-buttons">
<u-button type="primary"
class="action-btn"
+ :loading="downloading"
@click="downloadReport">
- 涓嬭浇鎶ュ憡
+ 涓嬭浇鍚堟牸璇丳DF
</u-button>
- </view> -->
+ </view>
</view>
<view v-else
class="no-data">
@@ -136,6 +147,8 @@
import PageHeader from "@/components/PageHeader.vue";
import dayjs from "dayjs";
import { qualityInspectParamInfo } from "@/api/qualityManagement/materialInspection.js";
+ import { downloadQualityInspectPdf } from "@/api/qualityManagement/qualityInspectFile.js";
+ import { NUMERIC_TYPE, isItemPass } from "./_utils/inspection";
// 鏄剧ず鎻愮ず淇℃伅
const showToast = message => {
@@ -149,6 +162,8 @@
const detailData = ref(null);
// 妫�楠岄」鐩�
const inspectionItems = ref([]);
+ // 鍚堟牸璇丳DF 涓嬭浇涓�
+ const downloading = ref(false);
// 杩斿洖涓婁竴椤�
const goBack = () => {
@@ -178,31 +193,34 @@
return state ? "success" : "warning";
};
- // 鑾峰彇缁撴灉鏍峰紡
- const getResultClass = (testValue, standardValue) => {
- // 绠�鍗曠殑缁撴灉鍒ゆ柇閫昏緫锛屽疄闄呴」鐩腑鍙兘闇�瑕佹洿澶嶆潅鐨勫垽鏂�
- if (testValue === "鍚堟牸") {
- return "result-passed";
- } else if (testValue === "涓嶅悎鏍�") {
- return "result-rejected";
- }
- return "";
+ // 鏁板瓧鍨嬫墠鏈変笂涓嬮檺
+ const isNumeric = item => item?.parameterType === NUMERIC_TYPE;
+
+ const judgeText = item => {
+ const val = item?.testValue;
+ if (val === null || val === undefined || val === "") return "-";
+ return isItemPass(item) ? "鍚堟牸" : "涓嶅悎鏍�";
};
- // 涓嬭浇鎶ュ憡
- const downloadReport = () => {
- uni.showToast({
- title: "鎶ュ憡涓嬭浇涓�...",
- icon: "loading",
- });
+ const judgeClass = item => {
+ const val = item?.testValue;
+ if (val === null || val === undefined || val === "") return "";
+ return isItemPass(item) ? "result-passed" : "result-rejected";
+ };
- // 妯℃嫙涓嬭浇
- setTimeout(() => {
- uni.showToast({
- title: "鎶ュ憡涓嬭浇鎴愬姛",
- icon: "success",
+ // 涓嬭浇 PDF 鍚堟牸璇�
+ const downloadReport = () => {
+ if (!optionsId.value) {
+ showToast("鍙傛暟閿欒");
+ return;
+ }
+ downloading.value = true;
+ // downloadFileByPost 鍐呴儴宸� toast锛岃繖閲屽彧璐熻矗鏀跺熬 loading
+ downloadQualityInspectPdf(optionsId.value)
+ .catch(() => {})
+ .finally(() => {
+ downloading.value = false;
});
- }, 1500);
};
// 鑾峰彇璇︽儏鏁版嵁
@@ -413,6 +431,14 @@
color: #f56c6c;
}
+ .action-buttons {
+ padding: 4px 0 8px;
+ }
+
+ .action-btn {
+ width: 100%;
+ }
+
// 绌虹姸鎬�
.no-data {
padding: 60px 20px;
diff --git a/src/pages/qualityManagement/finalInspection/fileList.vue b/src/pages/qualityManagement/finalInspection/fileList.vue
index 126158d..4c8925e 100644
--- a/src/pages/qualityManagement/finalInspection/fileList.vue
+++ b/src/pages/qualityManagement/finalInspection/fileList.vue
@@ -3,6 +3,15 @@
<!-- 椤甸潰澶撮儴 -->
<PageHeader title="闄勪欢绠$悊"
@back="goBack" />
+ <!-- 闇�姹備節.4锛氫笅杞� PDF 鍚堟牸璇� -->
+ <view class="pdf-bar">
+ <u-button type="primary"
+ size="small"
+ :loading="downloadingPdf"
+ @click="downloadPdf">
+ 涓嬭浇鍚堟牸璇丳DF
+ </u-button>
+ </view>
<!-- 闄勪欢鍒楄〃 -->
<view class="file-list-container">
<view v-if="fileList.length > 0"
@@ -78,10 +87,28 @@
qualityInspectFileListPage,
qualityInspectFileDel,
} from "@/api/qualityManagement/materialInspection";
+ import { downloadQualityInspectPdf } from "@/api/qualityManagement/qualityInspectFile.js";
+ import { openFile } from "@/utils/file";
import { blobValidate } from "@/utils/ruoyi";
// 闄勪欢鍒楄〃
const fileList = ref([]);
+ // 鍚堟牸璇丳DF 涓嬭浇涓�
+ const downloadingPdf = ref(false);
+
+ // 涓嬭浇 PDF 鍚堟牸璇侊紙涓庨檮浠跺悓涓�浠� inspectId锛�
+ const downloadPdf = () => {
+ if (!rulesRegulationsManagementId.value) {
+ showToast("鍙傛暟閿欒");
+ return;
+ }
+ downloadingPdf.value = true;
+ downloadQualityInspectPdf(rulesRegulationsManagementId.value)
+ .catch(() => {})
+ .finally(() => {
+ downloadingPdf.value = false;
+ });
+ };
// 杩斿洖涓婁竴椤�
const goBack = () => {
@@ -259,81 +286,11 @@
});
});
};
- // 涓嬭浇鏂囦欢
+ // 涓嬭浇骞堕瑙堛��
+ // 鍘熸潵璧扮殑鏄� /common/download?...&delete=true 鈥斺�� 浼氭妸鏈嶅姟绔檮浠跺垹鎺夛紝
+ // 涓斿洖璋冮噷寮曠敤浜嗘湭瀹氫箟鐨� resolve/reject銆傛敼涓虹洿鎺ユ寜 url 鎵撳紑銆�
const downloadFile = file => {
- var url =
- config.baseUrl +
- "/common/download?fileName=" +
- encodeURIComponent(file.url) +
- "&delete=true";
- console.log(url, "url");
-
- uni
- .downloadFile({
- url: url,
- responseType: "blob",
- header: { Authorization: "Bearer " + getToken() },
- })
- .then(res => {
- let osType = uni.getStorageSync("deviceInfo").osName;
- let filePath = res.tempFilePath;
- if (osType === "ios") {
- uni.openDocument({
- filePath: filePath,
- showMenu: true,
- success: res => {
- resolve(res);
- },
- fail: err => {
- console.log("uni.openDocument--fail");
- reject(err);
- },
- });
- } else {
- uni.saveFile({
- tempFilePath: filePath,
- success: fileRes => {
- uni.showToast({
- icon: "none",
- mask: true,
- title:
- "鏂囦欢宸蹭繚瀛橈細Android/data/uni.UNI720216F/apps/__UNI__720216F/" +
- fileRes.savedFilePath, //淇濆瓨璺緞
- duration: 3000,
- });
- setTimeout(() => {
- //鎵撳紑鏂囨。鏌ョ湅
- uni.openDocument({
- filePath: fileRes.savedFilePath,
- success: function (res) {
- resolve(fileRes);
- },
- });
- }, 3000);
- },
- 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 => {
- console.error("涓嬭浇澶辫触:", err);
- showToast("涓嬭浇澶辫触");
- });
+ openFile(file?.url);
};
// 纭鍒犻櫎
@@ -431,6 +388,10 @@
padding: 20rpx;
}
+ .pdf-bar {
+ padding: 16rpx 20rpx 0;
+ }
+
.file-list {
background: #ffffff;
border-radius: 8rpx;
diff --git a/src/pages/qualityManagement/inspectionReport/detail.vue b/src/pages/qualityManagement/inspectionReport/detail.vue
new file mode 100644
index 0000000..6dcf6b5
--- /dev/null
+++ b/src/pages/qualityManagement/inspectionReport/detail.vue
@@ -0,0 +1,264 @@
+<template>
+ <view class="inspection-report-detail">
+ <PageHeader title="鎶ュ憡璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view class="detail-card"
+ v-if="!loading">
+ <view class="hero">
+ <text class="hero-title">{{ detail.reportNo || "-" }}</text>
+ <up-tag :text="detail.reportType || '鏈垎绫�'"
+ type="primary"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <view class="warn-banner"
+ v-if="warn.text"
+ :style="{ background: warn.level === 'normal' ? '#f0f9eb' : '#fef0f0' }">
+ <text class="warn-banner-text"
+ :style="{ color: warn.color }">鏈夋晥鏈熺姸鎬侊細{{ warn.text }}锛堝埌鏈熸椂闂� {{ detail.expirationTime || "-" }}锛�</text>
+ </view>
+
+ <u-cell-group title="鎶ュ憡淇℃伅"
+ class="detail-section">
+ <u-cell title="浜у搧鍚嶇О"
+ :value="detail.productName || '-'" />
+ <u-cell title="浜у搧鍨嬪彿"
+ :value="detail.productModel || '-'" />
+ <u-cell title="鎶ュ憡缂栧彿"
+ :value="detail.reportNo || '-'" />
+ <u-cell title="鎶ュ憡绫诲瀷"
+ :value="detail.reportType || '-'" />
+ <u-cell title="妫�娴嬪崟浣�"
+ :value="detail.testingAgency || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鏈夋晥鏈�"
+ class="detail-section">
+ <u-cell title="鎶ュ憡鏃ユ湡"
+ :value="detail.reportDate || '-'" />
+ <u-cell title="鏈夋晥鏈�(鏈�)"
+ :value="detail.validityPeriod != null ? String(detail.validityPeriod) : '-'" />
+ <u-cell title="鍒版湡鏃堕棿"
+ :value="detail.expirationTime || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="澶囨敞"
+ class="detail-section">
+ <u-cell title="澶囨敞"
+ :value="detail.remark || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="闄勪欢"
+ class="detail-section">
+ <view v-if="attachments.length === 0"
+ class="empty-file">
+ <text>鏆傛棤闄勪欢</text>
+ </view>
+ <view v-else
+ class="file-list">
+ <view v-for="(file, index) in attachments"
+ :key="index"
+ class="file-item"
+ @click="previewFile(file, index)">
+ <up-icon name="file-text"
+ size="18"
+ color="#2979ff"></up-icon>
+ <text class="file-name">{{ fileLabel(file, index) }}</text>
+ <up-icon name="download"
+ size="16"
+ color="#909399"></up-icon>
+ </view>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { toFileList } from "@/utils/file";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { getWarnInfo } from "@/utils/warning";
+ import { getInspectionReport } from "@/api/qualityManagement/inspectionReport";
+
+ const WARN_DAYS = 30;
+
+ const loading = ref(true);
+ const reportId = ref("");
+ const detail = ref({});
+
+ const attachments = computed(() => toFileList(detail.value.attachmentUrls));
+ const warn = computed(() => getWarnInfo(detail.value.expirationTime, WARN_DAYS));
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("inspectionReportDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/qualityManagement/inspectionReport/edit?id=${reportId.value}`,
+ });
+ };
+
+ const fileLabel = (file, index) =>
+ file.name || file.originalFilename || `闄勪欢${index + 1}`;
+
+ // request.ts 鏈�忎紶 responseType锛宐lob 涓嬭浇涓嶅彲鐢紝鍙兘鐢� downloadFile + openDocument
+ const previewFile = (file, index) => {
+ const url = file.url || file.previewURL;
+ if (!url) return;
+ uni.showLoading({ title: "鎵撳紑涓�...", mask: true });
+ uni.downloadFile({
+ url,
+ success: res => {
+ if (res.statusCode !== 200) {
+ uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" });
+ return;
+ }
+ uni.openDocument({
+ filePath: res.tempFilePath,
+ showMenu: true,
+ fail: () => uni.showToast({ title: "鏆備笉鏀寔棰勮璇ユ枃浠�", icon: "none" }),
+ });
+ },
+ fail: () => uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" }),
+ complete: () => uni.hideLoading(),
+ });
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const data = await loadDetailData({
+ cacheKey: "inspectionReportDetail",
+ id: reportId.value,
+ fetcher: getInspectionReport,
+ });
+ detail.value = data || {};
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇鎶ュ憡璇︽儏澶辫触", icon: "none" });
+ detail.value = {};
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ reportId.value = options?.id || "";
+ if (!reportId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .inspection-report-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .warn-banner {
+ margin-top: 12px;
+ padding: 10px 14px;
+ border-radius: 10px;
+ }
+
+ .warn-banner-text {
+ font-size: 13px;
+ line-height: 1.5;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .empty-file {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .file-list {
+ padding: 6px 16px 12px;
+ }
+
+ .file-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 0;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .file-item:last-child {
+ border-bottom: none;
+ }
+
+ .file-name {
+ flex: 1;
+ font-size: 14px;
+ color: #333;
+ word-break: break-all;
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/qualityManagement/inspectionReport/edit.vue b/src/pages/qualityManagement/inspectionReport/edit.vue
new file mode 100644
index 0000000..24aeee0
--- /dev/null
+++ b/src/pages/qualityManagement/inspectionReport/edit.vue
@@ -0,0 +1,290 @@
+<template>
+ <view class="account-detail">
+ <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="productName"
+ required
+ border-bottom>
+ <up-input v-model="form.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="浜у搧鍨嬪彿"
+ prop="productModel"
+ border-bottom>
+ <up-input v-model="form.productModel"
+ placeholder="璇疯緭鍏ヤ骇鍝佸瀷鍙�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎶ュ憡缂栧彿"
+ prop="reportNo"
+ required
+ border-bottom>
+ <up-input v-model="form.reportNo"
+ placeholder="璇疯緭鍏ユ姤鍛婄紪鍙�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎶ュ憡绫诲瀷"
+ prop="reportType"
+ border-bottom>
+ <FormPicker v-model="form.reportType"
+ type="select"
+ title="閫夋嫨鎶ュ憡绫诲瀷"
+ :options="reportTypeOptions"
+ placeholder="璇烽�夋嫨鎶ュ憡绫诲瀷" />
+ </up-form-item>
+ <up-form-item label="妫�娴嬪崟浣�"
+ prop="testingAgency"
+ border-bottom>
+ <up-input v-model="form.testingAgency"
+ placeholder="璇疯緭鍏ユ娴嬪崟浣�"
+ clearable />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鏈夋晥鏈�"
+ class="form-section">
+ <up-form-item label="鎶ュ憡鏃ユ湡"
+ prop="reportDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.reportDate"
+ type="date"
+ placeholder="璇烽�夋嫨鎶ュ憡鏃ユ湡"
+ @change="calculateExpiration" />
+ </up-form-item>
+ <up-form-item label="鏈夋晥鏈�(鏈�)"
+ prop="validityPeriod"
+ required
+ border-bottom>
+ <up-input v-model="form.validityPeriod"
+ type="number"
+ placeholder="1~120锛岄粯璁� 12"
+ clearable
+ @blur="calculateExpiration" />
+ </up-form-item>
+ <up-form-item label="鍒版湡鏃堕棿"
+ prop="expirationTime"
+ required
+ border-bottom>
+ <up-input v-model="form.expirationTime"
+ placeholder="绯荤粺鑷姩璁$畻"
+ disabled />
+ </up-form-item>
+ <up-form-item label="澶囨敞"
+ prop="remark">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉ㄥ唴瀹�"
+ auto-height
+ maxlength="500" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="闄勪欢"
+ class="form-section">
+ <view class="upload-wrap">
+ <CommonUpload v-model="form.attachmentFiles"
+ accept="all"
+ :max-count="5" />
+ </view>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </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 FormPicker from "@/components/FormPicker.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
+ import { formatDateToYMD } from "@/utils/ruoyi";
+ import { toFileList, toUrlString } from "@/utils/file";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { addMonths } from "@/utils/warning";
+ import {
+ addInspectionReport,
+ getInspectionReport,
+ updateInspectionReport,
+ } from "@/api/qualityManagement/inspectionReport";
+
+ const formRef = ref();
+ const loading = ref(false);
+ const reportId = ref("");
+
+ const reportTypeOptions = [
+ { name: "鍨嬪紡璇曢獙鎶ュ憡", value: "鍨嬪紡璇曢獙鎶ュ憡" },
+ { name: "鍑哄巶妫�楠屾姤鍛�", value: "鍑哄巶妫�楠屾姤鍛�" },
+ { name: "濮旀墭妫�楠屾姤鍛�", value: "濮旀墭妫�楠屾姤鍛�" },
+ ];
+
+ const form = ref({
+ id: undefined,
+ productName: "",
+ productModel: "",
+ reportNo: "",
+ reportType: "",
+ testingAgency: "",
+ reportDate: "",
+ validityPeriod: 12,
+ expirationTime: "",
+ remark: "",
+ attachmentFiles: [],
+ });
+
+ const rules = {
+ productName: [
+ { required: true, message: "璇疯緭鍏ヤ骇鍝佸悕绉�", trigger: "blur" },
+ ],
+ reportNo: [{ required: true, message: "璇疯緭鍏ユ姤鍛婄紪鍙�", trigger: "blur" }],
+ reportDate: [
+ { required: true, message: "璇烽�夋嫨鎶ュ憡鏃ユ湡", trigger: "change" },
+ ],
+ validityPeriod: [
+ { required: true, message: "璇疯緭鍏ユ湁鏁堟湡", trigger: "blur" },
+ ],
+ expirationTime: [
+ { required: true, message: "鍒版湡鏃堕棿涓嶈兘涓虹┖", trigger: "change" },
+ ],
+ };
+
+ const pageTitle = computed(() => (reportId.value ? "缂栬緫鎶ュ憡" : "鏂板鎶ュ憡"));
+
+ // 鍒版湡鏃堕棿 = 鎶ュ憡鏃ユ湡 + 鏈夋晥鏈熸湀锛屽彧璇�
+ const calculateExpiration = () => {
+ const months = Number(form.value.validityPeriod);
+ if (form.value.reportDate && Number.isFinite(months) && months > 0) {
+ form.value.expirationTime = addMonths(form.value.reportDate, months);
+ } else {
+ form.value.expirationTime = "";
+ }
+ };
+
+ const goBack = () => uni.navigateBack();
+
+ const loadDetail = async () => {
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "inspectionReportDetail",
+ id: reportId.value,
+ fetcher: getInspectionReport,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鏈壘鍒版姤鍛婁俊鎭�", icon: "none" });
+ return;
+ }
+
+ form.value = {
+ id: data.id,
+ productName: data.productName || "",
+ productModel: data.productModel || "",
+ reportNo: data.reportNo || "",
+ reportType: data.reportType || "",
+ testingAgency: data.testingAgency || "",
+ reportDate: (data.reportDate || "").slice(0, 10),
+ validityPeriod: data.validityPeriod ?? 12,
+ expirationTime: (data.expirationTime || "").slice(0, 10),
+ remark: data.remark || "",
+ attachmentFiles: toFileList(data.attachmentUrls),
+ };
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ calculateExpiration();
+ const payload = {
+ id: form.value.id,
+ productName: form.value.productName,
+ productModel: form.value.productModel,
+ reportNo: form.value.reportNo,
+ reportType: form.value.reportType,
+ testingAgency: form.value.testingAgency,
+ reportDate: form.value.reportDate,
+ validityPeriod: Number(form.value.validityPeriod),
+ expirationTime: form.value.expirationTime,
+ remark: form.value.remark,
+ attachmentUrls: toUrlString(form.value.attachmentFiles),
+ };
+
+ loading.value = true;
+ const action = reportId.value ? updateInspectionReport : addInspectionReport;
+ action(payload)
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 1000);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ reportId.value = options.id;
+ form.value.id = options.id;
+ loadDetail();
+ } else {
+ form.value.reportDate = formatDateToYMD(Date.now());
+ calculateExpiration();
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .upload-wrap {
+ padding: 12px;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/qualityManagement/inspectionReport/index.vue b/src/pages/qualityManagement/inspectionReport/index.vue
new file mode 100644
index 0000000..2f94b44
--- /dev/null
+++ b/src/pages/qualityManagement/inspectionReport/index.vue
@@ -0,0 +1,265 @@
+<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="query.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉版悳绱�"
+ clearable
+ @change="handleSearch" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ <view v-if="expiringCount > 0"
+ class="warn-tip">
+ <text class="warn-tip-text">{{ expiringCount }} 浠芥姤鍛婂凡鍒版湡鎴栧皢鍦� 30 澶╁唴鍒版湡</text>
+ </view>
+ </view>
+
+ <view v-if="reportList.length > 0"
+ class="ledger-list">
+ <view v-for="item in reportList"
+ :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.reportNo || "-" }}</text>
+ </view>
+ <up-tag :text="item.reportType || '鏈垎绫�'"
+ type="primary"
+ size="mini"
+ shape="circle" />
+ </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.productModel || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">妫�娴嬪崟浣�</text>
+ <text class="detail-value">{{ item.testingAgency || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶ュ憡鏃ユ湡</text>
+ <text class="detail-value">{{ item.reportDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍒版湡鏃堕棿</text>
+ <text class="detail-value">{{ item.expirationTime || "-" }}</text>
+ </view>
+ <view class="detail-row"
+ v-if="warnInfo(item).text">
+ <text class="detail-label">鏈夋晥鏈熺姸鎬�</text>
+ <text class="detail-value"
+ :style="{ color: warnInfo(item).color }">{{ warnInfo(item).text }}</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"
+ @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">
+ <text>鏆傛棤鎶ュ憡鏁版嵁</text>
+ </view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鎶ュ憡绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ delInspectionReport,
+ listInspectionReport,
+ } from "@/api/qualityManagement/inspectionReport";
+ import { unwrapList } from "@/utils/list";
+ import { getWarnInfo } from "@/utils/warning";
+
+ // 鎶ュ憡鏈夋晥鏈熼璀﹂槇鍊硷紙闇�姹傚崄锛氬埌鏈熷墠 30 澶╋級
+ const WARN_DAYS = 30;
+
+ const reportList = ref([]);
+ const showFilter = ref(false);
+
+ const query = reactive({
+ productName: "",
+ reportNo: "",
+ reportType: "",
+ });
+
+ const filterFields = [
+ {
+ prop: "productName",
+ label: "浜у搧鍚嶇О",
+ type: "input",
+ placeholder: "璇疯緭鍏ヤ骇鍝佸悕绉�",
+ },
+ {
+ prop: "reportNo",
+ label: "鎶ュ憡缂栧彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ユ姤鍛婄紪鍙�",
+ },
+ {
+ prop: "reportType",
+ label: "鎶ュ憡绫诲瀷",
+ type: "select",
+ options: [
+ { name: "鍨嬪紡璇曢獙鎶ュ憡", value: "鍨嬪紡璇曢獙鎶ュ憡" },
+ { name: "鍑哄巶妫�楠屾姤鍛�", value: "鍑哄巶妫�楠屾姤鍛�" },
+ { name: "濮旀墭妫�楠屾姤鍛�", value: "濮旀墭妫�楠屾姤鍛�" },
+ ],
+ },
+ ];
+
+ const warnInfo = item => getWarnInfo(item.expirationTime, WARN_DAYS);
+
+ const expiringCount = computed(
+ () =>
+ reportList.value.filter(item => {
+ const level = warnInfo(item).level;
+ return level === "overdue" || level === "warning";
+ }).length
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("inspectionReportDetail");
+ uni.navigateTo({ url: "/pages/qualityManagement/inspectionReport/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("inspectionReportDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/qualityManagement/inspectionReport/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("inspectionReportDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/qualityManagement/inspectionReport/detail?id=${item.id}`,
+ });
+ };
+
+ const getList = () => {
+ listInspectionReport({
+ current: -1,
+ size: -1,
+ productName: query.productName || undefined,
+ reportNo: query.reportNo || undefined,
+ reportType: query.reportType || undefined,
+ })
+ .then(res => {
+ reportList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ const handleSearch = () => {
+ getList();
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "娉ㄩ攢纭",
+ content: "璇ユ姤鍛婂皢琚敞閿�浣滃簾锛屾槸鍚︾‘璁わ紵",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delInspectionReport(item.id)
+ .then(() => {
+ uni.showToast({ title: "娉ㄩ攢鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "娉ㄩ攢澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .warn-tip {
+ margin-top: 10px;
+ padding: 8px 12px;
+ border-radius: 8px;
+ background: #fff5f5;
+ }
+
+ .warn-tip-text {
+ font-size: 13px;
+ color: #f56c6c;
+ }
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/sales/biddingManagement/detail.vue b/src/pages/sales/biddingManagement/detail.vue
new file mode 100644
index 0000000..cc70a02
--- /dev/null
+++ b/src/pages/sales/biddingManagement/detail.vue
@@ -0,0 +1,246 @@
+<template>
+ <view class="bidding-detail">
+ <PageHeader title="鎷涙姇鏍囪鎯�"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <view class="hero-left">
+ <text class="hero-title">{{ detail.bidDocName || "-" }}</text>
+ <text class="hero-sub">{{ detail.projectName || "-" }}</text>
+ </view>
+ <up-tag :text="detail.bidResult || '鏈~缁撴灉'"
+ :type="resultTagType(detail.bidResult)"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <view v-if="canStartContract"
+ class="contract-entry"
+ @click="startContract">
+ <up-icon name="plus-circle"
+ size="18"
+ color="#2979ff"></up-icon>
+ <text class="contract-entry-text">鍙戣捣鍚堝悓褰曞叆</text>
+ </view>
+
+ <u-cell-group title="椤圭洰淇℃伅"
+ class="detail-section">
+ <u-cell title="鎵�灞為」鐩�"
+ :value="detail.projectName || '-'" />
+ <u-cell title="鏍囦功鍚嶇О"
+ :value="detail.bidDocName || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鎷涙姇鏍囧崟浣嶄笌鏃ユ湡"
+ class="detail-section">
+ <u-cell title="鎷涙爣鍗曚綅"
+ :value="detail.tenderUnit || '-'" />
+ <u-cell title="鎶曟爣鍗曚綅"
+ :value="detail.bidUnit || '-'" />
+ <u-cell title="鎷涙爣鏃ユ湡"
+ :value="detail.tenderDate || '-'" />
+ <u-cell title="鎶曟爣鏃ユ湡"
+ :value="detail.bidDate || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鏍囦功娆句笌淇濊瘉閲�"
+ class="detail-section">
+ <u-cell title="鏍囦功娆鹃噾棰�"
+ :value="formatAmount(detail.bidDocFee)" />
+ <u-cell title="鏍囦功娆剧姸鎬�"
+ :value="detail.bidDocFeeStatus || '-'" />
+ <u-cell title="鐘舵�佸彉鏇存棩鏈�"
+ :value="detail.bidDocFeeStatusChangeDate || '-'" />
+ <u-cell title="鎶曟爣淇濊瘉閲�"
+ :value="formatAmount(detail.bidDeposit)" />
+ </u-cell-group>
+
+ <u-cell-group title="鎷涙姇鏍囩粨鏋�"
+ class="detail-section">
+ <u-cell title="鎷涙姇鏍囩粨鏋�"
+ :value="detail.bidResult || '-'" />
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { setPrefillProject } from "@/utils/prefill";
+ import { getBiddingDetail } from "@/api/salesManagement/biddingManagement";
+
+ // 闇�姹備笁.6锛氱粨鏋滃懡涓繖涓夌鏃跺彲鐩存帴鍙戣捣閿�鍞悎鍚屽綍鍏�
+ const CONTRACT_TRIGGER_RESULTS = [
+ "涓爣寰呭嚭閫氱煡涔�",
+ "宸插嚭涓爣閫氱煡涔�",
+ "宸茬鍚堝悓",
+ ];
+
+ const loading = ref(true);
+ const biddingId = ref("");
+ const detail = ref({});
+
+ const canStartContract = computed(() =>
+ CONTRACT_TRIGGER_RESULTS.includes(detail.value?.bidResult)
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("biddingDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/sales/biddingManagement/edit?id=${biddingId.value}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const resultTagType = result => {
+ if (result === "宸茬鍚堝悓") return "success";
+ if (result === "搴熸爣" || result === "娴佹爣" || result === "鏈腑鏍�") {
+ return "error";
+ }
+ if (result) return "warning";
+ return "info";
+ };
+
+ const startContract = () => {
+ setPrefillProject({
+ projectId: detail.value.projectId,
+ projectName: detail.value.projectName,
+ });
+ uni.setStorageSync("operationType", "add");
+ uni.removeStorageSync("editData");
+ uni.navigateTo({ url: "/pages/sales/salesAccount/detail" });
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const data = await loadDetailData({
+ cacheKey: "biddingDetail",
+ id: biddingId.value,
+ fetcher: getBiddingDetail,
+ });
+ detail.value = data || {};
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇鎷涙姇鏍囪鎯呭け璐�", icon: "none" });
+ detail.value = {};
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ biddingId.value = options?.id || "";
+ if (!biddingId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .bidding-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-left {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-width: 70%;
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ word-break: break-all;
+ }
+
+ .hero-sub {
+ font-size: 13px;
+ color: #606266;
+ word-break: break-all;
+ }
+
+ .contract-entry {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ margin-top: 12px;
+ padding: 11px 0;
+ border-radius: 10px;
+ border: 1px dashed #2979ff;
+ background: #f4f8ff;
+ }
+
+ .contract-entry-text {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/biddingManagement/edit.vue b/src/pages/sales/biddingManagement/edit.vue
new file mode 100644
index 0000000..d8315b2
--- /dev/null
+++ b/src/pages/sales/biddingManagement/edit.vue
@@ -0,0 +1,337 @@
+<template>
+ <view class="account-detail">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="椤圭洰淇℃伅"
+ class="form-section">
+ <up-form-item label="鎵�灞為」鐩�"
+ prop="projectId"
+ required
+ border-bottom>
+ <FormPicker v-model="form.projectId"
+ :display-text="form.projectName"
+ type="select"
+ title="閫夋嫨鎵�灞為」鐩�"
+ :options="projectOptions"
+ placeholder="璇烽�夋嫨鎵�灞為」鐩�"
+ @change="onProjectChange" />
+ </up-form-item>
+ <up-form-item label="鏍囦功鍚嶇О"
+ prop="bidDocName"
+ required
+ border-bottom>
+ <up-input v-model="form.bidDocName"
+ placeholder="璇疯緭鍏ユ爣涔﹀悕绉�"
+ clearable />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鎷涙姇鏍囧崟浣嶄笌鏃ユ湡"
+ class="form-section">
+ <up-form-item label="鎷涙爣鍗曚綅"
+ prop="tenderUnit"
+ required
+ border-bottom>
+ <up-input v-model="form.tenderUnit"
+ placeholder="璇疯緭鍏ユ嫑鏍囧崟浣�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎶曟爣鍗曚綅"
+ prop="bidUnit"
+ required
+ border-bottom>
+ <up-input v-model="form.bidUnit"
+ placeholder="璇疯緭鍏ユ姇鏍囧崟浣�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鎷涙爣鏃ユ湡"
+ prop="tenderDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.tenderDate"
+ type="date"
+ placeholder="璇烽�夋嫨鎷涙爣鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="鎶曟爣鏃ユ湡"
+ prop="bidDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.bidDate"
+ type="date"
+ placeholder="璇烽�夋嫨鎶曟爣鏃ユ湡" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鏍囦功娆句笌淇濊瘉閲�"
+ class="form-section">
+ <up-form-item label="鏍囦功娆鹃噾棰�"
+ prop="bidDocFee"
+ border-bottom>
+ <up-input v-model="form.bidDocFee"
+ type="number"
+ placeholder="璇疯緭鍏ユ爣涔︽閲戦"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鏍囦功娆剧姸鎬�"
+ prop="bidDocFeeStatus"
+ required
+ border-bottom>
+ <FormPicker v-model="form.bidDocFeeStatus"
+ type="select"
+ title="閫夋嫨鏍囦功娆剧姸鎬�"
+ :options="bidDocFeeStatusOptions"
+ placeholder="璇烽�夋嫨鏍囦功娆剧姸鎬�"
+ @change="onFeeStatusChange" />
+ </up-form-item>
+ <up-form-item label="鐘舵�佸彉鏇存棩鏈�"
+ prop="bidDocFeeStatusChangeDate"
+ border-bottom>
+ <FormPicker v-model="form.bidDocFeeStatusChangeDate"
+ type="date"
+ placeholder="鐘舵�佸彉鏇存椂鑷姩濉綋鍓嶆棩鏈�" />
+ </up-form-item>
+ <up-form-item label="鎶曟爣淇濊瘉閲�"
+ prop="bidDeposit"
+ border-bottom>
+ <up-input v-model="form.bidDeposit"
+ type="number"
+ placeholder="璇疯緭鍏ユ姇鏍囦繚璇侀噾閲戦"
+ clearable />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鎷涙姇鏍囩粨鏋�"
+ class="form-section">
+ <up-form-item label="鎷涙姇鏍囩粨鏋�"
+ prop="bidResult"
+ required
+ border-bottom>
+ <FormPicker v-model="form.bidResult"
+ type="select"
+ title="閫夋嫨鎷涙姇鏍囩粨鏋�"
+ :options="bidResultOptions"
+ placeholder="璇烽�夋嫨鎷涙姇鏍囩粨鏋�" />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </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 FormPicker from "@/components/FormPicker.vue";
+ import { formatDateToYMD } from "@/utils/ruoyi";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { loadProjectOptions, pickProjectName } from "@/utils/projectOptions";
+ import {
+ addOrUpdateBidding,
+ getBiddingDetail,
+ } from "@/api/salesManagement/biddingManagement";
+
+ const formRef = ref();
+ const loading = ref(false);
+ const biddingId = ref("");
+ const projectOptions = ref([]);
+
+ const bidDocFeeStatusOptions = [
+ { name: "鏈氦", value: "鏈氦" },
+ { name: "宸蹭氦", value: "宸蹭氦" },
+ { name: "宸查��", value: "宸查��" },
+ ];
+
+ const bidResultOptions = [
+ { name: "搴熸爣", value: "搴熸爣" },
+ { name: "娴佹爣", value: "娴佹爣" },
+ { name: "鏈腑鏍�", value: "鏈腑鏍�" },
+ { name: "涓爣寰呭嚭閫氱煡涔�", value: "涓爣寰呭嚭閫氱煡涔�" },
+ { name: "宸插嚭涓爣閫氱煡涔�", value: "宸插嚭涓爣閫氱煡涔�" },
+ { name: "宸茬鍚堝悓", value: "宸茬鍚堝悓" },
+ ];
+
+ const form = ref({
+ id: undefined,
+ projectId: "",
+ projectName: "",
+ bidDocName: "",
+ tenderUnit: "",
+ bidUnit: "",
+ tenderDate: "",
+ bidDate: "",
+ bidDocFee: "",
+ bidDocFeeStatus: "",
+ bidDocFeeStatusChangeDate: "",
+ bidDeposit: "",
+ bidResult: "",
+ });
+
+ const rules = {
+ projectId: [{ required: true, message: "璇烽�夋嫨鎵�灞為」鐩�", trigger: "change" }],
+ bidDocName: [{ required: true, message: "璇疯緭鍏ユ爣涔﹀悕绉�", trigger: "blur" }],
+ tenderUnit: [{ required: true, message: "璇疯緭鍏ユ嫑鏍囧崟浣�", trigger: "blur" }],
+ bidUnit: [{ required: true, message: "璇疯緭鍏ユ姇鏍囧崟浣�", trigger: "blur" }],
+ tenderDate: [{ required: true, message: "璇烽�夋嫨鎷涙爣鏃ユ湡", trigger: "change" }],
+ bidDate: [{ required: true, message: "璇烽�夋嫨鎶曟爣鏃ユ湡", trigger: "change" }],
+ bidDocFeeStatus: [
+ { required: true, message: "璇烽�夋嫨鏍囦功娆剧姸鎬�", trigger: "change" },
+ ],
+ bidResult: [{ required: true, message: "璇烽�夋嫨鎷涙姇鏍囩粨鏋�", trigger: "change" }],
+ };
+
+ const pageTitle = computed(() =>
+ biddingId.value ? "缂栬緫鎷涙姇鏍�" : "鏂板鎷涙姇鏍�"
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const numOrNull = value =>
+ value === "" || value === null || value === undefined
+ ? null
+ : Number.isNaN(Number(value))
+ ? null
+ : Number(value);
+
+ const onProjectChange = (value, item) => {
+ form.value.projectName = pickProjectName(item);
+ };
+
+ // 鏍囦功娆剧姸鎬佸彉鏇翠笖鏃ユ湡涓虹┖鏃惰嚜鍔ㄥ~褰撳ぉ
+ const onFeeStatusChange = () => {
+ if (form.value.bidDocFeeStatus && !form.value.bidDocFeeStatusChangeDate) {
+ form.value.bidDocFeeStatusChangeDate = formatDateToYMD(Date.now());
+ }
+ };
+
+ const loadOptions = async () => {
+ try {
+ projectOptions.value = await loadProjectOptions();
+ } catch (error) {
+ projectOptions.value = [];
+ }
+ };
+
+ const loadDetail = async () => {
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "biddingDetail",
+ id: biddingId.value,
+ fetcher: getBiddingDetail,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鏈壘鍒版嫑鎶曟爣淇℃伅", icon: "none" });
+ return;
+ }
+
+ form.value = {
+ id: data.id,
+ projectId: data.projectId ?? "",
+ projectName: data.projectName || "",
+ bidDocName: data.bidDocName || "",
+ tenderUnit: data.tenderUnit || "",
+ bidUnit: data.bidUnit || "",
+ tenderDate: (data.tenderDate || "").slice(0, 10),
+ bidDate: (data.bidDate || "").slice(0, 10),
+ bidDocFee: data.bidDocFee ?? "",
+ bidDocFeeStatus: data.bidDocFeeStatus || "",
+ bidDocFeeStatusChangeDate: (
+ data.bidDocFeeStatusChangeDate || ""
+ ).slice(0, 10),
+ bidDeposit: data.bidDeposit ?? "",
+ bidResult: data.bidResult || "",
+ };
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ const payload = {
+ id: form.value.id,
+ projectId: form.value.projectId,
+ projectName: form.value.projectName,
+ bidDocName: form.value.bidDocName,
+ tenderUnit: form.value.tenderUnit,
+ bidUnit: form.value.bidUnit,
+ tenderDate: form.value.tenderDate,
+ bidDate: form.value.bidDate,
+ bidDocFee: numOrNull(form.value.bidDocFee),
+ bidDocFeeStatus: form.value.bidDocFeeStatus,
+ bidDocFeeStatusChangeDate: form.value.bidDocFeeStatusChangeDate || null,
+ bidDeposit: numOrNull(form.value.bidDeposit),
+ bidResult: form.value.bidResult,
+ };
+
+ loading.value = true;
+ addOrUpdateBidding(payload)
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 1000);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ loadOptions();
+ if (options?.id) {
+ biddingId.value = options.id;
+ form.value.id = options.id;
+ loadDetail();
+ } else {
+ form.value.tenderDate = formatDateToYMD(Date.now());
+ form.value.bidDate = formatDateToYMD(Date.now());
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/biddingManagement/index.vue b/src/pages/sales/biddingManagement/index.vue
new file mode 100644
index 0000000..d0fc841
--- /dev/null
+++ b/src/pages/sales/biddingManagement/index.vue
@@ -0,0 +1,305 @@
+<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="query.projectName"
+ placeholder="璇疯緭鍏ラ」鐩悕绉版悳绱�"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="biddingList.length > 0"
+ class="ledger-list">
+ <view v-for="item in biddingList"
+ :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.bidDocName || "-" }}</text>
+ </view>
+ <up-tag :text="item.bidResult || '鏈~缁撴灉'"
+ :type="resultTagType(item.bidResult)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <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.tenderUnit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曟爣鍗曚綅</text>
+ <text class="detail-value">{{ item.bidUnit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎷涙爣鏃ユ湡</text>
+ <text class="detail-value">{{ item.tenderDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曟爣鏃ユ湡</text>
+ <text class="detail-value">{{ item.bidDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏍囦功娆�</text>
+ <text class="detail-value">{{ formatAmount(item.bidDocFee) }}锛坽{ item.bidDocFeeStatus || "-" }}锛�</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎶曟爣淇濊瘉閲�</text>
+ <text class="detail-value">{{ formatAmount(item.bidDeposit) }}</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"
+ @click="goDetail(item)">璇︽儏</up-button>
+ <up-button class="action-btn"
+ size="small"
+ type="error"
+ plain
+ @click="handleDelete(item)">鍒犻櫎</up-button>
+ </view>
+ <view v-if="canStartContract(item)"
+ class="contract-entry"
+ @click="startContract(item)">
+ <up-icon name="plus-circle"
+ size="16"
+ color="#2979ff"></up-icon>
+ <text class="contract-entry-text">鍙戣捣鍚堝悓褰曞叆</text>
+ </view>
+ </view>
+ </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>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鎷涙姇鏍囩瓫閫�"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ biddingListPage,
+ delBidding,
+ } from "@/api/salesManagement/biddingManagement";
+ import { unwrapList } from "@/utils/list";
+ import { setPrefillProject } from "@/utils/prefill";
+
+ // 闇�姹備笁.6锛氱粨鏋滃懡涓繖涓夌鏃跺彲鐩存帴鍙戣捣閿�鍞悎鍚屽綍鍏�
+ const CONTRACT_TRIGGER_RESULTS = [
+ "涓爣寰呭嚭閫氱煡涔�",
+ "宸插嚭涓爣閫氱煡涔�",
+ "宸茬鍚堝悓",
+ ];
+
+ const BID_RESULT_OPTIONS = [
+ { name: "搴熸爣", value: "搴熸爣" },
+ { name: "娴佹爣", value: "娴佹爣" },
+ { name: "鏈腑鏍�", value: "鏈腑鏍�" },
+ { name: "涓爣寰呭嚭閫氱煡涔�", value: "涓爣寰呭嚭閫氱煡涔�" },
+ { name: "宸插嚭涓爣閫氱煡涔�", value: "宸插嚭涓爣閫氱煡涔�" },
+ { name: "宸茬鍚堝悓", value: "宸茬鍚堝悓" },
+ ];
+
+ const biddingList = ref([]);
+ const showFilter = ref(false);
+
+ const query = reactive({
+ projectName: "",
+ tenderUnit: "",
+ bidResult: "",
+ bidDateRange: [],
+ });
+
+ const filterFields = [
+ {
+ prop: "projectName",
+ label: "椤圭洰鍚嶇О",
+ type: "input",
+ placeholder: "璇疯緭鍏ラ」鐩悕绉�",
+ },
+ {
+ prop: "tenderUnit",
+ label: "鎷涙爣鍗曚綅",
+ type: "input",
+ placeholder: "璇疯緭鍏ユ嫑鏍囧崟浣�",
+ },
+ {
+ prop: "bidResult",
+ label: "鎷涙姇鏍囩粨鏋�",
+ type: "select",
+ options: BID_RESULT_OPTIONS,
+ },
+ { prop: "bidDateRange", label: "鎶曟爣鏃ユ湡", type: "daterange" },
+ ];
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("biddingDetail");
+ uni.navigateTo({ url: "/pages/sales/biddingManagement/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("biddingDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/biddingManagement/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("biddingDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/biddingManagement/detail?id=${item.id}`,
+ });
+ };
+
+ const canStartContract = item =>
+ CONTRACT_TRIGGER_RESULTS.includes(item?.bidResult);
+
+ // 闇�姹備笁.6锛氫腑鏍囧悗鐩存帴鐢熸垚涓�鏉¢攢鍞彴璐︼紝甯﹂」鐩�/瀹㈡埛淇℃伅璺虫柊澧為〉
+ const startContract = item => {
+ setPrefillProject({
+ projectId: item.projectId,
+ projectName: item.projectName,
+ });
+ uni.setStorageSync("operationType", "add");
+ uni.removeStorageSync("editData");
+ uni.navigateTo({ url: "/pages/sales/salesAccount/detail" });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const resultTagType = result => {
+ if (result === "宸茬鍚堝悓") return "success";
+ if (result === "搴熸爣" || result === "娴佹爣" || result === "鏈腑鏍�") {
+ return "error";
+ }
+ if (result) return "warning";
+ return "info";
+ };
+
+ const getList = () => {
+ const [bidDateStart, bidDateEnd] = Array.isArray(query.bidDateRange)
+ ? query.bidDateRange
+ : [];
+ biddingListPage({
+ current: -1,
+ size: -1,
+ projectName: query.projectName || undefined,
+ tenderUnit: query.tenderUnit || undefined,
+ bidResult: query.bidResult || undefined,
+ bidDateStart: bidDateStart || undefined,
+ bidDateEnd: bidDateEnd || undefined,
+ })
+ .then(res => {
+ biddingList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ユ嫑鎶曟爣璁板綍鍚楋紵",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delBidding(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .contract-entry {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ margin: 0 12px 12px;
+ padding: 9px 0;
+ border-radius: 8px;
+ border: 1px dashed #2979ff;
+ background: #f4f8ff;
+ }
+
+ .contract-entry-text {
+ font-size: 13px;
+ color: #2979ff;
+ }
+</style>
diff --git a/src/pages/sales/contractManagement/detail.vue b/src/pages/sales/contractManagement/detail.vue
new file mode 100644
index 0000000..b6e5d2b
--- /dev/null
+++ b/src/pages/sales/contractManagement/detail.vue
@@ -0,0 +1,304 @@
+<template>
+ <view class="contract-detail">
+ <PageHeader title="鍚堝悓璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <text class="hero-title">{{ detail.contractNo || "-" }}</text>
+ <up-tag :text="detail.archiveStatus || '鏈~鐘舵��'"
+ :type="archiveTagType(detail.archiveStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <u-cell-group title="鍚堝悓涓绘。"
+ class="detail-section">
+ <u-cell title="鍚堝悓缂栧彿"
+ :value="detail.contractNo || '-'" />
+ <u-cell title="绛捐鏃堕棿"
+ :value="detail.signDate || '-'" />
+ <u-cell title="绛剧害鍗曚綅绠�绉�"
+ :value="detail.signingUnit || '-'" />
+ <u-cell title="绛剧害鍛ㄦ湡"
+ :value="detail.contractPeriod || '-'" />
+ <u-cell title="瀛樻。鐘舵��"
+ :value="detail.archiveStatus || '-'" />
+ <u-cell title="瀛樻。鎯呭喌璇存槑"
+ :value="detail.archiveRemark || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="浜у搧淇℃伅"
+ class="detail-section">
+ <view v-if="productList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤浜у搧鏄庣粏</text>
+ </view>
+ <view v-else
+ class="product-list">
+ <view v-for="(row, index) in productList"
+ :key="index"
+ class="product-card">
+ <view class="product-head">浜у搧 {{ index + 1 }}</view>
+ <view class="info-item">
+ <text class="info-label">浜у搧澶х被</text>
+ <text class="info-value">{{ row.productCategory || "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">瑙勬牸鍨嬪彿</text>
+ <text class="info-value">{{ row.specificationModel || "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鍚堝悓璁¢噺鍗曚綅</text>
+ <text class="info-value">{{ row.contractUnit || "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鍚◣鍗曚环</text>
+ <text class="info-value">{{ formatAmount(row.taxInclusiveUnitPrice) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">绾﹀畾/鏆備及鐢ㄩ噺</text>
+ <text class="info-value">{{ row.agreedQuantity ?? "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">宸插彂璐ф暟閲�</text>
+ <text class="info-value">{{ row.shippedQuantity ?? "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">浜у��</text>
+ <text class="info-value highlight">{{ formatAmount(row.outputValue) }}</text>
+ </view>
+ </view>
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="灞ョ害涓庤川淇�"
+ class="detail-section">
+ <u-cell title="灞ョ害淇濊瘉閲�"
+ :value="formatAmount(detail.performanceBond)" />
+ <u-cell title="璐ㄤ繚閲戞儏鍐�"
+ :value="formatAmount(detail.warrantyDeposit)" />
+ <u-cell title="浜у��"
+ :value="formatAmount(detail.outputValue)" />
+ </u-cell-group>
+
+ <u-cell-group title="鍥炴涓庣粨绠�"
+ class="detail-section">
+ <u-cell title="宸插洖娆鹃噾棰�"
+ :value="formatAmount(detail.receivedAmount)" />
+ <u-cell title="缁撶畻娆℃暟"
+ :value="detail.settlementCount ?? '-'" />
+ <u-cell title="搴旀敹閲戦"
+ :value="formatAmount(detail.receivableAmount)" />
+ <u-cell title="搴斾粯姣斾緥"
+ :value="detail.payableRatio || '-'" />
+ <u-cell title="鍚堝悓鑺傜偣搴旀敹"
+ :value="formatAmount(detail.nodeReceivable)" />
+ <u-cell title="鍚堝悓绾﹀畾浠樻鑺傜偣"
+ :value="detail.paymentNodes || '-'" />
+ <u-cell title="缁撶畻璁板綍"
+ :value="detail.settlementRecord || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="寮�绁ㄤ笌杩濈害"
+ class="detail-section">
+ <u-cell title="寮�绁ㄨ姹�"
+ :value="detail.invoiceRequirement || '-'" />
+ <u-cell title="鏈�杩戜竴娆″紑绁ㄦ椂闂�"
+ :value="detail.lastInvoiceDate || '-'" />
+ <u-cell title="宸插紑绁ㄩ噾棰�"
+ :value="formatAmount(detail.invoicedAmount)" />
+ <u-cell title="杩濈害閲戠害瀹�"
+ :value="detail.penaltyAgreement || '-'" />
+ <u-cell title="鎯呭喌璇存槑"
+ :value="detail.remark || '-'" />
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { getContractDetail } from "@/api/salesManagement/contractManagement";
+
+ const loading = ref(true);
+ const contractId = ref("");
+ const detail = ref({});
+
+ const productList = computed(() =>
+ Array.isArray(detail.value.productList) ? detail.value.productList : []
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("contractDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/sales/contractManagement/edit?id=${contractId.value}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const archiveTagType = status => {
+ if (status === "宸插瓨妗�") return "success";
+ if (status === "宸茬敤绔犳湭瀵勫洖") return "warning";
+ return "info";
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const data = await loadDetailData({
+ cacheKey: "contractDetail",
+ id: contractId.value,
+ fetcher: getContractDetail,
+ });
+ detail.value = data || {};
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇鍚堝悓璇︽儏澶辫触", icon: "none" });
+ detail.value = {};
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ contractId.value = options?.id || "";
+ if (!contractId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .contract-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .empty-text {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .product-list {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .product-card {
+ background: #f9fafc;
+ border-radius: 10px;
+ overflow: hidden;
+ }
+
+ .product-head {
+ padding: 12px 16px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ border-bottom: 1px solid #eef2f7;
+ }
+
+ .info-item {
+ display: flex;
+ padding: 10px 16px;
+ border-bottom: 1px solid #f8f8f8;
+ }
+
+ .info-item:last-child {
+ border-bottom: none;
+ }
+
+ .info-label {
+ width: 120px;
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .info-value {
+ flex: 1;
+ font-size: 14px;
+ color: #303133;
+ text-align: right;
+ word-break: break-all;
+ }
+
+ .highlight {
+ color: #2979ff;
+ font-weight: 600;
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/contractManagement/edit.vue b/src/pages/sales/contractManagement/edit.vue
new file mode 100644
index 0000000..9ee981b
--- /dev/null
+++ b/src/pages/sales/contractManagement/edit.vue
@@ -0,0 +1,628 @@
+<template>
+ <view class="account-detail">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="鍚堝悓涓绘。"
+ class="form-section">
+ <up-form-item label="鍚堝悓缂栧彿"
+ prop="contractNo"
+ required>
+ <up-input v-model="form.contractNo"
+ placeholder="璇疯緭鍏ュ悎鍚岀紪鍙�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绛捐鏃堕棿"
+ prop="signDate"
+ required>
+ <FormPicker v-model="form.signDate"
+ type="date"
+ placeholder="璇烽�夋嫨绛捐鏃堕棿" />
+ </up-form-item>
+ <up-form-item label="绛剧害鍗曚綅绠�绉�"
+ prop="signingUnit"
+ required>
+ <up-input v-model="form.signingUnit"
+ placeholder="璇疯緭鍏ョ绾﹀崟浣嶇畝绉�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绛剧害鍛ㄦ湡"
+ prop="contractPeriod">
+ <up-input v-model="form.contractPeriod"
+ placeholder="璇疯緭鍏ョ绾﹀懆鏈�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="瀛樻。鐘舵��"
+ prop="archiveStatus"
+ required>
+ <FormPicker v-model="form.archiveStatus"
+ type="select"
+ title="閫夋嫨瀛樻。鐘舵��"
+ :options="ARCHIVE_STATUS_OPTIONS"
+ placeholder="璇烽�夋嫨瀛樻。鐘舵��" />
+ </up-form-item>
+ <up-form-item label="瀛樻。鎯呭喌璇存槑"
+ prop="archiveRemark">
+ <up-input v-model="form.archiveRemark"
+ placeholder="璇疯緭鍏ュ瓨妗f儏鍐佃鏄�"
+ clearable />
+ </up-form-item>
+ </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="productList.length === 0"
+ class="empty-text">
+ <text>鏆傛棤浜у搧锛岃鍏堟坊鍔犱骇鍝�</text>
+ </view>
+ <view v-else
+ class="product-list">
+ <view v-for="(row, index) in productList"
+ :key="row.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>
+ </view>
+ <up-divider></up-divider>
+ <view class="product-body">
+ <up-form-item label="浜у搧澶х被">
+ <up-input v-model="row.productCategory"
+ placeholder="璇烽�夋嫨浜у搧澶х被"
+ readonly
+ @click="openCategoryPicker(index)" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="openCategoryPicker(index)"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="瑙勬牸鍨嬪彿">
+ <up-input v-model="row.specificationModel"
+ placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
+ readonly
+ @click="openModelPicker(index)" />
+ <template #right>
+ <up-icon name="arrow-right"
+ @click="openModelPicker(index)"></up-icon>
+ </template>
+ </up-form-item>
+ <up-form-item label="鍚堝悓璁¢噺鍗曚綅">
+ <up-input v-model="row.contractUnit"
+ placeholder="璇疯緭鍏ヨ閲忓崟浣�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍚◣鍗曚环">
+ <up-input v-model="row.taxInclusiveUnitPrice"
+ type="number"
+ placeholder="璇疯緭鍏ュ惈绋庡崟浠�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绾﹀畾/鏆備及鐢ㄩ噺">
+ <up-input v-model="row.agreedQuantity"
+ type="number"
+ placeholder="璇疯緭鍏ョ害瀹氱敤閲�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="宸插彂璐ф暟閲�">
+ <up-input v-model="row.shippedQuantity"
+ type="number"
+ placeholder="璇疯緭鍏ュ凡鍙戣揣鏁伴噺"
+ clearable />
+ </up-form-item>
+ <up-form-item label="浜у��">
+ <up-input :model-value="formatAmount(productOutputValue(row))"
+ disabled
+ placeholder="鑷姩璁$畻" />
+ </up-form-item>
+ </view>
+ </view>
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="灞ョ害涓庤川淇�"
+ class="form-section">
+ <up-form-item label="灞ョ害淇濊瘉閲�">
+ <up-input v-model="form.performanceBond"
+ type="number"
+ placeholder="璇疯緭鍏ュ饱绾︿繚璇侀噾"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璐ㄤ繚閲戞儏鍐�">
+ <up-input v-model="form.warrantyDeposit"
+ type="number"
+ placeholder="璇疯緭鍏ヨ川淇濋噾"
+ clearable />
+ </up-form-item>
+ <up-form-item label="浜у��">
+ <up-input :model-value="formatAmount(computedOutputValue)"
+ disabled
+ placeholder="鑷姩姹囨��" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鍥炴涓庣粨绠�"
+ class="form-section">
+ <up-form-item label="宸插洖娆鹃噾棰�">
+ <up-input v-model="form.receivedAmount"
+ type="number"
+ placeholder="璇疯緭鍏ュ凡鍥炴閲戦"
+ clearable />
+ </up-form-item>
+ <up-form-item label="缁撶畻娆℃暟">
+ <up-input v-model="form.settlementCount"
+ type="number"
+ placeholder="璇疯緭鍏ョ粨绠楁鏁�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="搴旀敹閲戦">
+ <up-input v-model="form.receivableAmount"
+ type="number"
+ placeholder="璇疯緭鍏ュ簲鏀堕噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="搴斾粯姣斾緥">
+ <up-input v-model="form.payableRatio"
+ placeholder="渚嬪锛�30% 鎴� 0.3"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鍚堝悓鑺傜偣搴旀敹">
+ <up-input v-model="form.nodeReceivable"
+ type="number"
+ placeholder="璇疯緭鍏ュ悎鍚岃妭鐐瑰簲鏀�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绾﹀畾浠樻鑺傜偣">
+ <up-input v-model="form.paymentNodes"
+ placeholder="璇疯緭鍏ュ悎鍚岀害瀹氫粯娆捐妭鐐�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="缁撶畻璁板綍">
+ <up-textarea v-model="form.settlementRecord"
+ placeholder="璇疯緭鍏ョ粨绠楄褰�"
+ auto-height />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="寮�绁ㄤ笌杩濈害"
+ class="form-section">
+ <up-form-item label="寮�绁ㄨ姹�">
+ <up-input v-model="form.invoiceRequirement"
+ placeholder="璇疯緭鍏ュ紑绁ㄨ姹�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鏈�杩戝紑绁ㄦ椂闂�">
+ <FormPicker v-model="form.lastInvoiceDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏈�杩戜竴娆″紑绁ㄦ椂闂�" />
+ </up-form-item>
+ <up-form-item label="宸插紑绁ㄩ噾棰�">
+ <up-input v-model="form.invoicedAmount"
+ type="number"
+ placeholder="璇疯緭鍏ュ凡寮�绁ㄩ噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="杩濈害閲戠害瀹�">
+ <up-textarea v-model="form.penaltyAgreement"
+ placeholder="璇疯緭鍏ヨ繚绾﹂噾绾﹀畾"
+ auto-height />
+ </up-form-item>
+ <up-form-item label="鎯呭喌璇存槑">
+ <up-textarea v-model="form.remark"
+ placeholder="璇疯緭鍏ユ儏鍐佃鏄�"
+ auto-height />
+ </up-form-item>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+
+ <up-action-sheet :show="showCategorySheet"
+ title="閫夋嫨浜у搧澶х被"
+ :actions="categoryActions"
+ @select="onSelectCategory"
+ @close="showCategorySheet = false" />
+ <up-action-sheet :show="showModelSheet"
+ title="閫夋嫨瑙勬牸鍨嬪彿"
+ :actions="modelActions"
+ @select="onSelectModel"
+ @close="showModelSheet = false" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, onMounted, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FormPicker from "@/components/FormPicker.vue";
+ import { modelList, productTreeList } from "@/api/basicData/product";
+ import { loadDetailData } from "@/utils/detailCache";
+ import {
+ addOrUpdateContract,
+ getContractDetail,
+ } from "@/api/salesManagement/contractManagement";
+
+ const ARCHIVE_STATUS_OPTIONS = [
+ { name: "宸插瓨妗�", value: "宸插瓨妗�" },
+ { name: "宸茬敤绔犳湭瀵勫洖", value: "宸茬敤绔犳湭瀵勫洖" },
+ { name: "鏈绾�", value: "鏈绾�" },
+ ];
+
+ const formRef = ref();
+ const loading = ref(false);
+ const contractId = ref("");
+ const productList = ref([]);
+ const categoryOptions = ref([]);
+ const categoryActions = ref([]);
+ const modelActions = ref([]);
+ const showCategorySheet = ref(false);
+ const showModelSheet = ref(false);
+ const currentProductIndex = ref(-1);
+
+ let uidSeed = 1;
+
+ const form = ref({
+ id: undefined,
+ contractNo: "",
+ signDate: "",
+ signingUnit: "",
+ contractPeriod: "",
+ archiveStatus: "",
+ archiveRemark: "",
+ performanceBond: undefined,
+ warrantyDeposit: undefined,
+ outputValue: undefined,
+ receivedAmount: undefined,
+ settlementCount: undefined,
+ receivableAmount: undefined,
+ payableRatio: "",
+ nodeReceivable: undefined,
+ paymentNodes: "",
+ settlementRecord: "",
+ invoiceRequirement: "",
+ lastInvoiceDate: "",
+ invoicedAmount: undefined,
+ penaltyAgreement: "",
+ remark: "",
+ });
+
+ const rules = {
+ contractNo: [{ required: true, message: "璇疯緭鍏ュ悎鍚岀紪鍙�", trigger: "blur" }],
+ signDate: [{ required: true, message: "璇烽�夋嫨绛捐鏃堕棿", trigger: "change" }],
+ signingUnit: [
+ { required: true, message: "璇疯緭鍏ョ绾﹀崟浣嶇畝绉�", trigger: "blur" },
+ ],
+ archiveStatus: [
+ { required: true, message: "璇烽�夋嫨瀛樻。鐘舵��", trigger: "change" },
+ ],
+ };
+
+ const pageTitle = computed(() => (contractId.value ? "缂栬緫鍚堝悓" : "鏂板鍚堝悓"));
+
+ const productOutputValue = row =>
+ Number(
+ (
+ Number(row?.taxInclusiveUnitPrice || 0) *
+ Number(row?.shippedQuantity || 0)
+ ).toFixed(2)
+ );
+
+ const computedOutputValue = computed(() =>
+ Number(
+ productList.value
+ .reduce((sum, row) => sum + productOutputValue(row), 0)
+ .toFixed(2)
+ )
+ );
+
+ const formatAmount = amount => `楼${Number(amount || 0).toFixed(2)}`;
+ const goBack = () => uni.navigateBack();
+
+ const flattenProductTree = nodes => {
+ const result = [];
+ const walk = list => {
+ (list || []).forEach(item => {
+ if (item.children && item.children.length) {
+ walk(item.children);
+ } else {
+ result.push({
+ label: item.label || item.productName || "",
+ value: item.id || item.value,
+ });
+ }
+ });
+ };
+ walk(nodes);
+ return result;
+ };
+
+ 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 createEmptyProduct = () => ({
+ uid: `p_${uidSeed++}`,
+ categoryId: "",
+ productCategory: "",
+ productModelId: "",
+ specificationModel: "",
+ contractUnit: "",
+ taxInclusiveUnitPrice: undefined,
+ agreedQuantity: undefined,
+ shippedQuantity: undefined,
+ modelOptions: [],
+ });
+
+ const addProduct = () => productList.value.push(createEmptyProduct());
+
+ const removeProduct = index => {
+ productList.value.splice(index, 1);
+ };
+
+ const fetchBaseOptions = async () => {
+ try {
+ const res = await productTreeList();
+ const nodes = Array.isArray(res) ? res : res?.data || [];
+ categoryOptions.value = flattenProductTree(nodes);
+ } catch (error) {
+ categoryOptions.value = [];
+ }
+ };
+
+ const fetchModelOptions = async (categoryId, row) => {
+ try {
+ const res = await modelList({ id: categoryId });
+ const rows = res?.data?.records || res?.data || res?.records || res || [];
+ row.modelOptions = Array.isArray(rows) ? rows : [];
+ } catch (error) {
+ row.modelOptions = [];
+ }
+ };
+
+ const openCategoryPicker = index => {
+ currentProductIndex.value = index;
+ categoryActions.value = categoryOptions.value.map(item => ({
+ name: item.label,
+ value: item.value,
+ label: item.label,
+ }));
+ showCategorySheet.value = true;
+ };
+
+ const openModelPicker = index => {
+ currentProductIndex.value = index;
+ const row = productList.value[index];
+ if (!row?.categoryId) {
+ uni.showToast({ title: "璇峰厛閫夋嫨浜у搧澶х被", icon: "none" });
+ return;
+ }
+ modelActions.value = (row.modelOptions || []).map(item => ({
+ name: item.model || item.specification,
+ value: item.id,
+ unit: item.unit,
+ }));
+ if (!modelActions.value.length) {
+ uni.showToast({ title: "鏆傛棤瑙勬牸鍨嬪彿", icon: "none" });
+ return;
+ }
+ showModelSheet.value = true;
+ };
+
+ const onSelectCategory = action => {
+ const row = productList.value[currentProductIndex.value];
+ showCategorySheet.value = false;
+ if (!row) return;
+ row.categoryId = action.value;
+ row.productCategory = action.label;
+ row.productModelId = "";
+ row.specificationModel = "";
+ row.contractUnit = "";
+ row.modelOptions = [];
+ fetchModelOptions(action.value, row);
+ };
+
+ const onSelectModel = action => {
+ const row = productList.value[currentProductIndex.value];
+ showModelSheet.value = false;
+ if (!row) return;
+ row.productModelId = action.value;
+ row.specificationModel = action.name;
+ row.contractUnit = action.unit || row.contractUnit;
+ };
+
+ const normalizeProductRows = async rows => {
+ const normalized = await Promise.all(
+ (Array.isArray(rows) ? rows : []).map(async item => {
+ const row = {
+ uid: `p_${uidSeed++}`,
+ categoryId:
+ item.categoryId ||
+ findNodeIdByLabel(categoryOptions.value, item.productCategory) ||
+ "",
+ productCategory: item.productCategory || "",
+ productModelId: item.productModelId || "",
+ specificationModel: item.specificationModel || "",
+ contractUnit: item.contractUnit || "",
+ taxInclusiveUnitPrice: item.taxInclusiveUnitPrice,
+ agreedQuantity: item.agreedQuantity,
+ shippedQuantity: item.shippedQuantity,
+ modelOptions: [],
+ };
+ if (row.categoryId) {
+ await fetchModelOptions(row.categoryId, row);
+ // 浠呭瓨浜嗗瀷鍙峰悕绉版椂锛屼粠鍨嬪彿鍒楄〃鍙嶆煡 id锛屼繚璇侀�夋嫨鍣ㄨ兘鍙嶆樉
+ if (!row.productModelId && row.specificationModel) {
+ const matched = row.modelOptions.find(
+ m => (m.model || m.specification) === row.specificationModel
+ );
+ if (matched) row.productModelId = matched.id;
+ }
+ }
+ return row;
+ })
+ );
+ productList.value = normalized;
+ };
+
+ const loadDetail = async () => {
+ if (!contractId.value) return;
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "contractDetail",
+ id: contractId.value,
+ fetcher: getContractDetail,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鑾峰彇鍚堝悓璇︽儏澶辫触", icon: "none" });
+ return;
+ }
+ const { productList: rows, ...rest } = data;
+ form.value = { ...form.value, ...rest, id: data.id };
+ await normalizeProductRows(rows);
+ };
+
+ const buildPayload = () => ({
+ ...form.value,
+ outputValue: computedOutputValue.value,
+ productList: productList.value.map(row => ({
+ productCategory: row.productCategory,
+ specificationModel: row.specificationModel,
+ productModelId: row.productModelId,
+ contractUnit: row.contractUnit,
+ taxInclusiveUnitPrice: Number(row.taxInclusiveUnitPrice || 0),
+ agreedQuantity: Number(row.agreedQuantity || 0),
+ shippedQuantity: Number(row.shippedQuantity || 0),
+ outputValue: productOutputValue(row),
+ })),
+ });
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ loading.value = true;
+ addOrUpdateContract(buildPayload())
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 300);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ contractId.value = options.id;
+ form.value.id = options.id;
+ }
+ });
+
+ onMounted(async () => {
+ await fetchBaseOptions();
+ if (contractId.value) await loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .section-tools {
+ display: flex;
+ justify-content: flex-end;
+ padding: 12px 12px 0;
+ }
+
+ .product-list {
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .product-card {
+ background: #fff;
+ border-radius: 12px;
+ padding: 0 12px 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .product-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 0;
+ }
+
+ .product-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .empty-text {
+ padding: 16px 12px;
+ color: #999;
+ font-size: 14px;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/contractManagement/index.vue b/src/pages/sales/contractManagement/index.vue
new file mode 100644
index 0000000..398c606
--- /dev/null
+++ b/src/pages/sales/contractManagement/index.vue
@@ -0,0 +1,279 @@
+<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="query.contractNo"
+ placeholder="璇疯緭鍏ュ悎鍚岀紪鍙锋悳绱�"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="contractList.length > 0"
+ class="ledger-list">
+ <view v-for="item in contractList"
+ :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.contractNo || "-" }}</text>
+ </view>
+ <up-tag :text="item.archiveStatus || '鏈~鐘舵��'"
+ :type="archiveTagType(item.archiveStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">绛剧害鏃ユ湡</text>
+ <text class="detail-value">{{ item.signDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绛剧害鍗曚綅</text>
+ <text class="detail-value">{{ item.signingUnit || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绛剧害鍛ㄦ湡</text>
+ <text class="detail-value">{{ item.contractPeriod || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">浜у��</text>
+ <text class="detail-value highlight">{{ formatAmount(item.outputValue) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸叉敹娆�</text>
+ <text class="detail-value">{{ formatAmount(item.receivedAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴旀敹閲戦</text>
+ <text class="detail-value">{{ formatAmount(item.receivableAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">灞ョ害淇濊瘉閲�</text>
+ <text class="detail-value">{{ formatAmount(item.performanceBond) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璐ㄤ繚閲�</text>
+ <text class="detail-value">{{ formatAmount(item.warrantyDeposit) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">缁撶畻鍗曟暟閲�</text>
+ <text class="detail-value">{{ item.settlementCount ?? "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴旀敹娆炬瘮渚�</text>
+ <text class="detail-value">{{ item.payableRatio || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏈�杩戝紑绁ㄦ棩鏈�</text>
+ <text class="detail-value">{{ item.lastInvoiceDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸插紑绁ㄩ噾棰�</text>
+ <text class="detail-value">{{ formatAmount(item.invoicedAmount) }}</text>
+ </view>
+ <view class="detail-row"
+ v-if="item.remark">
+ <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"
+ @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">
+ <text>鏆傛棤鍚堝悓鏁版嵁</text>
+ </view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鍚堝悓绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ contractListPage,
+ delContract,
+ } from "@/api/salesManagement/contractManagement";
+ import { unwrapList } from "@/utils/list";
+
+ const ARCHIVE_STATUS_OPTIONS = [
+ { name: "宸插瓨妗�", value: "宸插瓨妗�" },
+ { name: "宸茬敤绔犳湭瀵勫洖", value: "宸茬敤绔犳湭瀵勫洖" },
+ { name: "鏈绾�", value: "鏈绾�" },
+ ];
+
+ const contractList = ref([]);
+ const showFilter = ref(false);
+
+ const query = reactive({
+ contractNo: "",
+ signingUnit: "",
+ archiveStatus: "",
+ signDateRange: [],
+ });
+
+ const filterFields = [
+ {
+ prop: "contractNo",
+ label: "鍚堝悓缂栧彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ュ悎鍚岀紪鍙�",
+ },
+ {
+ prop: "signingUnit",
+ label: "绛剧害鍗曚綅",
+ type: "input",
+ placeholder: "璇疯緭鍏ョ绾﹀崟浣�",
+ },
+ {
+ prop: "archiveStatus",
+ label: "瀛樻。鐘舵��",
+ type: "select",
+ options: ARCHIVE_STATUS_OPTIONS,
+ },
+ { prop: "signDateRange", label: "绛剧害鏃ユ湡", type: "daterange" },
+ ];
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("contractDetail");
+ uni.navigateTo({ url: "/pages/sales/contractManagement/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("contractDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/contractManagement/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("contractDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/contractManagement/detail?id=${item.id}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const archiveTagType = status => {
+ if (status === "宸插瓨妗�") return "success";
+ if (status === "宸茬敤绔犳湭瀵勫洖") return "warning";
+ if (status === "鏈绾�") return "info";
+ return "info";
+ };
+
+ const getList = () => {
+ const [signDateStart, signDateEnd] = Array.isArray(query.signDateRange)
+ ? query.signDateRange
+ : [];
+ contractListPage({
+ current: -1,
+ size: -1,
+ contractNo: query.contractNo || undefined,
+ signingUnit: query.signingUnit || undefined,
+ archiveStatus: query.archiveStatus || undefined,
+ signDateStart: signDateStart || undefined,
+ signDateEnd: signDateEnd || undefined,
+ })
+ .then(res => {
+ contractList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ュ悎鍚屽悧锛�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delContract(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/sales/deliveryLedger/detail.vue b/src/pages/sales/deliveryLedger/detail.vue
new file mode 100644
index 0000000..4679109
--- /dev/null
+++ b/src/pages/sales/deliveryLedger/detail.vue
@@ -0,0 +1,405 @@
+<template>
+ <view class="delivery-detail">
+ <PageHeader title="鍙戣揣璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text v-if="!locked"
+ class="header-action"
+ @click="goEdit">琛ュ厖淇℃伅</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <view class="hero-left">
+ <text class="hero-title">{{ detail.shippingNo || detail.salesContractNo || "-" }}</text>
+ <text class="hero-sub">{{ detail.projectName || "-" }}</text>
+ </view>
+ <up-tag :text="statusText(detail.status)"
+ :type="statusTagType(detail.status)"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <view v-if="locked"
+ class="lock-banner">
+ <text class="lock-banner-text">褰撳墠鐘舵�佸凡杩涘叆姝e紡娴佺▼锛屽彂璐т俊鎭笉鍙慨鏀�</text>
+ </view>
+
+ <u-cell-group title="鍚堝悓涓庡鎴�"
+ class="detail-section">
+ <u-cell title="閿�鍞悎鍚屽彿"
+ :value="detail.salesContractNo || '-'" />
+ <u-cell title="鍙戣揣鍗曞彿"
+ :value="detail.shippingNo || '-'" />
+ <u-cell title="鎵�灞為」鐩�"
+ :value="detail.projectName || '-'" />
+ <u-cell title="瀹㈡埛鍚嶇О"
+ :value="detail.customerName || '-'" />
+ <u-cell title="浜у搧鍚嶇О"
+ :value="detail.productName || '-'" />
+ <u-cell title="瑙勬牸鍨嬪彿"
+ :value="detail.specificationModel || '-'" />
+ <u-cell title="鍙戣揣鏁伴噺"
+ :value="detail.totalQuantity != null ? String(detail.totalQuantity) : '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鍙戣揣淇℃伅"
+ class="detail-section">
+ <u-cell title="鍙戣揣绫诲瀷"
+ :value="detail.type || '-'" />
+ <u-cell title="鍙戣揣鏃ユ湡"
+ :value="detail.shippingDate || '-'" />
+ <u-cell title="鍙戣揣杞︾墝鍙�"
+ :value="detail.shippingCarNumber || '-'" />
+ <u-cell title="蹇�掑叕鍙�"
+ :value="detail.expressCompany || '-'" />
+ <u-cell title="蹇�掑崟鍙�"
+ :value="detail.expressNumber || '-'" />
+ <u-cell title="鍑哄簱鎵规"
+ :value="detail.outboundBatches || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鍗曟嵁涓庡綊妗�"
+ class="detail-section">
+ <u-cell title="鍙戣揣鍗曟嵁"
+ :value="detail.shippingDocument || '-'" />
+ <u-cell title="鏄惁瀵硅处"
+ :value="yesNo(detail.isReconciled)" />
+ <u-cell v-if="Number(detail.isReconciled) === 1"
+ title="瀵硅处鍗曞綊妗f儏鍐�"
+ :value="detail.statementArchivingStatus || '-'" />
+ <u-cell title="鏄惁缁撶畻"
+ :value="yesNo(detail.isSettled)" />
+ <u-cell v-if="Number(detail.isSettled) === 1"
+ title="缁撶畻鍗曞綊妗f儏鍐�"
+ :value="detail.settlementArchivingStatus || '-'" />
+ <u-cell title="鏄惁宸插紑绁�"
+ :value="yesNo(detail.isInvoiced)" />
+ </u-cell-group>
+
+ <u-cell-group title="鍙戣揣浜у搧鏄庣粏"
+ class="detail-section">
+ <view v-if="products.length === 0"
+ class="empty-block">
+ <text>鏆傛棤浜у搧鏄庣粏</text>
+ </view>
+ <view v-for="(row, index) in products"
+ :key="index"
+ class="product-row">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value">{{ row.productName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ row.specificationModel || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵规鍙�</text>
+ <text class="detail-value">{{ row.batchNo || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙戣揣鏁伴噺</text>
+ <text class="detail-value">
+ {{ row.deliveryQuantity === "" ? "-" : row.deliveryQuantity }}{{ row.unit ? ` ${row.unit}` : "" }}
+ </text>
+ </view>
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="鍙戣揣鍥剧墖"
+ class="detail-section">
+ <view v-if="files.length === 0"
+ class="empty-block">
+ <text>鏆傛棤鍥剧墖</text>
+ </view>
+ <view v-else
+ class="file-list">
+ <view v-for="(file, index) in files"
+ :key="index"
+ class="file-item"
+ @click="previewFile(file)">
+ <up-icon name="photo"
+ size="18"
+ color="#2979ff"></up-icon>
+ <text class="file-name">{{ fileLabel(file, index) }}</text>
+ <up-icon name="download"
+ size="16"
+ color="#909399"></up-icon>
+ </view>
+ </view>
+ </u-cell-group>
+
+ <u-cell-group title="閫�璐у崟"
+ class="detail-section">
+ <view v-if="returnNos.length === 0"
+ class="empty-block">
+ <text>鏆傛棤閫�璐у崟</text>
+ </view>
+ <view v-for="(no, index) in returnNos"
+ :key="index"
+ class="file-item">
+ <up-icon name="arrow-return-left"
+ size="18"
+ color="#ff9900"></up-icon>
+ <text class="file-name">{{ no }}</text>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { loadDetailData } from "@/utils/detailCache";
+ import {
+ resolveDeliveryFiles,
+ resolveDeliveryProducts,
+ resolveReturnNos,
+ } from "@/utils/delivery";
+ import { getDeliveryDetail } from "@/api/salesManagement/deliveryLedger";
+
+ // 杩欎袱绉嶇姸鎬佸凡杩涘叆姝e紡娴佺▼锛屼笉鍏佽鍐嶆敼鍙戣揣淇℃伅
+ const LOCKED_STATUS = ["瀹℃牳閫氳繃", "宸插彂璐�"];
+
+ const STATUS_TEXT = {
+ 0: "寰呭鏍�",
+ 1: "瀹℃牳涓�",
+ 2: "瀹℃牳鎷掔粷",
+ 3: "瀹℃牳閫氳繃",
+ };
+
+ const loading = ref(true);
+ const deliveryId = ref("");
+ const detail = ref({});
+
+ const products = computed(() => resolveDeliveryProducts(detail.value));
+ const files = computed(() => resolveDeliveryFiles(detail.value));
+ const returnNos = computed(() => resolveReturnNos(detail.value.returnNoList));
+ const locked = computed(() => LOCKED_STATUS.includes(detail.value?.status));
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ if (locked.value) {
+ uni.showToast({ title: "璇ョ姸鎬佺殑鏁版嵁涓嶅彲缂栬緫鍙戣揣淇℃伅", icon: "none" });
+ return;
+ }
+ uni.setStorageSync("deliveryDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/sales/deliveryLedger/edit?id=${deliveryId.value}`,
+ });
+ };
+
+ const statusText = status => {
+ if (status === null || status === undefined || status === "") return "-";
+ return STATUS_TEXT[status] || String(status);
+ };
+
+ const statusTagType = status => {
+ if (status === "宸插彂璐�" || status === 3 || status === "3") return "success";
+ if (status === 1 || status === "1") return "warning";
+ if (status === 2 || status === "2") return "error";
+ return "info";
+ };
+
+ const yesNo = value => {
+ if (value === null || value === undefined || value === "") return "-";
+ return Number(value) === 1 ? "鏄�" : "鍚�";
+ };
+
+ const fileLabel = (file, index) =>
+ file.name || file.originalFilename || `鍥剧墖${index + 1}`;
+
+ // request.ts 鏈�忎紶 responseType锛宐lob 涓嬭浇涓嶅彲鐢紝鍙兘鐢� downloadFile + openDocument
+ const previewFile = file => {
+ const url = file.url || file.previewURL;
+ if (!url) return;
+ uni.showLoading({ title: "鎵撳紑涓�...", mask: true });
+ uni.downloadFile({
+ url,
+ success: res => {
+ if (res.statusCode !== 200) {
+ uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" });
+ return;
+ }
+ uni.openDocument({
+ filePath: res.tempFilePath,
+ showMenu: true,
+ fail: () => uni.showToast({ title: "鏆備笉鏀寔棰勮璇ユ枃浠�", icon: "none" }),
+ });
+ },
+ fail: () => uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" }),
+ complete: () => uni.hideLoading(),
+ });
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const data = await loadDetailData({
+ cacheKey: "deliveryDetail",
+ id: deliveryId.value,
+ fetcher: getDeliveryDetail,
+ });
+ detail.value = data || {};
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇鍙戣揣璇︽儏澶辫触", icon: "none" });
+ detail.value = {};
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ deliveryId.value = options?.id || "";
+ if (!deliveryId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .delivery-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08);
+ }
+
+ .hero-left {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-width: 70%;
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ word-break: break-all;
+ }
+
+ .hero-sub {
+ font-size: 13px;
+ color: #606266;
+ word-break: break-all;
+ }
+
+ .lock-banner {
+ margin-top: 12px;
+ padding: 10px 14px;
+ border-radius: 10px;
+ background: #f4f4f5;
+ }
+
+ .lock-banner-text {
+ font-size: 13px;
+ color: #909399;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .empty-block {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .product-row {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .product-row:last-child {
+ border-bottom: none;
+ }
+
+ .detail-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 4px 0;
+ }
+
+ .detail-label {
+ font-size: 13px;
+ color: #909399;
+ }
+
+ .detail-value {
+ font-size: 13px;
+ color: #333;
+ max-width: 70%;
+ text-align: right;
+ word-break: break-all;
+ }
+
+ .file-list {
+ padding: 6px 16px 12px;
+ }
+
+ .file-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 16px;
+ }
+
+ .file-name {
+ flex: 1;
+ font-size: 14px;
+ color: #333;
+ word-break: break-all;
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/deliveryLedger/edit.vue b/src/pages/sales/deliveryLedger/edit.vue
new file mode 100644
index 0000000..ad63b0b
--- /dev/null
+++ b/src/pages/sales/deliveryLedger/edit.vue
@@ -0,0 +1,385 @@
+<template>
+ <view class="account-detail">
+ <PageHeader title="琛ュ厖鍙戣揣淇℃伅"
+ @back="goBack" />
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="鍙戣揣淇℃伅"
+ class="form-section">
+ <up-form-item label="鍙戣揣绫诲瀷"
+ prop="type"
+ required
+ border-bottom>
+ <FormPicker v-model="form.type"
+ type="select"
+ title="閫夋嫨鍙戣揣绫诲瀷"
+ :options="typeOptions"
+ placeholder="璇烽�夋嫨鍙戣揣绫诲瀷"
+ @change="onTypeChange" />
+ </up-form-item>
+ <up-form-item label="鍙戣揣鏃ユ湡"
+ prop="shippingDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.shippingDate"
+ type="date"
+ placeholder="璇烽�夋嫨鍙戣揣鏃ユ湡" />
+ </up-form-item>
+ <up-form-item v-if="form.type === '璐ц溅'"
+ label="鍙戣揣杞︾墝鍙�"
+ prop="shippingCarNumber"
+ required
+ border-bottom>
+ <up-input v-model="form.shippingCarNumber"
+ placeholder="璇疯緭鍏ュ彂璐ц溅鐗屽彿"
+ clearable />
+ </up-form-item>
+ <up-form-item v-if="form.type === '蹇��'"
+ label="蹇�掑叕鍙�"
+ prop="expressCompany"
+ required
+ border-bottom>
+ <up-input v-model="form.expressCompany"
+ placeholder="璇疯緭鍏ュ揩閫掑叕鍙�"
+ clearable />
+ </up-form-item>
+ <up-form-item v-if="form.type === '蹇��'"
+ label="蹇�掑崟鍙�"
+ prop="expressNumber">
+ <up-input v-model="form.expressNumber"
+ placeholder="璇疯緭鍏ュ揩閫掑崟鍙�"
+ clearable />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鍗曟嵁涓庡綊妗�"
+ class="form-section">
+ <up-form-item label="鍙戣揣鍗曟嵁"
+ prop="shippingDocument"
+ border-bottom>
+ <up-input v-model="form.shippingDocument"
+ placeholder="璇疯緭鍏ュ彂璐у崟鎹�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鏄惁瀵硅处"
+ prop="isReconciled"
+ border-bottom>
+ <up-switch v-model="form.isReconciled"
+ :active-value="1"
+ :inactive-value="0" />
+ </up-form-item>
+ <up-form-item v-if="form.isReconciled === 1"
+ label="瀵硅处鍗曞綊妗�"
+ prop="statementArchivingStatus"
+ border-bottom>
+ <up-textarea v-model="form.statementArchivingStatus"
+ placeholder="璇峰~鍐欏璐﹀崟褰掓。鎯呭喌"
+ auto-height
+ maxlength="200" />
+ </up-form-item>
+ <up-form-item label="鏄惁缁撶畻"
+ prop="isSettled"
+ border-bottom>
+ <up-switch v-model="form.isSettled"
+ :active-value="1"
+ :inactive-value="0" />
+ </up-form-item>
+ <up-form-item v-if="form.isSettled === 1"
+ label="缁撶畻鍗曞綊妗�"
+ prop="settlementArchivingStatus"
+ border-bottom>
+ <up-textarea v-model="form.settlementArchivingStatus"
+ placeholder="璇峰~鍐欑粨绠楀崟褰掓。鎯呭喌"
+ auto-height
+ maxlength="200" />
+ </up-form-item>
+ <up-form-item label="鏄惁宸插紑绁�"
+ prop="isInvoiced">
+ <up-switch v-model="form.isInvoiced"
+ :active-value="1"
+ :inactive-value="0" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鍙戣揣鍥剧墖"
+ class="form-section">
+ <view class="upload-wrap">
+ <CommonUpload v-model="form.deliveryFiles"
+ accept="image"
+ :max-count="6" />
+ </view>
+ </u-cell-group>
+ </up-form>
+
+ <u-cell-group title="鍙戣揣浜у搧鏄庣粏"
+ class="form-section">
+ <view v-if="products.length === 0"
+ class="empty-block">
+ <text>鏆傛棤浜у搧鏄庣粏</text>
+ </view>
+ <view v-for="(row, index) in products"
+ :key="index"
+ class="product-row">
+ <view class="detail-row">
+ <text class="detail-label">浜у搧鍚嶇О</text>
+ <text class="detail-value">{{ row.productName || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瑙勬牸鍨嬪彿</text>
+ <text class="detail-value">{{ row.specificationModel || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵规鍙�</text>
+ <text class="detail-value">{{ row.batchNo || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙戣揣鏁伴噺</text>
+ <text class="detail-value">
+ {{ row.deliveryQuantity === "" ? "-" : row.deliveryQuantity }}{{ row.unit ? ` ${row.unit}` : "" }}
+ </text>
+ </view>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </view>
+</template>
+
+<script setup>
+ import { ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FooterButtons from "@/components/FooterButtons.vue";
+ import FormPicker from "@/components/FormPicker.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
+ import { formatDateToYMD } from "@/utils/ruoyi";
+ import { loadDetailData } from "@/utils/detailCache";
+ import {
+ resolveDeliveryFiles,
+ resolveDeliveryProducts,
+ } from "@/utils/delivery";
+ import {
+ deductStock,
+ getDeliveryDetail,
+ } from "@/api/salesManagement/deliveryLedger";
+
+ const formRef = ref();
+ const loading = ref(false);
+ const deliveryId = ref("");
+ const products = ref([]);
+
+ const typeOptions = [
+ { name: "璐ц溅", value: "璐ц溅" },
+ { name: "蹇��", value: "蹇��" },
+ ];
+
+ const form = ref({
+ id: undefined,
+ type: "",
+ shippingDate: "",
+ shippingCarNumber: "",
+ expressCompany: "",
+ expressNumber: "",
+ shippingDocument: "",
+ isReconciled: 0,
+ statementArchivingStatus: "",
+ isSettled: 0,
+ settlementArchivingStatus: "",
+ isInvoiced: 0,
+ deliveryFiles: [],
+ });
+
+ // 杞︾墝涓庡揩閫掑叕鍙告寜鍙戣揣绫诲瀷浜岄�変竴蹇呭~
+ const rules = {
+ type: [{ required: true, message: "璇烽�夋嫨鍙戣揣绫诲瀷", trigger: "change" }],
+ shippingDate: [
+ { required: true, message: "璇烽�夋嫨鍙戣揣鏃ユ湡", trigger: "change" },
+ ],
+ };
+
+ const goBack = () => uni.navigateBack();
+
+ const onTypeChange = value => {
+ if (value === "璐ц溅") {
+ form.value.expressCompany = "";
+ form.value.expressNumber = "";
+ } else {
+ form.value.shippingCarNumber = "";
+ }
+ };
+
+ const loadDetail = async () => {
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "deliveryDetail",
+ id: deliveryId.value,
+ fetcher: getDeliveryDetail,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鏈壘鍒板彂璐т俊鎭�", icon: "none" });
+ return;
+ }
+
+ form.value = {
+ id: data.id,
+ type: data.type || "",
+ shippingDate: (data.shippingDate || "").slice(0, 10),
+ shippingCarNumber: data.shippingCarNumber || "",
+ expressCompany: data.expressCompany || "",
+ expressNumber: data.expressNumber || "",
+ shippingDocument: data.shippingDocument || "",
+ isReconciled: data.isReconciled ?? 0,
+ statementArchivingStatus: data.statementArchivingStatus || "",
+ isSettled: data.isSettled ?? 0,
+ settlementArchivingStatus: data.settlementArchivingStatus || "",
+ isInvoiced: data.isInvoiced ?? 0,
+ deliveryFiles: resolveDeliveryFiles(data),
+ };
+ products.value = resolveDeliveryProducts(data);
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ if (form.value.type === "璐ц溅" && !form.value.shippingCarNumber) {
+ uni.showToast({ title: "璇疯緭鍏ュ彂璐ц溅鐗屽彿", icon: "none" });
+ return;
+ }
+ if (form.value.type === "蹇��" && !form.value.expressCompany) {
+ uni.showToast({ title: "璇疯緭鍏ュ揩閫掑叕鍙�", icon: "none" });
+ return;
+ }
+
+ // 闇�姹傚叚锛氬彂璐у彴璐﹁姹傝ˉ鍏呭崟鎹笌瀵硅处/缁撶畻/寮�绁ㄦ儏鍐点��
+ // 鍙傝�冨疄鐜帮紙绠$悊绔級鍙彁浜ゅ墠 7 涓瓧娈碉紝杩欓噷鎸夐渶姹備竴骞舵彁浜わ紝
+ // 鍚庣鑻ユ湭鎺ユ敹杩欏嚑涓瓧娈典細琚拷鐣ワ紝涓嶅奖鍝嶅彂璐т富娴佺▼銆�
+ const payload = {
+ id: form.value.id,
+ type: form.value.type,
+ shippingDate: form.value.shippingDate,
+ shippingCarNumber:
+ form.value.type === "璐ц溅" ? form.value.shippingCarNumber : "",
+ expressCompany:
+ form.value.type === "蹇��" ? form.value.expressCompany : "",
+ expressNumber: form.value.type === "蹇��" ? form.value.expressNumber : "",
+ storageBlobDTOs: (form.value.deliveryFiles || [])
+ .filter(file => file?.url)
+ .map(file => ({ url: file.url, name: file.name || "" })),
+ shippingDocument: form.value.shippingDocument,
+ isReconciled: form.value.isReconciled,
+ statementArchivingStatus:
+ form.value.isReconciled === 1
+ ? form.value.statementArchivingStatus
+ : "",
+ isSettled: form.value.isSettled,
+ settlementArchivingStatus:
+ form.value.isSettled === 1 ? form.value.settlementArchivingStatus : "",
+ isInvoiced: form.value.isInvoiced,
+ };
+
+ loading.value = true;
+ deductStock(payload)
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 1200);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ if (options?.id) {
+ deliveryId.value = options.id;
+ form.value.id = options.id;
+ loadDetail();
+ } else {
+ form.value.shippingDate = formatDateToYMD(Date.now());
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .upload-wrap {
+ padding: 12px;
+ }
+
+ .empty-block {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .product-row {
+ padding: 12px 16px;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .product-row:last-child {
+ border-bottom: none;
+ }
+
+ .detail-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 4px 0;
+ }
+
+ .detail-label {
+ font-size: 13px;
+ color: #909399;
+ }
+
+ .detail-value {
+ font-size: 13px;
+ color: #333;
+ max-width: 70%;
+ text-align: right;
+ word-break: break-all;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/deliveryLedger/index.vue b/src/pages/sales/deliveryLedger/index.vue
new file mode 100644
index 0000000..37be165
--- /dev/null
+++ b/src/pages/sales/deliveryLedger/index.vue
@@ -0,0 +1,336 @@
+<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="query.salesContractNo"
+ placeholder="璇疯緭鍏ラ攢鍞悎鍚屽彿鎼滅储"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="deliveryList.length > 0"
+ class="ledger-list">
+ <view v-for="item in deliveryList"
+ :key="item.id"
+ class="ledger-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="car"
+ size="16"
+ color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.shippingNo || item.salesContractNo || "-" }}</text>
+ </view>
+ <up-tag :text="statusText(item.status)"
+ :type="statusTagType(item.status)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <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">{{ item.projectName || "-" }}</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.productName || "-" }}</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.shippingDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙戣揣鏁伴噺</text>
+ <text class="detail-value">{{ item.totalQuantity ?? "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍙戣揣杞︾墝鍙�</text>
+ <text class="detail-value">{{ item.shippingCarNumber || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">蹇�掑叕鍙�</text>
+ <text class="detail-value">{{ item.expressCompany || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">蹇�掑崟鍙�</text>
+ <text class="detail-value">{{ item.expressNumber || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍑哄簱鎵规</text>
+ <text class="detail-value">{{ item.outboundBatches || "-" }}</text>
+ </view>
+ </view>
+
+ <view v-if="returnNos(item).length > 0"
+ class="return-entry"
+ @click="showReturnNos(item)">
+ <up-icon name="arrow-return-left"
+ size="15"
+ color="#ff9900"></up-icon>
+ <text class="return-entry-text">鏌ョ湅 {{ returnNos(item).length }} 涓��璐у崟</text>
+ </view>
+
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ :disabled="isLocked(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
+ :disabled="isLocked(item)"
+ @click="handleDelete(item)">鍒犻櫎</up-button>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <text>鏆傛棤鍙戣揣鍙拌处鏁版嵁</text>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鍙戣揣鍙拌处绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { computed, reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ deliveryLedgerListPage,
+ delDeliveryLedger,
+ } from "@/api/salesManagement/deliveryLedger";
+ import { unwrapList } from "@/utils/list";
+ import { loadProjectOptions } from "@/utils/projectOptions";
+
+ // 杩欎袱绉嶇姸鎬佸凡杩涘叆姝e紡娴佺▼锛屼笉鍏佽鍐嶆敼鍙戣揣淇℃伅鎴栧垹闄�
+ const LOCKED_STATUS = ["瀹℃牳閫氳繃", "宸插彂璐�"];
+
+ const STATUS_TEXT = {
+ 0: "寰呭鏍�",
+ 1: "瀹℃牳涓�",
+ 2: "瀹℃牳鎷掔粷",
+ 3: "瀹℃牳閫氳繃",
+ };
+
+ const deliveryList = ref([]);
+ const showFilter = ref(false);
+ const projectOptions = ref([]);
+
+ const query = reactive({
+ salesContractNo: "",
+ shippingCarNumber: "",
+ expressNumber: "",
+ projectId: "",
+ shippingDateRange: [],
+ });
+
+ const filterFields = computed(() => [
+ {
+ prop: "salesContractNo",
+ label: "閿�鍞悎鍚屽彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ラ攢鍞悎鍚屽彿",
+ },
+ {
+ prop: "shippingCarNumber",
+ label: "鍙戣揣杞︾墝鍙�",
+ type: "input",
+ placeholder: "璇疯緭鍏ュ彂璐ц溅鐗屽彿",
+ },
+ {
+ prop: "expressNumber",
+ label: "蹇�掑崟鍙�",
+ type: "input",
+ placeholder: "璇疯緭鍏ュ揩閫掑崟鍙�",
+ },
+ { prop: "projectId", label: "鎵�灞為」鐩�", type: "select", options: projectOptions.value },
+ { prop: "shippingDateRange", label: "鍙戣揣鏃ユ湡", type: "daterange" },
+ ]);
+
+ const goBack = () => uni.navigateBack();
+
+ const statusText = status => {
+ if (status === null || status === undefined || status === "") return "-";
+ return STATUS_TEXT[status] || String(status);
+ };
+
+ const statusTagType = status => {
+ if (status === "宸插彂璐�" || status === 3 || status === "3") return "success";
+ if (status === 1 || status === "1") return "warning";
+ if (status === 2 || status === "2") return "error";
+ return "info";
+ };
+
+ const isLocked = item => LOCKED_STATUS.includes(item?.status);
+
+ const returnNos = item =>
+ Array.isArray(item?.returnNoList) ? item.returnNoList : [];
+
+ const goDetail = item => {
+ uni.setStorageSync("deliveryDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/deliveryLedger/detail?id=${item.id}`,
+ });
+ };
+
+ const goEdit = item => {
+ if (isLocked(item)) {
+ uni.showToast({ title: "璇ョ姸鎬佺殑鏁版嵁涓嶅彲缂栬緫鍙戣揣淇℃伅", icon: "none" });
+ return;
+ }
+ uni.setStorageSync("deliveryDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/deliveryLedger/edit?id=${item.id}`,
+ });
+ };
+
+ // 闇�姹傚叚.4锛氶��璐х粺璁★紝寮圭獥鍒椾妇閫�璐у崟鍙�
+ const showReturnNos = item => {
+ const list = returnNos(item);
+ if (list.length === 0) return;
+ uni.showModal({
+ title: "閫�璐у崟鍙�",
+ content: list.join("\n"),
+ showCancel: false,
+ confirmText: "鐭ラ亾浜�",
+ });
+ };
+
+ const getList = () => {
+ const [beginShippingDate, endShippingDate] = Array.isArray(
+ query.shippingDateRange
+ )
+ ? query.shippingDateRange
+ : [];
+ deliveryLedgerListPage({
+ current: -1,
+ size: -1,
+ salesContractNo: query.salesContractNo || undefined,
+ shippingCarNumber: query.shippingCarNumber || undefined,
+ expressNumber: query.expressNumber || undefined,
+ projectId: query.projectId || undefined,
+ beginShippingDate: beginShippingDate || undefined,
+ endShippingDate: endShippingDate || undefined,
+ })
+ .then(res => {
+ deliveryList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ if (isLocked(item)) {
+ uni.showToast({ title: "璇ョ姸鎬佺殑鏁版嵁涓嶅彲鍒犻櫎", icon: "none" });
+ return;
+ }
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ュ彂璐у彴璐﹀悧锛�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delDeliveryLedger(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ let projectsLoaded = false;
+
+ const loadProjects = () => {
+ if (projectsLoaded) return;
+ projectsLoaded = true;
+ loadProjectOptions()
+ .then(options => {
+ projectOptions.value = options;
+ })
+ .catch(() => {
+ projectsLoaded = false;
+ });
+ };
+
+ onShow(() => {
+ getList();
+ loadProjects();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .return-entry {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin: 0 12px 10px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ background: #fffaf0;
+ }
+
+ .return-entry-text {
+ font-size: 13px;
+ color: #ff9900;
+ }
+</style>
diff --git a/src/pages/sales/lawsuitManagement/detail.vue b/src/pages/sales/lawsuitManagement/detail.vue
new file mode 100644
index 0000000..bf7bae9
--- /dev/null
+++ b/src/pages/sales/lawsuitManagement/detail.vue
@@ -0,0 +1,262 @@
+<template>
+ <view class="lawsuit-detail">
+ <PageHeader title="璇夎璇︽儏"
+ @back="goBack">
+ <template #right>
+ <text class="header-action"
+ @click="goEdit">缂栬緫</text>
+ </template>
+ </PageHeader>
+
+ <view v-if="!loading"
+ class="detail-card">
+ <view class="hero">
+ <text class="hero-title">{{ detail.caseNo || "-" }}</text>
+ <up-tag :text="detail.lawsuitStatus || '鏈~鐘舵��'"
+ :type="statusTagType(detail.lawsuitStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+
+ <u-cell-group title="妗堜欢淇℃伅"
+ class="detail-section">
+ <u-cell title="妗堜欢缂栧彿"
+ :value="detail.caseNo || '-'" />
+ <u-cell title="璇夎鐘舵��"
+ :value="detail.lawsuitStatus || '-'" />
+ <u-cell title="璇夎鏍囩殑閲戦"
+ :value="formatAmount(detail.claimAmount)" />
+ <u-cell title="绔嬫鏃堕棿"
+ :value="detail.filingDate || '-'" />
+ <u-cell title="寮�搴椂闂�"
+ :value="detail.hearingDate || '-'" />
+ <u-cell title="鎵垮姙寰嬪笀"
+ :value="detail.lawyer || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="鍏宠仈淇℃伅"
+ class="detail-section">
+ <u-cell title="鎵�灞為」鐩�"
+ :value="detail.projectName || '-'" />
+ <u-cell title="鍏宠仈閿�鍞悎鍚�"
+ :value="detail.contractNo || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="杩涘睍涓庣粨鏋�"
+ class="detail-section">
+ <u-cell title="鍒ゅ喅缁撴灉鎽樿"
+ :value="detail.judgmentSummary || '-'" />
+ <u-cell title="杩涘睍澶囨敞"
+ :value="detail.progressRemark || '-'" />
+ </u-cell-group>
+
+ <u-cell-group title="闄勪欢"
+ class="detail-section">
+ <view v-if="attachments.length === 0"
+ class="empty-file">
+ <text>鏆傛棤闄勪欢</text>
+ </view>
+ <view v-else
+ class="file-list">
+ <view v-for="(file, index) in attachments"
+ :key="index"
+ class="file-item"
+ @click="previewFile(file, index)">
+ <up-icon name="file-text"
+ size="18"
+ color="#2979ff"></up-icon>
+ <text class="file-name">{{ fileLabel(file, index) }}</text>
+ <up-icon name="download"
+ size="16"
+ color="#909399"></up-icon>
+ </view>
+ </view>
+ </u-cell-group>
+ </view>
+
+ <view v-else
+ class="loading-tip">
+ <text>鍔犺浇涓�...</text>
+ </view>
+ </view>
+</template>
+
+<script setup>
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import { toFileList } from "@/utils/file";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { getLawsuitDetail } from "@/api/salesManagement/lawsuitManagement";
+
+ const loading = ref(true);
+ const lawsuitId = ref("");
+ const detail = ref({});
+
+ const attachments = computed(() =>
+ toFileList(
+ detail.value.storageBlobVOs ||
+ detail.value.attachmentList ||
+ detail.value.fileList
+ )
+ );
+
+ const goBack = () => uni.navigateBack();
+
+ const goEdit = () => {
+ uni.setStorageSync("lawsuitDetail", detail.value || {});
+ uni.navigateTo({
+ url: `/pages/sales/lawsuitManagement/edit?id=${lawsuitId.value}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const statusTagType = status => {
+ if (status === "宸插垽鍐�" || status === "宸插拰瑙�") return "success";
+ if (status === "宸叉挙璇�") return "info";
+ if (status) return "warning";
+ return "info";
+ };
+
+ const fileLabel = (file, index) =>
+ file.name || file.originalFilename || `闄勪欢${index + 1}`;
+
+ // request.ts 鏈�忎紶 responseType锛宐lob 涓嬭浇涓嶅彲鐢紝鍙兘鐢� downloadFile + openDocument
+ const previewFile = file => {
+ const url = file.url || file.previewURL;
+ if (!url) return;
+ uni.showLoading({ title: "鎵撳紑涓�...", mask: true });
+ uni.downloadFile({
+ url,
+ success: res => {
+ if (res.statusCode !== 200) {
+ uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" });
+ return;
+ }
+ uni.openDocument({
+ filePath: res.tempFilePath,
+ showMenu: true,
+ fail: () => uni.showToast({ title: "鏆備笉鏀寔棰勮璇ユ枃浠�", icon: "none" }),
+ });
+ },
+ fail: () => uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" }),
+ complete: () => uni.hideLoading(),
+ });
+ };
+
+ const loadDetail = async () => {
+ loading.value = true;
+ try {
+ const data = await loadDetailData({
+ cacheKey: "lawsuitDetail",
+ id: lawsuitId.value,
+ fetcher: getLawsuitDetail,
+ });
+ detail.value = data || {};
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇璇夎璇︽儏澶辫触", icon: "none" });
+ detail.value = {};
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ onLoad(options => {
+ lawsuitId.value = options?.id || "";
+ if (!lawsuitId.value) {
+ loading.value = false;
+ return;
+ }
+ loadDetail();
+ });
+</script>
+
+<style scoped lang="scss">
+ .lawsuit-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 24px;
+ }
+
+ .header-action {
+ font-size: 14px;
+ color: #2979ff;
+ }
+
+ .detail-card {
+ padding: 12px;
+ }
+
+ .hero {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 18px;
+ border-radius: 16px;
+ background: linear-gradient(135deg, #fff7ea 0%, #ffffff 100%);
+ box-shadow: 0 6px 18px rgba(230, 162, 60, 0.1);
+ }
+
+ .hero-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2d3d;
+ max-width: 70%;
+ word-break: break-all;
+ }
+
+ .detail-section {
+ margin-top: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .empty-file {
+ padding: 16px;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .file-list {
+ padding: 6px 16px 12px;
+ }
+
+ .file-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 0;
+ border-bottom: 1px solid #f2f4f8;
+ }
+
+ .file-item:last-child {
+ border-bottom: none;
+ }
+
+ .file-name {
+ flex: 1;
+ font-size: 14px;
+ color: #333;
+ word-break: break-all;
+ }
+
+ .loading-tip {
+ padding: 80px 0;
+ text-align: center;
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/lawsuitManagement/edit.vue b/src/pages/sales/lawsuitManagement/edit.vue
new file mode 100644
index 0000000..105d494
--- /dev/null
+++ b/src/pages/sales/lawsuitManagement/edit.vue
@@ -0,0 +1,344 @@
+<template>
+ <view class="account-detail">
+ <PageHeader :title="pageTitle"
+ @back="goBack" />
+ <view class="form-container">
+ <up-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120"
+ input-align="right"
+ error-message-align="right">
+ <u-cell-group title="妗堜欢淇℃伅"
+ class="form-section">
+ <up-form-item label="妗堜欢缂栧彿"
+ prop="caseNo"
+ required
+ border-bottom>
+ <up-input v-model="form.caseNo"
+ placeholder="璇疯緭鍏ユ浠剁紪鍙�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="璇夎鐘舵��"
+ prop="lawsuitStatus"
+ required
+ border-bottom>
+ <FormPicker v-model="form.lawsuitStatus"
+ type="select"
+ title="閫夋嫨璇夎鐘舵��"
+ :options="lawsuitStatusOptions"
+ placeholder="璇烽�夋嫨璇夎鐘舵��" />
+ </up-form-item>
+ <up-form-item label="璇夎鏍囩殑閲戦"
+ prop="claimAmount"
+ border-bottom>
+ <up-input v-model="form.claimAmount"
+ type="number"
+ placeholder="璇疯緭鍏ヨ瘔璁兼爣鐨勯噾棰�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="绔嬫鏃堕棿"
+ prop="filingDate"
+ required
+ border-bottom>
+ <FormPicker v-model="form.filingDate"
+ type="date"
+ placeholder="璇烽�夋嫨绔嬫鏃堕棿" />
+ </up-form-item>
+ <up-form-item label="寮�搴椂闂�"
+ prop="hearingDate"
+ border-bottom>
+ <FormPicker v-model="form.hearingDate"
+ type="date"
+ placeholder="璇烽�夋嫨寮�搴椂闂�" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="鍏宠仈淇℃伅"
+ class="form-section">
+ <up-form-item label="鎵�灞為」鐩�"
+ prop="projectId"
+ required
+ border-bottom>
+ <FormPicker v-model="form.projectId"
+ :display-text="form.projectName"
+ type="select"
+ title="閫夋嫨鎵�灞為」鐩�"
+ :options="projectOptions"
+ placeholder="璇烽�夋嫨鎵�灞為」鐩�"
+ @change="onProjectChange" />
+ </up-form-item>
+ <up-form-item label="鍏宠仈閿�鍞悎鍚�"
+ prop="contractId"
+ required
+ border-bottom>
+ <FormPicker v-model="form.contractId"
+ :display-text="form.contractNo"
+ type="select"
+ title="閫夋嫨鍏宠仈閿�鍞悎鍚�"
+ :options="contractOptions"
+ placeholder="璇烽�夋嫨鍏宠仈閿�鍞悎鍚�"
+ @change="onContractChange" />
+ </up-form-item>
+ <up-form-item label="鎵垮姙寰嬪笀"
+ prop="lawyer"
+ border-bottom>
+ <up-input v-model="form.lawyer"
+ placeholder="璇疯緭鍏ユ壙鍔炲緥甯�/娉曞姟璐熻矗浜�"
+ clearable />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="杩涘睍涓庣粨鏋�"
+ class="form-section">
+ <up-form-item label="鍒ゅ喅缁撴灉鎽樿"
+ prop="judgmentSummary"
+ border-bottom>
+ <up-textarea v-model="form.judgmentSummary"
+ placeholder="璇疯緭鍏ュ垽鍐崇粨鏋滄憳瑕�"
+ auto-height
+ maxlength="500" />
+ </up-form-item>
+ <up-form-item label="杩涘睍澶囨敞"
+ prop="progressRemark">
+ <up-textarea v-model="form.progressRemark"
+ placeholder="璇疯緭鍏ヨ瘔璁艰繘灞曞娉�"
+ auto-height
+ maxlength="500" />
+ </up-form-item>
+ </u-cell-group>
+
+ <u-cell-group title="闄勪欢"
+ class="form-section">
+ <view class="upload-wrap">
+ <CommonUpload v-model="form.attachmentFiles"
+ accept="all"
+ :max-count="5" />
+ </view>
+ </u-cell-group>
+ </up-form>
+ </view>
+
+ <FooterButtons :loading="loading"
+ confirmText="淇濆瓨"
+ @cancel="goBack"
+ @confirm="handleSubmit" />
+ </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 FormPicker from "@/components/FormPicker.vue";
+ import CommonUpload from "@/components/CommonUpload.vue";
+ import { toFileList } from "@/utils/file";
+ import { loadDetailData } from "@/utils/detailCache";
+ import { loadProjectOptions, pickProjectName } from "@/utils/projectOptions";
+ import {
+ addOrUpdateLawsuit,
+ getLawsuitDetail,
+ } from "@/api/salesManagement/lawsuitManagement";
+ import { contractListNoPage } from "@/api/salesManagement/contractManagement";
+
+ const formRef = ref();
+ const loading = ref(false);
+ const lawsuitId = ref("");
+ const projectOptions = ref([]);
+ const contractOptions = ref([]);
+
+ const lawsuitStatusOptions = [
+ { name: "绔嬫涓�", value: "绔嬫涓�" },
+ { name: "瀹$悊涓�", value: "瀹$悊涓�" },
+ { name: "宸插垽鍐�", value: "宸插垽鍐�" },
+ { name: "宸叉挙璇�", value: "宸叉挙璇�" },
+ { name: "宸插拰瑙�", value: "宸插拰瑙�" },
+ ];
+
+ const form = ref({
+ id: undefined,
+ caseNo: "",
+ lawsuitStatus: "",
+ claimAmount: "",
+ filingDate: "",
+ hearingDate: "",
+ lawyer: "",
+ projectId: "",
+ projectName: "",
+ contractId: "",
+ contractNo: "",
+ judgmentSummary: "",
+ progressRemark: "",
+ attachmentFiles: [],
+ });
+
+ const rules = {
+ caseNo: [{ required: true, message: "璇疯緭鍏ユ浠剁紪鍙�", trigger: "blur" }],
+ lawsuitStatus: [
+ { required: true, message: "璇烽�夋嫨璇夎鐘舵��", trigger: "change" },
+ ],
+ filingDate: [{ required: true, message: "璇烽�夋嫨绔嬫鏃堕棿", trigger: "change" }],
+ projectId: [{ required: true, message: "璇烽�夋嫨鎵�灞為」鐩�", trigger: "change" }],
+ contractId: [
+ { required: true, message: "璇烽�夋嫨鍏宠仈閿�鍞悎鍚�", trigger: "change" },
+ ],
+ };
+
+ const pageTitle = computed(() => (lawsuitId.value ? "缂栬緫璇夎" : "鏂板璇夎"));
+
+ const goBack = () => uni.navigateBack();
+
+ const numOrNull = value =>
+ value === "" || value === null || value === undefined
+ ? null
+ : Number.isNaN(Number(value))
+ ? null
+ : Number(value);
+
+ const onProjectChange = (value, item) => {
+ form.value.projectName = pickProjectName(item);
+ };
+
+ const onContractChange = (value, item) => {
+ form.value.contractNo = item?.contractNo || "";
+ };
+
+ const loadOptions = async () => {
+ try {
+ projectOptions.value = await loadProjectOptions();
+ } catch (error) {
+ projectOptions.value = [];
+ }
+ try {
+ const res = await contractListNoPage();
+ const records = res?.data?.records || res?.records || res?.data || [];
+ contractOptions.value = (Array.isArray(records) ? records : []).map(
+ item => ({
+ name: item.signingUnit
+ ? `${item.contractNo}锛�${item.signingUnit}锛塦
+ : item.contractNo,
+ value: item.id,
+ raw: item,
+ })
+ );
+ } catch (error) {
+ contractOptions.value = [];
+ }
+ };
+
+ const loadDetail = async () => {
+ let data = null;
+ try {
+ data = await loadDetailData({
+ cacheKey: "lawsuitDetail",
+ id: lawsuitId.value,
+ fetcher: getLawsuitDetail,
+ });
+ } catch (error) {
+ data = null;
+ }
+ if (!data) {
+ uni.showToast({ title: "鏈壘鍒拌瘔璁间俊鎭�", icon: "none" });
+ return;
+ }
+
+ form.value = {
+ id: data.id,
+ caseNo: data.caseNo || "",
+ lawsuitStatus: data.lawsuitStatus || "",
+ claimAmount: data.claimAmount ?? "",
+ filingDate: (data.filingDate || "").slice(0, 10),
+ hearingDate: (data.hearingDate || "").slice(0, 10),
+ lawyer: data.lawyer || "",
+ projectId: data.projectId ?? "",
+ projectName: data.projectName || "",
+ contractId: data.contractId ?? "",
+ contractNo: data.contractNo || "",
+ judgmentSummary: data.judgmentSummary || "",
+ progressRemark: data.progressRemark || "",
+ attachmentFiles: toFileList(
+ data.storageBlobVOs || data.attachmentList || data.fileList
+ ),
+ };
+ };
+
+ const handleSubmit = async () => {
+ const valid = await formRef.value.validate().catch(() => false);
+ if (!valid) return;
+
+ const payload = {
+ id: form.value.id,
+ caseNo: form.value.caseNo,
+ lawsuitStatus: form.value.lawsuitStatus,
+ claimAmount: numOrNull(form.value.claimAmount),
+ filingDate: form.value.filingDate,
+ hearingDate: form.value.hearingDate || null,
+ lawyer: form.value.lawyer,
+ projectId: form.value.projectId,
+ projectName: form.value.projectName,
+ contractId: form.value.contractId,
+ contractNo: form.value.contractNo,
+ judgmentSummary: form.value.judgmentSummary,
+ progressRemark: form.value.progressRemark,
+ storageBlobDTOs: (form.value.attachmentFiles || [])
+ .filter(file => file?.url)
+ .map(file => ({ url: file.url, name: file.name || "" })),
+ };
+
+ loading.value = true;
+ addOrUpdateLawsuit(payload)
+ .then(() => {
+ uni.showToast({ title: "淇濆瓨鎴愬姛", icon: "success" });
+ setTimeout(() => uni.navigateBack(), 1000);
+ })
+ .catch(() => {
+ uni.showToast({ title: "淇濆瓨澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ onLoad(options => {
+ loadOptions();
+ if (options?.id) {
+ lawsuitId.value = options.id;
+ form.value.id = options.id;
+ loadDetail();
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/static/scss/form-common.scss";
+
+ .account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 100px;
+ }
+
+ .form-container {
+ padding: 12px 12px 0;
+ }
+
+ .form-section {
+ margin-bottom: 12px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
+ }
+
+ .upload-wrap {
+ padding: 12px;
+ }
+
+ :deep(.u-cell-group__title) {
+ padding: 14px 18px 10px !important;
+ font-size: 15px !important;
+ font-weight: 600 !important;
+ color: #22324d !important;
+ background: #f8fbff !important;
+ }
+</style>
diff --git a/src/pages/sales/lawsuitManagement/index.vue b/src/pages/sales/lawsuitManagement/index.vue
new file mode 100644
index 0000000..0d04fdc
--- /dev/null
+++ b/src/pages/sales/lawsuitManagement/index.vue
@@ -0,0 +1,264 @@
+<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="query.caseNo"
+ placeholder="璇疯緭鍏ユ浠剁紪鍙锋悳绱�"
+ clearable
+ @change="getList" />
+ </view>
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
+ size="24"
+ color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <view v-if="lawsuitList.length > 0"
+ class="ledger-list">
+ <view v-for="item in lawsuitList"
+ :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.caseNo || "-" }}</text>
+ </view>
+ <up-tag :text="item.lawsuitStatus || '鏈~鐘舵��'"
+ :type="statusTagType(item.lawsuitStatus)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <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.contractNo || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">璇夎鏍囩殑</text>
+ <text class="detail-value">{{ formatAmount(item.claimAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绔嬫鏃堕棿</text>
+ <text class="detail-value">{{ item.filingDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">寮�搴椂闂�</text>
+ <text class="detail-value">{{ item.hearingDate || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鎵垮姙寰嬪笀</text>
+ <text class="detail-value">{{ item.lawyer || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍒ゅ喅缁撴灉</text>
+ <text class="detail-value">{{ item.judgmentSummary || "-" }}</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"
+ @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">
+ <text>鏆傛棤璇夎鏁版嵁</text>
+ </view>
+
+ <view class="fab-button"
+ @click="goAdd">
+ <up-icon name="plus"
+ size="28"
+ color="#ffffff"></up-icon>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="璇夎绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
+ </view>
+</template>
+
+<script setup>
+ import { reactive, ref } from "vue";
+ import { onShow } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ lawsuitListPage,
+ delLawsuit,
+ } from "@/api/salesManagement/lawsuitManagement";
+ import { unwrapList } from "@/utils/list";
+
+ const LAWSUIT_STATUS_OPTIONS = [
+ { name: "绔嬫涓�", value: "绔嬫涓�" },
+ { name: "瀹$悊涓�", value: "瀹$悊涓�" },
+ { name: "宸插垽鍐�", value: "宸插垽鍐�" },
+ { name: "宸叉挙璇�", value: "宸叉挙璇�" },
+ { name: "宸插拰瑙�", value: "宸插拰瑙�" },
+ ];
+
+ const lawsuitList = ref([]);
+ const showFilter = ref(false);
+
+ const query = reactive({
+ caseNo: "",
+ projectName: "",
+ contractNo: "",
+ lawsuitStatus: "",
+ filingDateRange: [],
+ });
+
+ const filterFields = [
+ {
+ prop: "caseNo",
+ label: "妗堜欢缂栧彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ユ浠剁紪鍙�",
+ },
+ {
+ prop: "projectName",
+ label: "鎵�灞為」鐩�",
+ type: "input",
+ placeholder: "璇疯緭鍏ラ」鐩悕绉�",
+ },
+ {
+ prop: "contractNo",
+ label: "鍚堝悓缂栧彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ュ悎鍚岀紪鍙�",
+ },
+ {
+ prop: "lawsuitStatus",
+ label: "璇夎鐘舵��",
+ type: "select",
+ options: LAWSUIT_STATUS_OPTIONS,
+ },
+ { prop: "filingDateRange", label: "绔嬫鏃堕棿", type: "daterange" },
+ ];
+
+ const goBack = () => uni.navigateBack();
+
+ const goAdd = () => {
+ uni.removeStorageSync("lawsuitDetail");
+ uni.navigateTo({ url: "/pages/sales/lawsuitManagement/edit" });
+ };
+
+ const goEdit = item => {
+ uni.setStorageSync("lawsuitDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/lawsuitManagement/edit?id=${item.id}`,
+ });
+ };
+
+ const goDetail = item => {
+ uni.setStorageSync("lawsuitDetail", item || {});
+ uni.navigateTo({
+ url: `/pages/sales/lawsuitManagement/detail?id=${item.id}`,
+ });
+ };
+
+ const formatAmount = amount =>
+ amount === null || amount === undefined || amount === ""
+ ? "-"
+ : `楼${Number(amount).toFixed(2)}`;
+
+ const statusTagType = status => {
+ if (status === "宸插垽鍐�" || status === "宸插拰瑙�") return "success";
+ if (status === "宸叉挙璇�") return "info";
+ if (status) return "warning";
+ return "info";
+ };
+
+ const getList = () => {
+ const [filingDateStart, filingDateEnd] = Array.isArray(query.filingDateRange)
+ ? query.filingDateRange
+ : [];
+ lawsuitListPage({
+ current: -1,
+ size: -1,
+ caseNo: query.caseNo || undefined,
+ projectName: query.projectName || undefined,
+ contractNo: query.contractNo || undefined,
+ lawsuitStatus: query.lawsuitStatus || undefined,
+ filingDateStart: filingDateStart || undefined,
+ filingDateEnd: filingDateEnd || undefined,
+ })
+ .then(res => {
+ lawsuitList.value = unwrapList(res);
+ })
+ .catch(() => {
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ const handleDelete = item => {
+ if (!item?.id) return;
+ uni.showModal({
+ title: "鍒犻櫎纭",
+ content: "纭鍒犻櫎璇ヨ瘔璁艰褰曞悧锛�",
+ success: res => {
+ if (!res.confirm) return;
+ uni.showLoading({ title: "澶勭悊涓�...", mask: true });
+ delLawsuit(item.id)
+ .then(() => {
+ uni.showToast({ title: "鍒犻櫎鎴愬姛", icon: "success" });
+ getList();
+ })
+ .catch(() => {
+ uni.showToast({ title: "鍒犻櫎澶辫触", icon: "none" });
+ })
+ .finally(() => {
+ uni.hideLoading();
+ });
+ },
+ });
+ };
+
+ onShow(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ @import "@/styles/sales-common.scss";
+
+ .detail-value {
+ max-width: 70%;
+ word-break: break-all;
+ }
+</style>
diff --git a/src/pages/sales/receiptPaymentLedger/detail.vue b/src/pages/sales/receiptPaymentLedger/detail.vue
index b494332..323c255 100644
--- a/src/pages/sales/receiptPaymentLedger/detail.vue
+++ b/src/pages/sales/receiptPaymentLedger/detail.vue
@@ -1,176 +1,268 @@
<template>
<view class="receipt-payment-detail">
- <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
- <PageHeader title="瀹㈡埛寰�鏉ヨ鎯�"
+ <PageHeader :title="pageTitle"
@back="goBack" />
- <!-- 缁熻淇℃伅 -->
- <view class="summary-info"
- v-if="tableData.length > 0">
- <view class="summary-item">
- <text class="summary-label">鎬昏褰曟暟</text>
- <text class="summary-value">{{ tableData.length }}</text>
+
+ <!-- 绗簩绾э細椤圭洰涓嬬殑瀹㈡埛寰�鏉� -->
+ <template v-if="level === 'customer'">
+ <!-- 椤圭洰搴旀敹鎽樿锛堥渶姹備竷锛氬簲鏀惰处娆剧鐞嗛噸鐐逛俊鎭憳瑕侊級 -->
+ <view class="summary-info">
+ <view class="summary-item">
+ <text class="summary-label">瀹㈡埛鏁�</text>
+ <text class="summary-value">{{ customerTotal }}</text>
+ </view>
+ <view class="summary-item">
+ <text class="summary-label">绛剧害鎬婚</text>
+ <text class="summary-value">{{ fmtMoney(sumOf("contractAmounts")) }}</text>
+ </view>
+ <view class="summary-item">
+ <text class="summary-label">宸插洖娆�</text>
+ <text class="summary-value highlight">{{ fmtMoney(sumOf("receiptPaymentAmount")) }}</text>
+ </view>
+ <view class="summary-item">
+ <text class="summary-label">搴旀敹鎬婚</text>
+ <text class="summary-value danger">{{ fmtMoney(sumOf("receiptableAmount")) }}</text>
+ </view>
+ <view class="summary-item">
+ <text class="summary-label">瓒呮湡鍚堝悓</text>
+ <text class="summary-value danger">{{ overdueCount }} 浠�</text>
+ </view>
</view>
- <view class="summary-item">
- <text class="summary-label">寮�绁ㄦ�婚噾棰�</text>
- <text class="summary-value">{{ formatAmount(invoiceTotal) }}</text>
- </view>
- <view class="summary-item">
- <text class="summary-label">鍥炴鎬婚噾棰�</text>
- <text class="summary-value highlight">{{ formatAmount(receiptTotal) }}</text>
- </view>
- <view class="summary-item">
- <text class="summary-label">搴旀敹鎬婚噾棰�</text>
- <text class="summary-value danger">{{ formatAmount(unReceiptTotal) }}</text>
- </view>
- </view>
- <!-- 鍥炴璁板綍鏄庣粏鍒楄〃 -->
- <view class="detail-list"
- v-if="tableData.length > 0">
- <view v-for="(item, index) in tableData"
- :key="index"
- class="detail-item">
- <view class="item-header">
- <view class="item-left">
- <view class="record-icon">
- <up-icon name="file-text"
- size="16"
- color="#ffffff"></up-icon>
+
+ <view class="detail-list"
+ v-if="customerList.length > 0">
+ <view v-for="item in customerList"
+ :key="item.customerId"
+ class="detail-item"
+ @click="goContracts(item)">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="record-icon">
+ <up-icon name="account"
+ size="16"
+ color="#ffffff" />
+ </view>
+ <text class="item-index">{{ item.customerName || "鏈懡鍚嶅鎴�" }}</text>
</view>
- <text class="item-index">{{ index + 1 }}</text>
+ <up-tag :text="item.overdueCount > 0 ? `${item.overdueCount} 浠借秴鏈焋 : '姝e父灞ョ害'"
+ :type="item.overdueCount > 0 ? 'error' : 'success'"
+ size="mini"
+ shape="circle" />
</view>
- <view class="item-date">{{ item.happenTime }}</view>
- </view>
- <up-divider></up-divider>
- <view class="item-details">
- <view class="detail-row">
- <text class="detail-label">寮�绁ㄩ噾棰�(鍏�)</text>
- <text class="detail-value">{{ formatAmount(item.invoiceTotal) }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鍥炴閲戦(鍏�)</text>
- <text class="detail-value highlight">{{ formatAmount(item.receiptPaymentAmount) }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">搴旀敹閲戦(鍏�)</text>
- <text class="detail-value danger">{{ formatAmount(item.unReceiptPaymentAmount) }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鍙戠敓鏃ユ湡</text>
- <text class="detail-value">{{ item.receiptPaymentDate }}</text>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">绛剧害鎬婚</text>
+ <text class="detail-value">{{ fmtMoney(item.contractAmounts) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸插洖娆�</text>
+ <text class="detail-value highlight">{{ fmtMoney(item.receiptPaymentAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴旀敹閲戦</text>
+ <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閫�璐ч噾棰�</text>
+ <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍥炴杩涘害</text>
+ <text class="detail-value"
+ :class="{ danger: item.overdueCount > 0 }">
+ {{ calcRate(item.receiptPaymentAmount, item.contractAmounts) }}%
+ </text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堝悓鏁�</text>
+ <text class="detail-value">{{ item.contractCount || 0 }} 浠�</text>
+ </view>
</view>
</view>
</view>
- </view>
- <view v-else
- class="no-data">
- <text>鏆傛棤鍥炴璁板綍</text>
- </view>
+ <view v-else
+ class="no-data">
+ <text>璇ラ」鐩笅鏆傛棤瀹㈡埛寰�鏉ユ暟鎹�</text>
+ </view>
+ </template>
+
+ <!-- 绗笁绾э細瀹㈡埛涓嬬殑鍚堝悓鏄庣粏 -->
+ <template v-else>
+ <view class="detail-list"
+ v-if="contractList.length > 0">
+ <view v-for="(item, index) in contractList"
+ :key="index"
+ class="detail-item">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="record-icon">
+ <up-icon name="file-text"
+ size="16"
+ color="#ffffff" />
+ </view>
+ <text class="item-index">{{ item.contractName || item.salesContractNo || "鏈懡鍚嶅悎鍚�" }}</text>
+ </view>
+ <up-tag :text="contractStatusText(item)"
+ :type="contractStatusType(item)"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <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">{{ fmtMoney(item.contractAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">宸插洖娆�</text>
+ <text class="detail-value"
+ :class="{ danger: item.isPaymentTimeout }">
+ {{ fmtMoney(item.receiptPaymentAmount) }}
+ ({{ calcRate(item.receiptPaymentAmount, item.contractAmount) }}%)
+ </text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴旀敹閲戦</text>
+ <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閫�璐ч噾棰�</text>
+ <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else
+ class="no-data">
+ <text>璇ュ鎴蜂笅鏆傛棤鍚堝悓鏄庣粏</text>
+ </view>
+ </template>
</view>
</template>
<script setup>
- import { ref, computed, onMounted } from "vue";
- import { onShow } from "@dcloudio/uni-app";
- import { customerInteractions } from "@/api/salesManagement/receiptPayment.js";
+ import { computed, ref } from "vue";
+ import { onLoad } from "@dcloudio/uni-app";
+ import PageHeader from "@/components/PageHeader.vue";
+ import {
+ customewTransactions,
+ customewTransactionsDetails,
+ } from "@/api/salesManagement/indicatorStats";
+ import { unwrapList } from "@/utils/list";
- // 瀹㈡埛淇℃伅
+ // level: customer = 椤圭洰涓嬬殑瀹㈡埛姹囨�伙紱contract = 瀹㈡埛涓嬬殑鍚堝悓鏄庣粏
+ const level = ref("customer");
+ const projectId = ref("");
+ const projectName = ref("");
const customerId = ref("");
+ const customerName = ref("");
- // 琛ㄦ牸鏁版嵁
- const tableData = ref([]);
+ const customerList = ref([]);
+ const contractList = ref([]);
+ const customerTotal = ref(0);
- const invoiceTotal = computed(() => {
- return tableData.value.reduce((sum, item) => {
- return sum + (parseFloat(item.invoiceTotal) || 0);
- }, 0);
+ const pageTitle = computed(() => {
+ if (level.value === "customer") return projectName.value || "瀹㈡埛寰�鏉�";
+ return customerName.value || "鍚堝悓鏄庣粏";
});
- const receiptTotal = computed(() => {
- return tableData.value.reduce((sum, item) => {
- return sum + (parseFloat(item.receiptPaymentAmount) || 0);
- }, 0);
- });
+ const sumOf = field =>
+ customerList.value.reduce((sum, item) => sum + (Number(item[field]) || 0), 0);
- const unReceiptTotal = computed(() => {
- return tableData.value.reduce((sum, item) => {
- return sum + (parseFloat(item.unReceiptPaymentAmount) || 0);
- }, 0);
- });
+ const overdueCount = computed(() =>
+ customerList.value.reduce((sum, item) => sum + (Number(item.overdueCount) || 0), 0)
+ );
- // 杩斿洖涓婁竴椤�
- const goBack = () => {
- uni.removeStorageSync("customerId");
- uni.navigateBack();
- };
-
- // 鑾峰彇椤甸潰鍙傛暟
- const getPageParams = () => {
- // 浠庢湰鍦板瓨鍌ㄨ幏鍙栧鎴稩D
- const storedCustomerId = uni.getStorageSync("customerId");
- if (storedCustomerId) {
- customerId.value = storedCustomerId;
- }
- };
-
- // 鏌ヨ鍒楄〃
- const getList = () => {
- if (!customerId.value) {
- uni.showToast({
- title: "瀹㈡埛淇℃伅缂哄け",
- icon: "error",
+ const loadCustomers = () => {
+ if (!projectId.value) return;
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+ customewTransactions({ projectId: projectId.value, current: -1, size: -1 })
+ .then(res => {
+ customerList.value = unwrapList(res);
+ customerTotal.value = Number(res?.data?.total ?? customerList.value.length);
+ uni.hideLoading();
+ })
+ .catch(() => {
+ uni.hideLoading();
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
});
- return;
- }
+ };
- showLoadingToast("鍔犺浇涓�...");
- const param = {
+ const loadContracts = () => {
+ if (!projectId.value || !customerId.value) return;
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+ customewTransactionsDetails({
+ projectId: projectId.value,
customerId: customerId.value,
current: -1,
size: -1,
- };
-
- customerInteractions(param)
+ })
.then(res => {
- tableData.value = res.data;
- closeToast();
+ contractList.value = unwrapList(res);
+ uni.hideLoading();
})
.catch(() => {
- closeToast();
- uni.showToast({
- title: "鏌ヨ澶辫触",
- icon: "error",
- });
+ uni.hideLoading();
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
});
};
- // 鏍煎紡鍖栭噾棰�
- const formatAmount = amount => {
- return amount ? parseFloat(amount).toFixed(2) : "0.00";
+ const goContracts = item => {
+ customerId.value = item.customerId;
+ customerName.value = item.customerName || "";
+ level.value = "contract";
+ loadContracts();
};
- // 鏄剧ず鍔犺浇鎻愮ず
- const showLoadingToast = message => {
- uni.showLoading({
- title: message,
- mask: true,
- });
+ // 杩斿洖锛氱涓夌骇閫�鍥炵浜岀骇锛岀浜岀骇鎵嶇湡姝i��鍑洪〉闈�
+ const goBack = () => {
+ if (level.value === "contract") {
+ level.value = "customer";
+ contractList.value = [];
+ return;
+ }
+ uni.navigateBack();
};
- // 鍏抽棴鎻愮ず
- const closeToast = () => {
- uni.hideLoading();
+ const contractStatusText = item => {
+ if (item.isPaymentTimeout) return "瓒呮湡鏈洖";
+ const amount = Number(item.contractAmount) || 0;
+ if (amount > 0 && (Number(item.receiptPaymentAmount) || 0) >= amount) return "宸茬粨娓�";
+ return "灞ヨ涓�";
};
- onShow(() => {
- // 椤甸潰鏄剧ず鏃惰幏鍙栧弬鏁板苟鍒锋柊鍒楄〃
- getPageParams();
- getList();
- });
+ const contractStatusType = item => {
+ if (item.isPaymentTimeout) return "error";
+ const amount = Number(item.contractAmount) || 0;
+ if (amount > 0 && (Number(item.receiptPaymentAmount) || 0) >= amount) return "success";
+ return "primary";
+ };
- onMounted(() => {
- // 椤甸潰鍔犺浇鏃惰幏鍙栧弬鏁板苟鍒锋柊鍒楄〃
- getPageParams();
- getList();
+ const fmtMoney = value => {
+ const num = Number(value);
+ if (!Number.isFinite(num)) return "楼 0.00";
+ return "楼 " + num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ };
+
+ const calcRate = (paid, amount) => {
+ const total = Number(amount);
+ if (!Number.isFinite(total) || total === 0) return "0.0";
+ return (((Number(paid) || 0) / total) * 100).toFixed(1);
+ };
+
+ onLoad(options => {
+ projectId.value = options?.projectId || "";
+ projectName.value = options?.projectName ? decodeURIComponent(options.projectName) : "";
+ if (!projectId.value) {
+ uni.showToast({ title: "缂哄皯椤圭洰鍙傛暟", icon: "none" });
+ return;
+ }
+ loadCustomers();
});
</script>
@@ -249,6 +341,8 @@
display: flex;
align-items: center;
gap: 8px;
+ flex: 1;
+ min-width: 0;
}
.record-icon {
@@ -259,17 +353,16 @@
display: flex;
align-items: center;
justify-content: center;
+ flex-shrink: 0;
}
.item-index {
font-size: 14px;
color: #333;
font-weight: 500;
- }
-
- .item-date {
- font-size: 12px;
- color: #666;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.item-details {
diff --git a/src/pages/sales/receiptPaymentLedger/index.vue b/src/pages/sales/receiptPaymentLedger/index.vue
index 7daa076..e038083 100644
--- a/src/pages/sales/receiptPaymentLedger/index.vue
+++ b/src/pages/sales/receiptPaymentLedger/index.vue
@@ -1,167 +1,261 @@
<template>
<view class="sales-account">
- <!-- 浣跨敤閫氱敤椤甸潰澶撮儴缁勪欢 -->
<PageHeader title="瀹㈡埛寰�鏉�"
@back="goBack" />
+
+ <!-- 缁熻鏉★紙闇�姹備竷锛氬簲鏀舵憳瑕侊級 -->
+ <view class="summary-strip">
+ <view class="summary-cell">
+ <text class="summary-num">{{ summary.totalProjects || 0 }}</text>
+ <text class="summary-cap">椤圭洰</text>
+ </view>
+ <view class="summary-cell">
+ <text class="summary-num">{{ summary.totalCustomers || 0 }}</text>
+ <text class="summary-cap">瀹㈡埛</text>
+ </view>
+ <view class="summary-cell">
+ <text class="summary-num">{{ summary.totalContracts || 0 }}</text>
+ <text class="summary-cap">鍚堝悓</text>
+ </view>
+ <view class="summary-cell">
+ <text class="summary-num">{{ fmtMoney(summary.totalAmount) }}</text>
+ <text class="summary-cap">绛剧害鎬婚</text>
+ </view>
+ <view class="summary-cell overdue">
+ <text class="summary-num">{{ summary.totalOverdueCount || 0 }}</text>
+ <text class="summary-cap">瓒�30澶╂湭鍥炴</text>
+ </view>
+ </view>
+
<!-- 鎼滅储鍖哄煙 -->
<view class="search-section">
<view class="search-bar">
<view class="search-input">
- <up-input class="search-text"
- placeholder="璇疯緭鍏ュ鎴峰悕绉�"
- v-model="searchForm.searchText"
- @change="searchChange"
- clearable />
+ <up-input v-model="query.projectName"
+ class="search-text"
+ placeholder="璇疯緭鍏ラ」鐩悕绉版悳绱�"
+ clearable
+ @change="getList" />
</view>
- <view class="search-button"
- @click="handleQuery">
- <up-icon name="search"
+ <view class="filter-button"
+ @click="showFilter = true">
+ <up-icon name="list"
size="24"
- color="#999"></up-icon>
+ color="#999" />
</view>
</view>
</view>
- <!-- 瀹㈡埛鍒楄〃 -->
- <view class="customer-list-container">
- <view class="customer-list"
- v-if="tableData.length > 0">
- <view v-for="(item, index) in tableData"
- :key="item.id"
- class="customer-item"
- @click="rowClickMethod(item)">
- <view class="item-header">
- <view class="item-left">
- <view class="customer-icon">
- <up-icon name="file-text"
- size="16"
- color="#ffffff"></up-icon>
- </view>
- <text class="customer-name">{{ item.customerName }}</text>
- </view>
- <view class="item-right">
- <up-icon name="arrow-right"
+
+ <!-- 椤圭洰鍒楄〃 -->
+ <view v-if="projectList.length > 0"
+ class="ledger-list">
+ <view v-for="item in projectList"
+ :key="item.projectId"
+ class="ledger-item"
+ @click="goDetail(item)">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text"
size="16"
- color="#999"></up-icon>
+ color="#ffffff" />
</view>
+ <text class="item-id">{{ item.projectName || "-" }}</text>
</view>
- <up-divider></up-divider>
- <view class="item-details">
- <view class="detail-row">
- <text class="detail-label">寮�绁ㄩ噾棰�(鍏�)</text>
- <text class="detail-value">{{ formattedNumber(item.invoiceTotal) }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">鍥炴閲戦(鍏�)</text>
- <text class="detail-value">{{ formattedNumber(item.receiptPaymentAmount) }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">搴旀敹閲戦(鍏�)</text>
- <text class="detail-value highlight danger">{{ formattedNumber(item.unReceiptPaymentAmount) }}</text>
- </view>
+ <up-tag :text="overdueText(item.overdueCount)"
+ :type="item.overdueCount > 0 ? 'error' : 'success'"
+ size="mini"
+ shape="circle" />
+ </view>
+ <up-divider></up-divider>
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">椤圭洰缂栧彿</text>
+ <text class="detail-value">{{ item.projectCode || "-" }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">绛剧害鎬婚</text>
+ <text class="detail-value highlight">{{ fmtMoney(item.contractAmounts) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">寮�绁ㄩ噾棰�</text>
+ <text class="detail-value">{{ fmtMoney(item.invoiceAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍥炴閲戦</text>
+ <text class="detail-value highlight">{{ fmtMoney(item.receiptPaymentAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">搴旀敹閲戦</text>
+ <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">閫�璐ч噾棰�</text>
+ <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍥炴杩涘害</text>
+ <text class="detail-value"
+ :class="{ danger: item.overdueCount > 0 }">
+ {{ calcRate(item.receiptPaymentAmount, item.contractAmounts) }}%
+ </text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">瀹㈡埛 / 鍚堝悓</text>
+ <text class="detail-value">{{ item.customerCount || 0 }} 瀹� / {{ item.contractCount || 0 }} 浠�</text>
</view>
</view>
- </view>
- <view v-else
- class="no-data">
- <text>鏆傛棤瀹㈡埛鏁版嵁</text>
+ <view class="action-buttons">
+ <up-button class="action-btn"
+ size="small"
+ type="primary"
+ @click.stop="goDetail(item)">鏌ョ湅瀹㈡埛寰�鏉�</up-button>
+ </view>
</view>
</view>
+ <view v-else
+ class="no-data">
+ <text>鏆傛棤椤圭洰寰�鏉ユ暟鎹�</text>
+ </view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="瀹㈡埛寰�鏉ョ瓫閫�"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
</view>
</template>
<script setup>
- import { onMounted, ref, reactive, toRefs } from "vue";
+ import { reactive, ref, onMounted } from "vue";
import { onShow } from "@dcloudio/uni-app";
- import { invoiceLedgerSalesAccount } from "@/api/salesManagement/invoiceLedger";
+ import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
+ import {
+ projectTransactions,
+ transactionsSummary,
+ } from "@/api/salesManagement/indicatorStats";
+ import { unwrapList } from "@/utils/list";
- const tableData = ref([]);
+ const projectList = ref([]);
+ const summary = ref({});
+ const showFilter = ref(false);
- const page = reactive({
- current: -1,
- size: -1,
+ const query = reactive({
+ projectName: "",
+ customerName: "",
+ contractName: "",
});
- const data = reactive({
- searchForm: {
- searchText: "",
- invoiceDate: "",
- },
- });
+ const filterFields = [
+ { prop: "customerName", label: "瀹㈡埛鍚嶇О", type: "input", placeholder: "璇疯緭鍏ュ鎴峰悕绉�" },
+ { prop: "contractName", label: "鍚堝悓鍚嶇О", type: "input", placeholder: "璇疯緭鍏ュ悎鍚屽悕绉�" },
+ ];
- const { searchForm } = toRefs(data);
-
- // 杩斿洖涓婁竴椤�
const goBack = () => {
uni.navigateBack();
};
- // 鎼滅储妗嗗彉鍖栨椂瑙﹀彂
- const searchChange = val => {
- searchForm.value.searchText = val;
- getList();
- };
- // 鏌ヨ鍒楄〃
- const handleQuery = () => {
- getList();
- };
const getList = () => {
- showLoadingToast("鍔犺浇涓�...");
- invoiceLedgerSalesAccount({ ...searchForm.value, ...page })
+ uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
+ const params = { ...query, current: -1, size: -1 };
+ projectTransactions(params)
.then(res => {
- tableData.value = res.data.records;
- closeToast();
+ projectList.value = unwrapList(res);
+ uni.hideLoading();
})
.catch(() => {
- closeToast();
- uni.showToast({
- title: "鏌ヨ澶辫触",
- icon: "error",
- });
+ uni.hideLoading();
+ uni.showToast({ title: "鏌ヨ澶辫触", icon: "none" });
});
};
- const formattedNumber = value => {
- return parseFloat(value || 0).toFixed(2);
+ const getSummary = () => {
+ transactionsSummary()
+ .then(res => {
+ summary.value = res.data || {};
+ })
+ .catch(() => {});
};
- // 鏄剧ず鍔犺浇鎻愮ず
- const showLoadingToast = message => {
- uni.showLoading({
- title: message,
- mask: true,
- });
+ const onFilterSearch = values => {
+ Object.assign(query, values || {});
+ getList();
};
- // 鍏抽棴鎻愮ず
- const closeToast = () => {
- uni.hideLoading();
- };
-
- const rowClickMethod = row => {
- // 浣跨敤 uni.setStorageSync 瀛樺偍瀹㈡埛淇℃伅
- uni.setStorageSync("customerId", row.id);
- // 璺宠浆鍒板洖娆捐褰曟槑缁嗛〉闈�
+ const goDetail = item => {
+ if (!item?.projectId) return;
uni.navigateTo({
- url: "/pages/sales/receiptPaymentLedger/detail",
+ url: `/pages/sales/receiptPaymentLedger/detail?projectId=${item.projectId}`
+ + `&projectName=${encodeURIComponent(item.projectName || "")}`,
});
+ };
+
+ const overdueText = count => (count > 0 ? `${count} 浠借秴鏈焋 : "姝e父灞ョ害");
+
+ const fmtMoney = value => {
+ const num = Number(value);
+ if (!Number.isFinite(num)) return "楼 0.00";
+ return "楼 " + num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+ };
+
+ const calcRate = (paid, amount) => {
+ const total = Number(amount);
+ if (!Number.isFinite(total) || total === 0) return "0.0";
+ return (((Number(paid) || 0) / total) * 100).toFixed(1);
};
onShow(() => {
- // 椤甸潰鏄剧ず鏃跺埛鏂板垪琛�
getList();
+ getSummary();
});
onMounted(() => {
getList();
+ getSummary();
});
</script>
<style scoped lang="scss">
@import "@/styles/sales-common.scss";
- // 瀹㈡埛寰�鏉ョ壒鏈夋牱寮�
+ .summary-strip {
+ display: flex;
+ flex-wrap: wrap;
+ background: #ffffff;
+ margin: 12px 12px 0;
+ border-radius: 12px;
+ padding: 12px 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ .summary-cell {
+ flex: 1 0 33%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 6px 0;
+ }
+
+ .summary-num {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333333;
+ }
+
+ .summary-cap {
+ font-size: 11px;
+ color: #909399;
+ margin-top: 2px;
+ }
+
+ .summary-cell.overdue .summary-num {
+ color: #ff4757;
+ }
+
.detail-value.danger {
- color: #ff4757; // 涓庡叕鍏辨牱寮忎腑鐨� #ee0a24 涓嶅悓
+ color: #ff4757;
font-weight: 500;
}
-</style>
\ No newline at end of file
+</style>
diff --git a/src/pages/sales/salesAccount/detail.vue b/src/pages/sales/salesAccount/detail.vue
index f8a42ab..90acbc3 100644
--- a/src/pages/sales/salesAccount/detail.vue
+++ b/src/pages/sales/salesAccount/detail.vue
@@ -41,27 +41,102 @@
</template>
</up-form-item>
<up-form-item label="椤圭洰鍚嶇О"
- prop="projectName"
+ prop="projectId"
required>
- <up-input v-model="form.projectName"
- placeholder="璇疯緭鍏ラ」鐩悕绉�" />
+ <FormPicker v-model="form.projectId"
+ type="select"
+ title="閫夋嫨椤圭洰"
+ :options="projectOptions"
+ placeholder="璇烽�夋嫨椤圭洰"
+ @change="onProjectChange" />
</up-form-item>
<up-form-item label="绛捐鏃ユ湡"
prop="executionDate"
required>
- <up-input v-model="form.executionDate"
- readonly
- placeholder="鐐瑰嚮閫夋嫨鏃堕棿"
- @click="showDatePicker = true" />
- <template #right>
- <up-icon name="arrow-right"
- @click="showDatePicker = true"></up-icon>
- </template>
+ <FormPicker v-model="form.executionDate"
+ type="date"
+ placeholder="璇烽�夋嫨绛捐鏃ユ湡"
+ @change="onExecutionDateChange" />
+ </up-form-item>
+ <up-form-item label="骞翠唤">
+ <up-input :model-value="form.year === '' ? '鑷姩鍙栫璁㈡棩鏈�' : String(form.year)"
+ disabled />
</up-form-item>
<up-form-item label="浠樻鏂瑰紡"
prop="paymentMethod">
- <up-input v-model="form.paymentMethod"
- placeholder="璇疯緭鍏ヤ粯娆炬柟寮�" />
+ <FormPicker v-model="form.paymentMethod"
+ type="select"
+ title="閫夋嫨浠樻鏂瑰紡"
+ :display-text="paymentDisplayText"
+ :options="paymentOptions"
+ placeholder="璇烽�夋嫨浠樻鏂瑰紡" />
+ </up-form-item>
+ <up-form-item label="浜よ揣鏃ユ湡"
+ prop="deliveryDate">
+ <FormPicker v-model="form.deliveryDate"
+ type="date"
+ placeholder="璇烽�夋嫨浜よ揣鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="绛惧崟鍗曚綅"
+ prop="signingUnitId">
+ <FormPicker v-model="form.signingUnitId"
+ type="select"
+ title="閫夋嫨绛惧崟鍗曚綅"
+ :options="deptOptions"
+ placeholder="璇烽�夋嫨绛惧崟鍗曚綅" />
+ </up-form-item>
+ <up-form-item label="鍔炰簨澶�"
+ prop="officeId">
+ <FormPicker v-model="form.officeId"
+ type="select"
+ title="閫夋嫨鍔炰簨澶�"
+ :options="deptOptions"
+ placeholder="璇烽�夋嫨鍔炰簨澶�" />
+ </up-form-item>
+ <up-form-item label="缁勮礋璐d汉"
+ prop="groupLeaderId">
+ <FormPicker v-model="form.groupLeaderId"
+ type="select"
+ title="閫夋嫨缁勮礋璐d汉"
+ :options="userOptions"
+ placeholder="璇烽�夋嫨缁勮礋璐d汉" />
+ </up-form-item>
+ <up-form-item label="鏄惁寮�鍙戠エ">
+ <view class="switch-row">
+ <up-switch v-model="form.isInvoice"
+ :activeValue="1"
+ :inactiveValue="0" />
+ </view>
+ </up-form-item>
+ <up-form-item label="鏄惁涓爣">
+ <view class="switch-row">
+ <up-switch v-model="form.isBid"
+ :activeValue="1"
+ :inactiveValue="0" />
+ </view>
+ </up-form-item>
+ <up-form-item v-if="form.isBid === 1"
+ label="涓爣浠�">
+ <up-input v-model="form.bidPrice"
+ type="number"
+ placeholder="璇疯緭鍏ヤ腑鏍囦环" />
+ </up-form-item>
+ <up-form-item v-else
+ label="鏈腑鏍囧師鍥�">
+ <up-input v-model="form.unbidReason"
+ placeholder="璇疯緭鍏ユ湭涓爣鍘熷洜" />
+ </up-form-item>
+ <up-form-item label="鍚堝悓鍘熶欢绾﹀畾鍥炰紶鏃ユ湡">
+ <FormPicker v-model="form.contractOriginalReturnDate"
+ type="date"
+ placeholder="璇烽�夋嫨鍚堝悓鍘熶欢绾﹀畾鍥炰紶鏃ユ湡" />
+ </up-form-item>
+ <up-form-item label="澶囨敞">
+ <up-textarea v-model="form.remarks"
+ placeholder="璇疯緭鍏ュ娉�"
+ :maxlength="200"
+ count
+ auto-height />
</up-form-item>
<up-form-item label="褰曞叆浜�"
prop="entryPersonName">
@@ -81,16 +156,6 @@
title="閫夋嫨涓氬姟鍛�"
@select="onSalesmanSelect"
@close="showPicker = false" />
- <!-- 鏃ユ湡閫夋嫨 -->
- <up-popup :show="showDatePicker"
- mode="bottom"
- @close="showDatePicker = false">
- <up-datetime-picker :show="true"
- v-model="pickerDateValue"
- @confirm="onDateConfirm"
- @cancel="showDatePicker = false"
- mode="date" />
- </up-popup>
<!-- 瀹㈡埛閫夋嫨 -->
<up-action-sheet :show="showCustomerPicker"
:actions="customerActionList"
@@ -283,6 +348,18 @@
</up-form-item>
</view>
</view>
+ <!-- 闇�姹備簲.4锛氱绾﹂噾棰� = 鍚◣鍗曚环 脳 鏁伴噺锛屽嵆鍚◣鎬讳环锛屾寜浜у搧琛屾眹鎬� -->
+ <view v-if="productData.length > 0"
+ class="product-summary">
+ <view class="summary-row">
+ <text class="summary-label">绛剧害閲戦鍚堣(鍏�)</text>
+ <text class="summary-value highlight">{{ productTotals.taxInclusive }}</text>
+ </view>
+ <view class="summary-row">
+ <text class="summary-label">涓嶅惈绋庢�讳环鍚堣(鍏�)</text>
+ <text class="summary-value">{{ productTotals.taxExclusive }}</text>
+ </view>
+ </view>
</view>
</up-form>
<!-- 浣跨敤鍏叡搴曢儴鎸夐挳缁勪欢 -->
@@ -297,7 +374,8 @@
<script setup>
import { onMounted, ref, computed } from "vue";
import { userListNoPage } from "@/api/system/user";
- import { formatDateToYMD } from "@/utils/ruoyi";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess";
import {
addOrUpdateSalesLedger,
customerList,
@@ -305,10 +383,33 @@
modelList,
productTreeList,
} from "@/api/salesManagement/salesLedger";
+ import { getProject } from "@/api/projectManagement/project";
import useUserStore from "@/store/modules/user";
import { calculateTaxExclusiveTotalPrice } from "@/utils/summarizeTable";
+ import { useDict } from "@/utils/dict";
+ import { loadProjectOptions } from "@/utils/projectOptions";
+ import { takePrefillProject } from "@/utils/prefill";
+ import FormPicker from "@/components/FormPicker.vue";
import PageHeader from "@/components/PageHeader.vue";
import FooterButtons from "@/components/FooterButtons.vue";
+
+ const { checkout_payment } = useDict("checkout_payment");
+ const paymentOptions = computed(() =>
+ (checkout_payment.value || []).map(item => ({
+ label: item.label,
+ value: item.value,
+ }))
+ );
+
+ // 鏀瑰姩鍓嶄粯娆炬柟寮忔槸鑷敱鏂囨湰锛岃�佸崟鎹殑鍊煎彲鑳戒笉鍦ㄥ瓧鍏搁噷锛屾鏃跺洖鏄惧師鍊艰�屼笉鏄┖鐧�
+ const paymentDisplayText = computed(() => {
+ const value = form.value.paymentMethod;
+ if (!value) return "";
+ const matched = paymentOptions.value.find(
+ item => String(item.value) === String(value)
+ );
+ return matched ? matched.label : String(value);
+ });
// 鑾峰彇椤甸潰鍙傛暟
const operationType = ref("");
@@ -319,47 +420,47 @@
const form = ref({
id: "",
salesContractNo: "",
+ autoGenerateContractNo: true,
customerId: "",
customerName: "",
+ projectId: "",
projectName: "",
executionDate: "",
+ year: "",
paymentMethod: "",
+ deliveryDate: "",
+ signingUnitId: "",
+ officeId: "",
+ groupLeaderId: "",
+ isInvoice: 0,
+ isBid: 0,
+ bidPrice: "",
+ unbidReason: "",
+ contractOriginalReturnDate: "",
+ remarks: "",
entryPerson: "",
entryPersonName: "",
entryDate: "",
});
const showPicker = ref(false);
- const showDatePicker = ref(false);
- const pickerDateValue = ref(Date.now());
const showCustomerPicker = ref(false);
const userList = ref([]);
const customerOption = ref([]);
+ const projectOptions = ref([]);
+ const deptOptions = ref([]);
+ const userOptions = ref([]);
const userActionList = computed(() => {
return userList.value.map(user => ({
name: user.text,
value: user.value,
}));
});
- const formatter = (type, value) => {
- if (type === "year") {
- return `${value}`;
- }
- if (type === "month") {
- return `${value}`;
- }
- if (type === "day") {
- return `${value}`;
- }
- return value;
- };
const customerActionList = computed(() => {
return customerOption.value.map(customer => ({
name: customer.text,
value: customer.value,
}));
});
-
- // 鏃ユ湡閫夋嫨鍒楄〃宸茬Щ闄わ紝鏀圭敤 up-datetime-picker
// 浜у搧澶х被閫夋嫨鍒楄〃
const categoryActionList = computed(() => {
@@ -444,7 +545,7 @@
customerName: [
{ required: true, message: "璇烽�夋嫨瀹㈡埛鍚嶇О", trigger: "change" },
],
- projectName: [{ required: true, message: "璇疯緭鍏ラ」鐩悕绉�", trigger: "blur" }],
+ projectId: [{ required: true, message: "璇烽�夋嫨椤圭洰", trigger: "change" }],
executionDate: [
{ required: true, message: "璇烽�夋嫨绛捐鏃ユ湡", trigger: "change" },
],
@@ -473,18 +574,85 @@
form.value.salesman = item.name;
};
- // 鏃ユ湡纭浜嬩欢
- const onDateConfirm = e => {
- form.value.executionDate = formatDateToYMD(e.value);
- // 淇濇寔pickerDateValue涓烘椂闂存埑鏍煎紡锛岃�屼笉鏄牸寮忓寲鐨勫瓧绗︿覆
- pickerDateValue.value = e.value;
- showDatePicker.value = false;
+ // 绛捐鏃ユ湡 鈫� 骞翠唤鑷姩鍙栧勾浠斤紙绠$悊绔悓姝ら�昏緫锛屽勾浠藉彧璇伙級
+ const onExecutionDateChange = value => {
+ form.value.year = value ? Number(String(value).substring(0, 4)) : "";
};
// 瀹㈡埛閫夋嫨浜嬩欢
const onCustomerSelect = item => {
form.value.customerName = item.name;
form.value.customerId = item.value;
+ };
+
+ // 闇�姹備簲.4锛氱绾﹂噾棰� = 鍚◣鍗曚环 脳 鏁伴噺锛岄�愯姹囨��
+ const productTotals = computed(() => {
+ const sumBy = key =>
+ (productData.value || [])
+ .reduce((sum, row) => sum + (Number(row?.[key]) || 0), 0)
+ .toFixed(2);
+ return {
+ taxInclusive: sumBy("taxInclusiveTotalPrice"),
+ taxExclusive: sumBy("taxExclusiveTotalPrice"),
+ };
+ });
+
+ // 闇�姹備簲.1锛氶�夐」鐩悗甯﹀嚭瀹㈡埛涓庝骇鍝佹槑缁嗭紙绠$悊绔� fetchAndPopulateProject 鍚岄�昏緫锛�
+ const applyProject = async id => {
+ const matched = projectOptions.value.find(
+ item => String(item.value) === String(id)
+ );
+ form.value.projectName = matched?.name || "";
+
+ if (!id) {
+ productData.value = [];
+ return;
+ }
+
+ try {
+ const res = await getProject(id);
+ const detail = res?.data?.data ?? res?.data ?? res;
+ const info = detail?.info || {};
+ if (info.clientId) form.value.customerId = info.clientId;
+ if (info.clientName) form.value.customerName = info.clientName;
+
+ const products = detail?.salesLedgerProductList;
+ productData.value = Array.isArray(products)
+ ? products.map(item => ({
+ ...item,
+ id: undefined,
+ salesLedgerId: undefined,
+ }))
+ : [];
+ } catch (error) {
+ uni.showToast({ title: "鑾峰彇椤圭洰淇℃伅澶辫触", icon: "none" });
+ }
+ };
+
+ // v-model 宸插厛鍐欏叆 projectId锛屽彇娑堟椂瑕佸洖婊氬埌涓婁竴涓」鐩�
+ const previousProjectId = ref("");
+
+ const confirmProjectChange = id => {
+ previousProjectId.value = id;
+ applyProject(id);
+ };
+
+ const onProjectChange = id => {
+ if (productData.value && productData.value.length > 0) {
+ uni.showModal({
+ title: "鎻愮ず",
+ content: "褰撳墠宸插綍鍏ヤ骇鍝佹槑缁嗭紝鍒囨崲椤圭洰灏嗚瑕嗙洊锛屾槸鍚︾户缁紵",
+ success: res => {
+ if (res.confirm) {
+ confirmProjectChange(id);
+ } else {
+ form.value.projectId = previousProjectId.value;
+ }
+ },
+ });
+ return;
+ }
+ confirmProjectChange(id);
};
// 鍘熸湁鐨勭‘璁ゆ柟娉曞凡琚柊鐨刟ction-sheet閫夋嫨鏂规硶鏇夸唬
@@ -820,33 +988,55 @@
const month = String(today.getMonth() + 1).padStart(2, "0");
const day = String(today.getDate()).padStart(2, "0");
form.value.entryDate = `${year}-${month}-${day}`;
- // 璁剧疆鏃ユ湡閫夋嫨鍣ㄩ粯璁ゅ�间负浠婂ぉ
- pickerDateValue.value = `${year}-${month}-${day}`;
};
// 濉厖琛ㄥ崟鏁版嵁锛堢紪杈戞ā寮忥級
const fillFormData = () => {
if (!editData.value) return;
- getSalesLedgerWithProducts({ id: editData.value.id, type: 1 }).then(res => {
- productData.value = res.productData;
- });
- console.log(editData.value);
- // 濉厖鍩烘湰淇℃伅
- form.value.salesContractNo = editData.value.salesContractNo || "";
- form.value.customerName = editData.value.customerName || "";
- form.value.projectName = editData.value.projectName || "";
- form.value.executionDate = editData.value.executionDate || "";
- form.value.paymentMethod = editData.value.paymentMethod || "";
- form.value.salesman = editData.value.salesman || "";
- form.value.entryPerson = editData.value.entryPerson || "";
- form.value.entryPersonName = editData.value.entryPersonName || "";
- form.value.entryDate = editData.value.entryDate || "";
- form.value.id = editData.value.id || "";
- form.value.customerId = editData.value.customerId || "";
+ const row = editData.value;
- // 璁剧疆鏃ユ湡閫夋嫨鍣ㄧ殑鍊�
- if (editData.value.executionDate) {
- pickerDateValue.value = editData.value.executionDate;
- }
+ // 鍏堢敤鍒楄〃琛屽悓姝ラ摵涓�閬嶏紝淇濊瘉璇锋眰杩斿洖鍓嶈〃鍗曚笉鏄┖鐨�
+ form.value.salesContractNo = row.salesContractNo || "";
+ form.value.customerName = row.customerName || "";
+ form.value.customerId = row.customerId || "";
+ form.value.projectId = row.projectId ?? "";
+ form.value.projectName = row.projectName || "";
+ form.value.executionDate = row.executionDate || "";
+ form.value.paymentMethod = row.paymentMethod || "";
+ form.value.deliveryDate = row.deliveryDate || "";
+ form.value.signingUnitId = row.signingUnitId ?? "";
+ form.value.officeId = row.officeId ?? "";
+ form.value.groupLeaderId = row.groupLeaderId ?? "";
+ form.value.isInvoice = row.isInvoice ?? 0;
+ form.value.isBid = row.isBid ?? 0;
+ form.value.bidPrice = row.bidPrice ?? "";
+ form.value.unbidReason = row.unbidReason || "";
+ form.value.contractOriginalReturnDate =
+ row.contractOriginalReturnDate || "";
+ form.value.remarks = row.remarks || "";
+ form.value.salesman = row.salesman || "";
+ form.value.entryPerson = row.entryPerson || "";
+ form.value.entryPersonName = row.entryPersonName || "";
+ form.value.entryDate = row.entryDate || "";
+ form.value.year = row.year || "";
+ form.value.id = row.id || "";
+ previousProjectId.value = form.value.projectId;
+
+ // 璇︽儏鎺ュ彛瀛楁姣斿垪琛ㄨ鍏紝鍐嶆暣浣撹鐩栦竴娆¤ˉ榻愭柊澧炲瓧娈�
+ getSalesLedgerWithProducts({ id: row.id, type: 1 })
+ .then(res => {
+ const data = res?.data ?? res ?? {};
+ productData.value = data.productData || [];
+ form.value = { ...form.value, ...data, id: row.id };
+ previousProjectId.value = form.value.projectId ?? "";
+ if (form.value.executionDate) {
+ form.value.year = Number(
+ String(form.value.executionDate).substring(0, 4)
+ );
+ }
+ })
+ .catch(() => {
+ uni.showToast({ title: "鑾峰彇鍙拌处璇︽儏澶辫触", icon: "none" });
+ });
};
const getUserList = () => {
userListNoPage().then(res => {
@@ -896,6 +1086,46 @@
});
};
+ // 椤圭洰涓嬫媺锛堥渶姹備簲.1 鐨勮仈鍔ㄥ叆鍙o級
+ const getProjectOptions = () => {
+ loadProjectOptions()
+ .then(options => {
+ projectOptions.value = options;
+ })
+ .catch(() => {
+ projectOptions.value = [];
+ });
+ };
+
+ // 绛惧崟鍗曚綅 / 鍔炰簨澶勫叡鐢ㄩ儴闂ㄤ笅鎷夛紝缁勮礋璐d汉鐢ㄥ憳宸ヤ笅鎷�
+ const getOrgOptions = async () => {
+ const [depts, users] = await Promise.all([
+ getDept().catch(() => ({})),
+ userListNoPageByTenantId().catch(() => ({})),
+ ]);
+ deptOptions.value = (depts?.data || []).map(item => ({
+ name: item.deptName,
+ value: item.deptId,
+ }));
+ const rows = users?.data || [];
+ userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({
+ name: item.nickName || item.userName || `鐢ㄦ埛${item.userId}`,
+ value: item.userId,
+ }));
+ };
+
+ // 闇�姹備笁.6锛氭嫑鎶曟爣涓爣鍚庡甫椤圭洰淇℃伅璺宠繘鏉ワ紙utils/prefill.js 鍐欏叆锛�
+ const applyPrefillProject = () => {
+ const prefill = takePrefillProject();
+ if (!prefill) return;
+ form.value.projectId = prefill.projectId ?? "";
+ form.value.projectName = prefill.projectName || "";
+ if (prefill.customerId) form.value.customerId = prefill.customerId;
+ if (prefill.customerName) form.value.customerName = prefill.customerName;
+ previousProjectId.value = form.value.projectId;
+ if (form.value.projectId) applyProject(form.value.projectId);
+ };
+
onMounted(() => {
// 鑾峰彇椤甸潰鍙傛暟
operationType.value = uni.getStorageSync("operationType") || "";
@@ -906,9 +1136,13 @@
getCustomerList();
// 鑾峰彇浜у搧澶х被tree鏁版嵁
getProductOptions();
+ // 鑾峰彇椤圭洰 / 閮ㄩ棬 / 鍛樺伐涓嬫媺
+ getProjectOptions();
+ getOrgOptions();
// 璧嬪�奸粯璁や俊鎭�
if (operationType.value === "add") {
setUserInfo();
+ applyPrefillProject();
}
// 鑾峰彇缂栬緫鏁版嵁骞跺~鍏呰〃鍗�
@@ -932,4 +1166,41 @@
<style lang="scss">
@import "@/static/scss/form-common.scss";
+
+ .switch-row {
+ display: flex;
+ justify-content: flex-end;
+ width: 100%;
+ }
+
+ .product-summary {
+ margin: 12px 0 0;
+ padding: 12px 14px;
+ background: #f8fbff;
+ border-radius: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .summary-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .summary-label {
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .summary-value {
+ font-size: 15px;
+ font-weight: 600;
+ color: #22324d;
+ }
+
+ .summary-value.highlight {
+ color: #2979ff;
+ }
</style>
diff --git a/src/pages/sales/salesAccount/goOut.vue b/src/pages/sales/salesAccount/goOut.vue
index 42321c9..2506425 100644
--- a/src/pages/sales/salesAccount/goOut.vue
+++ b/src/pages/sales/salesAccount/goOut.vue
@@ -82,6 +82,50 @@
</view>
</view>
</u-cell-group>
+ <!-- 鍗曟嵁涓庡綊妗o紙闇�姹傚叚锛� -->
+ <u-cell-group title="鍗曟嵁涓庡綊妗�"
+ class="form-section">
+ <up-form-item label="鍙戣揣鍗曟嵁"
+ prop="shippingDocument">
+ <up-input v-model="form.shippingDocument"
+ placeholder="璇疯緭鍏ュ彂璐у崟鎹�"
+ clearable />
+ </up-form-item>
+ <up-form-item label="鏄惁瀵硅处"
+ prop="isReconciled">
+ <up-switch v-model="form.isReconciled"
+ :active-value="1"
+ :inactive-value="0" />
+ </up-form-item>
+ <up-form-item v-if="form.isReconciled === 1"
+ label="瀵硅处鍗曞綊妗�"
+ prop="statementArchivingStatus">
+ <up-textarea v-model="form.statementArchivingStatus"
+ placeholder="璇峰~鍐欏璐﹀崟褰掓。鎯呭喌"
+ auto-height
+ maxlength="200" />
+ </up-form-item>
+ <up-form-item label="鏄惁缁撶畻"
+ prop="isSettled">
+ <up-switch v-model="form.isSettled"
+ :active-value="1"
+ :inactive-value="0" />
+ </up-form-item>
+ <up-form-item v-if="form.isSettled === 1"
+ label="缁撶畻鍗曞綊妗�"
+ prop="settlementArchivingStatus">
+ <up-textarea v-model="form.settlementArchivingStatus"
+ placeholder="璇峰~鍐欑粨绠楀崟褰掓。鎯呭喌"
+ auto-height
+ maxlength="200" />
+ </up-form-item>
+ <up-form-item label="鏄惁宸插紑绁�"
+ prop="isInvoiced">
+ <up-switch v-model="form.isInvoiced"
+ :active-value="1"
+ :inactive-value="0" />
+ </up-form-item>
+ </u-cell-group>
<!-- 鍙戣揣鍥剧墖 -->
<u-cell-group title="鍙戣揣鍥剧墖"
class="form-section">
@@ -133,6 +177,12 @@
shippingCarNumber: "",
expressCompany: "",
expressNumber: "",
+ shippingDocument: "",
+ isReconciled: 0,
+ statementArchivingStatus: "",
+ isSettled: 0,
+ settlementArchivingStatus: "",
+ isInvoiced: 0,
});
const rules = {
@@ -344,6 +394,16 @@
quantity: parseFloat(b.deliveryQuantity),
productModelId: goOutData.value.productModelId,
})),
+ // 闇�姹傚叚锛氬彂璐ф椂鍚屾鐧昏鍗曟嵁涓庡璐�/缁撶畻/寮�绁ㄦ儏鍐碉紝
+ // 鍚庣鏈帴鏀舵椂浼氳蹇界暐锛屼笉褰卞搷鍙戣揣涓绘祦绋�
+ shippingDocument: form.shippingDocument,
+ isReconciled: form.isReconciled,
+ statementArchivingStatus:
+ form.isReconciled === 1 ? form.statementArchivingStatus : "",
+ isSettled: form.isSettled,
+ settlementArchivingStatus:
+ form.isSettled === 1 ? form.settlementArchivingStatus : "",
+ isInvoiced: form.isInvoiced,
};
try {
diff --git a/src/pages/sales/salesAccount/index.vue b/src/pages/sales/salesAccount/index.vue
index 0b9c859..f51c57a 100644
--- a/src/pages/sales/salesAccount/index.vue
+++ b/src/pages/sales/salesAccount/index.vue
@@ -90,7 +90,7 @@
@click.stop="openOut(item)">
鍙戣揣鐘舵��
</u-button>
- <!-- <u-button class="detail-button"
+ <u-button class="detail-button"
size="small"
type="primary"
@click.stop="handleInfo('edit', item)">
@@ -98,18 +98,9 @@
</u-button>
<u-button class="detail-button"
size="small"
- type="primary"
- plain
- @click.stop="openOut(item)">
- 鍙戣揣鐘舵��
- </u-button> -->
- <!-- <u-button class="detail-button"
- size="small"
- type="error"
- plain
- @click.stop="handleDelete(item)">
- 鍒犻櫎
- </u-button> -->
+ @click.stop="openDetail(item)">
+ 璇︽儏
+ </u-button>
</view>
</view>
</view>
@@ -120,12 +111,12 @@
<text>鏆傛棤閿�鍞彴璐︽暟鎹�</text>
</view>
<!-- 娴姩鎿嶄綔鎸夐挳 -->
- <!-- <view class="fab-button"
+ <view class="fab-button"
@click="handleInfo('add')">
<up-icon name="plus"
size="24"
color="#ffffff"></up-icon>
- </view> -->
+ </view>
</view>
</template>
@@ -203,6 +194,14 @@
});
};
+ // 鍙璇︽儏锛歷iew.vue 浼氭寜 id 閲嶆柊鎷変竴閬嶅畬鏁村彴璐︼紝鍒楄〃琛屽彧浣滃崰浣�
+ const openDetail = item => {
+ uni.setStorageSync("editData", JSON.stringify(item));
+ uni.navigateTo({
+ url: "/pages/sales/salesAccount/view",
+ });
+ };
+
// 鍒犻櫎鍗曟潯閿�鍞彴璐�
const handleDelete = async row => {
if (!row || !row.id) return;
diff --git a/src/pages/sales/salesAccount/view.vue b/src/pages/sales/salesAccount/view.vue
index 53ffab5..0e26ccc 100644
--- a/src/pages/sales/salesAccount/view.vue
+++ b/src/pages/sales/salesAccount/view.vue
@@ -28,8 +28,46 @@
<text class="info-value">{{ form.executionDate }}</text>
</view>
<view class="info-item">
+ <text class="info-label">骞翠唤</text>
+ <text class="info-value">{{ form.year || "-" }}</text>
+ </view>
+ <view class="info-item">
<text class="info-label">浠樻鏂瑰紡</text>
- <text class="info-value">{{ form.paymentMethod }}</text>
+ <text class="info-value">{{ dictLabel(form.paymentMethod) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">浜よ揣鏃ユ湡</text>
+ <text class="info-value">{{ form.deliveryDate || "-" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">绛惧崟鍗曚綅</text>
+ <text class="info-value">{{ deptName(form.signingUnitId) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鍔炰簨澶�</text>
+ <text class="info-value">{{ deptName(form.officeId) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">缁勮礋璐d汉</text>
+ <text class="info-value">{{ userName(form.groupLeaderId) }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鏄惁寮�鍙戠エ</text>
+ <text class="info-value">{{ form.isInvoice === 1 ? "鏄�" : "鍚�" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鏄惁涓爣</text>
+ <text class="info-value">{{ form.isBid === 1 ? "鏄�" : "鍚�" }}</text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">{{ form.isBid === 1 ? "涓爣浠�" : "鏈腑鏍囧師鍥�" }}</text>
+ <text class="info-value">
+ {{ form.isBid === 1 ? form.bidPrice ?? "-" : form.unbidReason || "-" }}
+ </text>
+ </view>
+ <view class="info-item">
+ <text class="info-label">鍚堝悓鍘熶欢绾﹀畾鍥炰紶鏃ユ湡</text>
+ <text class="info-value">{{ form.contractOriginalReturnDate || "-" }}</text>
</view>
<view class="info-item">
<text class="info-label">褰曞叆浜�</text>
@@ -38,6 +76,11 @@
<view class="info-item">
<text class="info-label">褰曞叆鏃ユ湡</text>
<text class="info-value">{{ form.entryDate }}</text>
+ </view>
+ <view v-if="form.remarks"
+ class="info-item remarks-item">
+ <text class="info-label">澶囨敞</text>
+ <text class="info-value">{{ form.remarks }}</text>
</view>
</view>
</view>
@@ -107,13 +150,30 @@
class="no-product">
<text>鏆傛棤浜у搧淇℃伅</text>
</view>
+ <!-- 闇�姹備簲.4锛氱绾﹂噾棰� = 鍚◣鍗曚环 脳 鏁伴噺锛屾寜浜у搧琛屾眹鎬伙紙瀵归綈绠$悊绔� summarizeMainTable锛� -->
+ <view v-if="productData.length > 0"
+ class="product-summary">
+ <view class="summary-row">
+ <text class="summary-label">绛剧害閲戦鍚堣(鍏�)</text>
+ <text class="summary-value highlight">{{ productTotals.taxInclusive }}</text>
+ </view>
+ <view class="summary-row">
+ <text class="summary-label">涓嶅惈绋庢�讳环鍚堣(鍏�)</text>
+ <text class="summary-value">{{ productTotals.taxExclusive }}</text>
+ </view>
+ </view>
</view>
</template>
<script setup>
- import { onMounted, ref } from "vue";
+ import { computed, onMounted, ref } from "vue";
import { getSalesLedgerWithProducts } from "@/api/salesManagement/salesLedger";
+ import { getDept } from "@/api/collaborativeApproval/approvalProcess";
+ import { userListNoPageByTenantId } from "@/api/system/user";
+ import { useDict } from "@/utils/dict";
import PageHeader from "@/components/PageHeader.vue";
+
+ const { checkout_payment } = useDict("checkout_payment");
// 鑾峰彇椤甸潰鍙傛暟
const editData = ref(null);
@@ -134,6 +194,35 @@
// 浜у搧鏁版嵁
const productData = ref([]);
+ const deptOptions = ref([]);
+ const userOptions = ref([]);
+
+ const dictLabel = value => {
+ if (value === undefined || value === null || value === "") return "-";
+ const matched = (checkout_payment.value || []).find(
+ item => String(item.value) === String(value)
+ );
+ return matched ? matched.label : String(value);
+ };
+
+ // 鍚庣鍙瓨 id锛屽彧璇婚〉闇�瑕佽嚜宸卞弽鏌ラ儴闂� / 鍛樺伐鍚嶇О
+ const deptName = id =>
+ deptOptions.value.find(item => String(item.value) === String(id))?.name ||
+ "-";
+ const userName = id =>
+ userOptions.value.find(item => String(item.value) === String(id))?.name ||
+ "-";
+
+ const productTotals = computed(() => {
+ const sumBy = key =>
+ (productData.value || [])
+ .reduce((sum, row) => sum + (Number(row?.[key]) || 0), 0)
+ .toFixed(2);
+ return {
+ taxInclusive: sumBy("taxInclusiveTotalPrice"),
+ taxExclusive: sumBy("taxExclusiveTotalPrice"),
+ };
+ });
// 杩斿洖涓婁竴椤�
const goBack = () => {
@@ -153,7 +242,24 @@
});
};
+ const loadOrgOptions = async () => {
+ const [depts, users] = await Promise.all([
+ getDept().catch(() => ({})),
+ userListNoPageByTenantId().catch(() => ({})),
+ ]);
+ deptOptions.value = (depts?.data || []).map(item => ({
+ name: item.deptName,
+ value: item.deptId,
+ }));
+ const rows = users?.data || [];
+ userOptions.value = (Array.isArray(rows) ? rows : []).map(item => ({
+ name: item.nickName || item.userName || `鐢ㄦ埛${item.userId}`,
+ value: item.userId,
+ }));
+ };
+
onMounted(() => {
+ loadOrgOptions();
// 鑾峰彇缂栬緫鏁版嵁骞跺~鍏呰〃鍗�
const editDataStr = uni.getStorageSync("editData");
if (editDataStr) {
@@ -290,4 +396,40 @@
color: #999;
font-size: 0.875rem;
}
+
+ .remarks-item {
+ grid-column: 1 / -1;
+ }
+
+ .product-summary {
+ background: #fff;
+ margin: 1rem;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.04);
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ }
+
+ .summary-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .summary-label {
+ font-size: 0.8125rem;
+ color: #666;
+ }
+
+ .summary-value {
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: #333;
+ }
+
+ .summary-value.highlight {
+ color: #2979ff;
+ }
</style>
diff --git a/src/pages/sales/salesQuotation/detail.vue b/src/pages/sales/salesQuotation/detail.vue
index 45c3fd6..e780c5a 100644
--- a/src/pages/sales/salesQuotation/detail.vue
+++ b/src/pages/sales/salesQuotation/detail.vue
@@ -15,6 +15,10 @@
<text class="info-value">{{ detailData.customer || "-" }}</text>
</view>
<view class="info-item">
+ <text class="info-label">鎵�灞為」鐩�</text>
+ <text class="info-value">{{ detailData.projectName || "-" }}</text>
+ </view>
+ <view class="info-item">
<text class="info-label">涓氬姟鍛�</text>
<text class="info-value">{{ detailData.salesperson || "-" }}</text>
</view>
diff --git a/src/pages/sales/salesQuotation/edit.vue b/src/pages/sales/salesQuotation/edit.vue
index bfcb930..2490874 100644
--- a/src/pages/sales/salesQuotation/edit.vue
+++ b/src/pages/sales/salesQuotation/edit.vue
@@ -23,6 +23,16 @@
@click="showCustomerSheet = true"></up-icon>
</template>
</up-form-item>
+ <up-form-item label="鎵�灞為」鐩�"
+ prop="projectId">
+ <FormPicker v-model="form.projectId"
+ :display-text="form.projectName"
+ type="select"
+ title="閫夋嫨鎵�灞為」鐩�"
+ :options="projectOptions"
+ placeholder="璇烽�夋嫨鎵�灞為」鐩�"
+ @change="onProjectChange" />
+ </up-form-item>
<up-form-item label="涓氬姟鍛�"
prop="salesperson"
required>
@@ -199,7 +209,9 @@
import { onLoad } from "@dcloudio/uni-app";
import FooterButtons from "@/components/FooterButtons.vue";
import PageHeader from "@/components/PageHeader.vue";
+ import FormPicker from "@/components/FormPicker.vue";
import { formatDateToYMD } from "@/utils/ruoyi";
+ import { loadProjectOptions, pickProjectName } from "@/utils/projectOptions";
import { modelList, productTreeList } from "@/api/basicData/product";
import { userListNoPageByTenantId } from "@/api/system/user";
import {
@@ -224,6 +236,7 @@
const salespersonList = ref([]);
const productList = ref([]);
const modelActions = ref([]);
+ const projectOptions = ref([]);
let uidSeed = 1;
@@ -232,6 +245,8 @@
quotationNo: "",
customerId: undefined,
customer: "",
+ projectId: "",
+ projectName: "",
salesperson: "",
quotationDate: "",
validDate: "",
@@ -420,6 +435,31 @@
);
};
+ const onProjectChange = (value, item) => {
+ form.value.projectName = pickProjectName(item);
+ };
+
+ const loadProjectList = async () => {
+ try {
+ projectOptions.value = await loadProjectOptions();
+ } catch (error) {
+ projectOptions.value = [];
+ }
+ };
+
+ // 鎶ヤ环鍗曞彧甯� projectId 涓嶅甫鍚嶇О鏃讹紝浠庝笅鎷夐�夐」鍙嶆煡琛ュ叏锛岄伩鍏嶄繚瀛樻椂鎶婂悕绉版竻绌�
+ const resolveProjectName = () => {
+ if (form.value.projectName) return;
+ if (!form.value.projectId) {
+ form.value.projectName = "";
+ return;
+ }
+ const matched = projectOptions.value.find(
+ option => String(option.value) === String(form.value.projectId)
+ );
+ form.value.projectName = matched ? matched.name : "";
+ };
+
// 鏍规嵁鍚嶇О鍙嶆煡鑺傜偣 id锛屼究浜庝粎瀛樺悕绉版椂鐨勫弽鏄�
const findNodeIdByLabel = (nodes, label) => {
if (!label) return null;
@@ -498,6 +538,8 @@
quotationNo: data.quotationNo || "",
customerId: data.customerId,
customer: data.customer || "",
+ projectId: data.projectId ?? "",
+ projectName: data.projectName || "",
salesperson: data.salesperson || "",
quotationDate: data.quotationDate || "",
validDate: data.validDate || "",
@@ -546,6 +588,7 @@
// 鍚屾鏈�鏂扮殑鎬婚
form.value.totalAmount = totalAmount.value;
form.value.subtotal = totalAmount.value;
+ resolveProjectName();
const payload = {
...form.value,
@@ -586,7 +629,7 @@
});
onMounted(async () => {
- await fetchBaseOptions();
+ await Promise.all([fetchBaseOptions(), loadProjectList()]);
if (quotationId.value) {
await loadDetail();
}
diff --git a/src/pages/sales/salesQuotation/index.vue b/src/pages/sales/salesQuotation/index.vue
index 4ecf910..a2b9703 100644
--- a/src/pages/sales/salesQuotation/index.vue
+++ b/src/pages/sales/salesQuotation/index.vue
@@ -6,14 +6,14 @@
<view class="search-bar">
<view class="search-input">
<up-input class="search-text"
- v-model="quotationNo"
+ v-model="query.quotationNo"
placeholder="璇疯緭鍏ユ姤浠峰崟鍙锋悳绱�"
clearable
@change="getList" />
</view>
<view class="filter-button"
- @click="getList">
- <up-icon name="search"
+ @click="showFilter = true">
+ <up-icon name="list"
size="24"
color="#999"></up-icon>
</view>
@@ -49,6 +49,10 @@
<view class="detail-row">
<text class="detail-label">瀹㈡埛鍚嶇О</text>
<text class="detail-value">{{ item.customer || "-" }}</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>
@@ -103,20 +107,50 @@
size="28"
color="#ffffff"></up-icon>
</view>
+
+ <FilterPanel v-model:show="showFilter"
+ :model-value="query"
+ title="鎶ヤ环绛涢��"
+ :fields="filterFields"
+ @search="onFilterSearch"
+ @reset="onFilterSearch" />
</view>
</template>
<script setup>
- import { reactive, ref } from "vue";
+ import { computed, reactive, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import PageHeader from "@/components/PageHeader.vue";
+ import FilterPanel from "@/components/FilterPanel.vue";
import {
deleteQuotation,
getQuotationList,
} from "@/api/salesManagement/salesQuotation";
+ import { loadProjectOptions } from "@/utils/projectOptions";
- const quotationNo = ref("");
const quotationList = ref([]);
+ const showFilter = ref(false);
+ const projectOptions = ref([]);
+
+ const query = reactive({
+ quotationNo: "",
+ projectId: "",
+ });
+
+ const filterFields = computed(() => [
+ {
+ prop: "quotationNo",
+ label: "鎶ヤ环鍗曞彿",
+ type: "input",
+ placeholder: "璇疯緭鍏ユ姤浠峰崟鍙�",
+ },
+ {
+ prop: "projectId",
+ label: "鎵�灞為」鐩�",
+ type: "select",
+ options: projectOptions.value,
+ },
+ ]);
const tabList = reactive([
{ name: "鍏ㄩ儴", value: "" },
@@ -183,7 +217,8 @@
uni.showLoading({ title: "鍔犺浇涓�...", mask: true });
getQuotationList({
...page,
- quotationNo: quotationNo.value,
+ quotationNo: query.quotationNo,
+ projectId: query.projectId || undefined,
status: getCurrentStatus(),
})
.then(res => {
@@ -195,6 +230,26 @@
})
.finally(() => {
uni.hideLoading();
+ });
+ };
+
+ // FilterPanel 鍙洖鍚愮瓫閫夊�硷紝鍐欏洖 query 鍚庣粺涓�鏌ヨ
+ const onFilterSearch = values => {
+ Object.assign(query, values);
+ getList();
+ };
+
+ let projectsLoaded = false;
+
+ const loadProjects = () => {
+ if (projectsLoaded) return;
+ projectsLoaded = true;
+ loadProjectOptions()
+ .then(options => {
+ projectOptions.value = options;
+ })
+ .catch(() => {
+ projectsLoaded = false;
});
};
@@ -223,6 +278,7 @@
onShow(() => {
getList();
+ loadProjects();
});
</script>
diff --git a/src/pages/works.vue b/src/pages/works.vue
index 3b7ffa7..d2d6986 100644
--- a/src/pages/works.vue
+++ b/src/pages/works.vue
@@ -23,6 +23,29 @@
</up-grid>
</view>
</view>
+ <!-- 椤圭洰绠$悊妯″潡 -->
+ <view class="common-module project-module"
+ v-if="hasProjectItems">
+ <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 projectItems"
+ :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 marketing-module"
v-if="hasMarketingItems">
@@ -276,6 +299,29 @@
</up-grid>
</view>
</view>
+ <!-- 杞﹁締绠$悊妯″潡 -->
+ <view class="common-module vehicle-module"
+ v-if="hasVehicleItems">
+ <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 vehicleItems"
+ :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">
@@ -353,6 +399,30 @@
},
]);
+ // 椤圭洰绠$悊鍔熻兘鏁版嵁
+ const projectItems = reactive([
+ {
+ icon: "/static/images/icon/xiangmuguanli.svg",
+ label: "椤圭洰绠$悊",
+ },
+ ]);
+
+ // 杞﹁締绠$悊鍔熻兘鏁版嵁
+ const vehicleItems = reactive([
+ {
+ icon: "/static/images/icon/cheliangguanli.svg",
+ label: "杞﹁締绠$悊",
+ },
+ {
+ icon: "/static/images/icon/jiayoukaguanli.svg",
+ label: "鍔犳补鍗$鐞�",
+ },
+ {
+ icon: "/static/images/icon/jiayoujilu.svg",
+ label: "鍔犳补璁板綍",
+ },
+ ]);
+
// 钀ラ攢绠$悊鍔熻兘鏁版嵁
const marketingItems = reactive([
{
@@ -370,6 +440,22 @@
{
icon: "/static/images/icon/kehuwanglai.svg",
label: "瀹㈡埛寰�鏉�",
+ },
+ {
+ icon: "/static/images/icon/zhaotoubiaoguanli.svg",
+ label: "鎷涙姇鏍囩鐞�",
+ },
+ {
+ icon: "/static/images/icon/susongguanli.svg",
+ label: "璇夎绠$悊",
+ },
+ {
+ icon: "/static/images/icon/fahuotaizhang.svg",
+ label: "鍙戣揣鍙拌处",
+ },
+ {
+ icon: "/static/images/icon/hetongguanli.svg",
+ label: "閿�鍞悎鍚�",
},
]);
@@ -515,6 +601,10 @@
icon: "/static/images/icon/chuchangjianyan.svg",
label: "鍑哄巶妫�楠�",
},
+ {
+ icon: "/static/images/icon/baobiaoguanli.svg",
+ label: "鎶ヨ〃绠$悊",
+ },
]);
const safetyItems = reactive([
{
@@ -609,6 +699,14 @@
icon: "/static/images/icon/baoxiaoguanli.svg",
label: "璐圭敤鎶ラ攢",
path: "/pages/oa/ReimburseManage/cost-reimburse/index",
+ },
+ {
+ icon: "/static/images/icon/kuandaiyutongxin.svg",
+ label: "瀹藉甫涓庨�氫俊绠$悊",
+ },
+ {
+ icon: "/static/images/icon/fangwuzulin.svg",
+ label: "鎴垮眿绉熻祦绠$悊",
},
]);
@@ -759,6 +857,61 @@
case "瀹㈡埛寰�鏉�":
uni.navigateTo({
url: "/pages/sales/receiptPaymentLedger/index",
+ });
+ break;
+ case "椤圭洰绠$悊":
+ uni.navigateTo({
+ url: "/pages/projectManagement/project/index",
+ });
+ break;
+ case "鎷涙姇鏍囩鐞�":
+ uni.navigateTo({
+ url: "/pages/sales/biddingManagement/index",
+ });
+ break;
+ case "璇夎绠$悊":
+ uni.navigateTo({
+ url: "/pages/sales/lawsuitManagement/index",
+ });
+ break;
+ case "鍙戣揣鍙拌处":
+ uni.navigateTo({
+ url: "/pages/sales/deliveryLedger/index",
+ });
+ break;
+ case "閿�鍞悎鍚�":
+ uni.navigateTo({
+ url: "/pages/sales/contractManagement/index",
+ });
+ break;
+ case "鎶ヨ〃绠$悊":
+ uni.navigateTo({
+ url: "/pages/qualityManagement/inspectionReport/index",
+ });
+ break;
+ case "杞﹁締绠$悊":
+ uni.navigateTo({
+ url: "/pages/inventoryManagement/vehicleInfo/index",
+ });
+ break;
+ case "鍔犳补鍗$鐞�":
+ uni.navigateTo({
+ url: "/pages/inventoryManagement/fuelCard/index",
+ });
+ break;
+ case "鍔犳补璁板綍":
+ uni.navigateTo({
+ url: "/pages/inventoryManagement/fuelRecord/index",
+ });
+ break;
+ case "瀹藉甫涓庨�氫俊绠$悊":
+ uni.navigateTo({
+ url: "/pages/cooperativeOffice/broadbandCommunication/index",
+ });
+ break;
+ case "鎴垮眿绉熻祦绠$悊":
+ uni.navigateTo({
+ url: "/pages/cooperativeOffice/houseLeaseManagement/index",
});
break;
case "閲囪喘鍙拌处":
@@ -1322,6 +1475,9 @@
specialMapping: { 瑙勭珷鍒跺害: "瑙勭珷鍒跺害绠$悊" },
},
purchase: { specialMapping: { 渚涘簲鍟嗘。妗�: "渚涘簲鍟嗙鐞�" } },
+ // 鎵嬫満绔汉浜嬫ā鍧楀凡鍗犵敤銆屽悎鍚岀鐞嗐�峫abel锛堣蛋 /staff/staffContract锛夛紝
+ // 閿�鍞悎鍚屾敼鐢ㄣ�岄攢鍞悎鍚屻�峫abel锛屾澶勬槧灏勫洖鍚庣鐨勮彍鍗曟爣棰�
+ marketing: { specialMapping: { 閿�鍞悎鍚�: "鍚堝悓绠$悊" } },
};
console.log(allowedMenuTitles);
// 閫氱敤杩囨护鍑芥暟
@@ -1338,7 +1494,7 @@
};
// 杩囨护鍚勪釜妯″潡
- filterArray(marketingItems);
+ filterArray(marketingItems, menuMapping.marketing.specialMapping);
filterArray(designItems);
filterArray(purchaseItems, menuMapping.purchase.specialMapping);
filterArray(financeManagementItems);
@@ -1351,6 +1507,8 @@
filterArray(equipmentItems);
filterArray(archiveManagementItems);
filterArray(afterSalesServiceItems);
+ filterArray(projectItems);
+ filterArray(vehicleItems);
};
// 妫�鏌ユā鍧楁槸鍚︽湁鑿滃崟椤归渶瑕佹樉绀�
@@ -1375,6 +1533,8 @@
const hasArchiveManagementItems = computed(
() => archiveManagementItems.length > 0
);
+ const hasProjectItems = computed(() => projectItems.length > 0);
+ const hasVehicleItems = computed(() => vehicleItems.length > 0);
onMounted(() => {
// 姣忔杩涘叆棣栭〉閮藉己鍒跺埛鏂扮敤鎴蜂俊鎭拰璺敱鏉冮檺锛屼笉鍋氭湰鍦扮紦瀛樺垽鏂�
@@ -1753,6 +1913,14 @@
--module-color: #9c27b0;
}
+ .project-module {
+ --module-color: #0066ff;
+ }
+
+ .vehicle-module {
+ --module-color: #ff8a3d;
+ }
+
.module-header {
margin-bottom: 10px;
display: flex;
@@ -1999,6 +2167,14 @@
--module-color: #9c27b0;
}
+ .project-module {
+ --module-color: #0066ff;
+ }
+
+ .vehicle-module {
+ --module-color: #ff8a3d;
+ }
+
.module-header {
margin-bottom: 18px;
padding-left: 18px;
diff --git a/src/static/images/icon/baobiaoguanli.svg b/src/static/images/icon/baobiaoguanli.svg
new file mode 100644
index 0000000..1e0787c
--- /dev/null
+++ b/src/static/images/icon/baobiaoguanli.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#19BE6B"/>
+ <rect x="8.4" y="14" width="2.8" height="5.4" rx="0.8" fill="#A0E8C0"/>
+ <rect x="12.6" y="10.5" width="2.8" height="8.9" rx="0.8" fill="#FFFFFF"/>
+ <rect x="16.8" y="12.8" width="2.8" height="6.6" rx="0.8" fill="#A0E8C0"/>
+ <rect x="7" y="19.4" width="14" height="1.7" rx="0.85" fill="#FFFFFF"/>
+</svg>
diff --git a/src/static/images/icon/cheliangguanli.svg b/src/static/images/icon/cheliangguanli.svg
new file mode 100644
index 0000000..3474cbf
--- /dev/null
+++ b/src/static/images/icon/cheliangguanli.svg
@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#2F7BE0"/>
+ <path d="M9.2 13.2V11.6A1.8 1.8 0 0 1 11 9.8h6a1.8 1.8 0 0 1 1.8 1.8v1.6z" fill="#99D9FC"/>
+ <rect x="6" y="13.2" width="16" height="5.4" rx="2" fill="#FFFFFF"/>
+ <rect x="10.4" y="10.6" width="2.6" height="1.6" rx="0.5" fill="#2F7BE0"/>
+ <rect x="15" y="10.6" width="2.6" height="1.6" rx="0.5" fill="#2F7BE0"/>
+ <rect x="17.8" y="14.6" width="2.4" height="1.7" rx="0.85" fill="#99D9FC"/>
+ <circle cx="9.6" cy="18.6" r="1.9" fill="#FFFFFF"/>
+ <circle cx="9.6" cy="18.6" r="0.85" fill="#2F7BE0"/>
+ <circle cx="18.4" cy="18.6" r="1.9" fill="#FFFFFF"/>
+ <circle cx="18.4" cy="18.6" r="0.85" fill="#2F7BE0"/>
+</svg>
diff --git a/src/static/images/icon/fangwuzulin.svg b/src/static/images/icon/fangwuzulin.svg
new file mode 100644
index 0000000..58c3d08
--- /dev/null
+++ b/src/static/images/icon/fangwuzulin.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#4C9AFF"/>
+ <path d="M13.2 5.9a1.2 1.2 0 0 1 1.6 0l8.2 7.2a1.1 1.1 0 0 1-.7 1.9H5.7a1.1 1.1 0 0 1-.7-1.9z" fill="#FFFFFF"/>
+ <rect x="7.4" y="14.4" width="13.2" height="8" rx="1.4" fill="#FFFFFF"/>
+ <rect x="18.4" y="15.4" width="1.4" height="6" fill="#4C9AFF"/>
+ <rect x="8.2" y="15.4" width="1.4" height="6" fill="#4C9AFF"/>
+ <rect x="12.4" y="17" width="3.2" height="5.4" rx="0.8" fill="#4C9AFF"/>
+ <rect x="16.2" y="16.4" width="2.6" height="2.4" rx="0.6" fill="#BFDBFF"/>
+</svg>
diff --git a/src/static/images/icon/jiayoujilu.svg b/src/static/images/icon/jiayoujilu.svg
new file mode 100644
index 0000000..47c0fc8
--- /dev/null
+++ b/src/static/images/icon/jiayoujilu.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#12B7A0"/>
+ <rect x="6.5" y="7" width="10.5" height="15" rx="2.2" fill="#FFFFFF"/>
+ <rect x="8.8" y="9.4" width="6" height="4.2" rx="1" fill="#A7F0E4"/>
+ <rect x="8.8" y="15.2" width="6" height="1.6" rx="0.8" fill="#12B7A0"/>
+ <rect x="8.8" y="18" width="4.2" height="1.6" rx="0.8" fill="#12B7A0"/>
+ <path d="M20.2 9.4h1.8a1.8 1.8 0 0 1 1.8 1.8v5.6a1.8 1.8 0 1 1-3.6 0" fill="none" stroke="#FFFFFF" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
+</svg>
diff --git a/src/static/images/icon/jiayoukaguanli.svg b/src/static/images/icon/jiayoukaguanli.svg
new file mode 100644
index 0000000..98765d5
--- /dev/null
+++ b/src/static/images/icon/jiayoukaguanli.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#FF8A3D"/>
+ <rect x="4.6" y="8.4" width="18.8" height="11.6" rx="2.2" fill="#FFFFFF"/>
+ <rect x="4.6" y="11.2" width="18.8" height="2.6" fill="#FFD9B3"/>
+ <rect x="7.4" y="15.4" width="3.6" height="2.8" rx="0.9" fill="#FFB870"/>
+ <rect x="14.4" y="15.4" width="6.6" height="1.5" rx="0.75" fill="#FFD9B3"/>
+ <rect x="14.4" y="18" width="4.2" height="1.5" rx="0.75" fill="#FFD9B3"/>
+</svg>
diff --git a/src/static/images/icon/kuandaiyutongxin.svg b/src/static/images/icon/kuandaiyutongxin.svg
new file mode 100644
index 0000000..d51c014
--- /dev/null
+++ b/src/static/images/icon/kuandaiyutongxin.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#7C5CFC"/>
+ <path d="M6.6 12.2a10.4 10.4 0 0 1 14.8 0" fill="none" stroke="#C9BCFF" stroke-width="1.8" stroke-linecap="round"/>
+ <path d="M9.9 15.4a5.8 5.8 0 0 1 8.2 0" fill="none" stroke="#FFFFFF" stroke-width="1.8" stroke-linecap="round"/>
+ <circle cx="14" cy="19" r="1.9" fill="#FFFFFF"/>
+</svg>
diff --git a/src/static/images/icon/susongguanli.svg b/src/static/images/icon/susongguanli.svg
new file mode 100644
index 0000000..a666404
--- /dev/null
+++ b/src/static/images/icon/susongguanli.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#E6A23C"/>
+ <rect x="13.2" y="8" width="1.6" height="12" rx="0.8" fill="#FFFFFF"/>
+ <rect x="6.8" y="10.2" width="14.4" height="1.6" rx="0.8" fill="#FFFFFF"/>
+ <rect x="9.6" y="19.4" width="8.8" height="1.7" rx="0.85" fill="#FFFFFF"/>
+ <rect x="8" y="11.6" width="1" height="3.2" rx="0.5" fill="#FFE7A3"/>
+ <rect x="19" y="11.6" width="1" height="3.2" rx="0.5" fill="#FFE7A3"/>
+ <ellipse cx="8.5" cy="15.4" rx="2.6" ry="1.2" fill="#FFE7A3"/>
+ <ellipse cx="19.5" cy="15.4" rx="2.6" ry="1.2" fill="#FFE7A3"/>
+</svg>
diff --git a/src/static/images/icon/xiangmuguanli.svg b/src/static/images/icon/xiangmuguanli.svg
new file mode 100644
index 0000000..3d3c6a0
--- /dev/null
+++ b/src/static/images/icon/xiangmuguanli.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#0066FF"/>
+ <rect x="8" y="8.5" width="12" height="13.5" rx="2.5" fill="#FFFFFF"/>
+ <rect x="11" y="6" width="6" height="4" rx="2" fill="#7BF1FF"/>
+ <rect x="10.5" y="13" width="7" height="1.6" rx="0.8" fill="#0066FF"/>
+ <rect x="10.5" y="16.5" width="4.5" height="1.6" rx="0.8" fill="#0066FF"/>
+</svg>
diff --git a/src/static/images/icon/zhaotoubiaoguanli.svg b/src/static/images/icon/zhaotoubiaoguanli.svg
new file mode 100644
index 0000000..8fbeac8
--- /dev/null
+++ b/src/static/images/icon/zhaotoubiaoguanli.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28" fill="none">
+ <rect x="2" y="2" width="24" height="24" rx="7" fill="#3462CC"/>
+ <g transform="rotate(-45 14 14)">
+ <rect x="8.5" y="7.6" width="11" height="5" rx="1.6" fill="#FFFFFF"/>
+ <rect x="12.25" y="12.6" width="3.5" height="8" rx="1.6" fill="#99D9FC"/>
+ </g>
+ <circle cx="20.4" cy="20.4" r="3.6" fill="#3462CC"/>
+ <circle cx="20.4" cy="20.4" r="2.1" fill="#FFFFFF"/>
+</svg>
diff --git a/src/utils/delivery.js b/src/utils/delivery.js
new file mode 100644
index 0000000..8c9f997
--- /dev/null
+++ b/src/utils/delivery.js
@@ -0,0 +1,75 @@
+/**
+ * 鍙戣揣鍙拌处璇︽儏瑙e寘
+ *
+ * 鍚庣 /shippingInfo/getDateil/{id} 鐨勪骇鍝佹槑缁嗘暟缁勬墍鍦ㄥ瓧娈靛悕鍦ㄤ笉鍚屾祦绋嬩笅涓嶄竴鑷�
+ * 锛堝鎵硅鎯呰蛋 shippingProductDetailDtoList锛屽彂璐у彴璐﹁蛋 batchNoDetailList 绛夛級锛�
+ * 琛屽唴鐨勬壒娆″彿/鏁伴噺涔熸湁澶氬鍛藉悕銆傞泦涓湪杩欓噷鎺㈡祴锛岄〉闈㈤噷涓嶅嚭鐜拌8瀛楁鍚嶃��
+ */
+import { toFileList } from "@/utils/file";
+
+const LIST_KEYS = [
+ "batchNoDetailList",
+ "batchNoList",
+ "shippingBatchList",
+ "shippingInfoDetailList",
+ "shippingProductDetailDtoList",
+ "detailList",
+ "batchDetailList",
+ "rows",
+ "records",
+ "list",
+];
+
+function pickList(source) {
+ if (!source) return [];
+ for (const key of LIST_KEYS) {
+ if (Array.isArray(source[key])) return source[key];
+ }
+ return [];
+}
+
+const pick = (row, keys) => {
+ for (const key of keys) {
+ const value = row?.[key];
+ if (value !== undefined && value !== null && value !== "") return value;
+ }
+ return "";
+};
+
+/**
+ * 褰掍竴鍖栦骇鍝佹槑缁嗚
+ * @returns {Array<{batchNo, productName, specificationModel, deliveryQuantity, unit, remark}>}
+ */
+export function resolveDeliveryProducts(data) {
+ return pickList(data).map(row => ({
+ batchNo: pick(row, ["batchNo", "batchNumber"]),
+ productName: pick(row, ["productName", "product"]),
+ specificationModel: pick(row, ["specificationModel", "model", "specification"]),
+ deliveryQuantity: pick(row, ["deliveryQuantity", "quantity", "shippingQuantity"]),
+ unit: pick(row, ["unit", "measureUnit"]),
+ remark: pick(row, ["remark", "remarks"]),
+ }));
+}
+
+/**
+ * 鍙戣揣鍥剧墖鍥炴樉锛歞eliveryFileList / storageBlobVOs 涓ょ鍛藉悕
+ */
+export function resolveDeliveryFiles(data) {
+ return toFileList(
+ data?.deliveryFileList || data?.storageBlobVOs || data?.fileList
+ );
+}
+
+/**
+ * 閫�璐у崟鍙峰垪琛細鍏煎瀛楃涓叉暟缁勪笌 [{returnNo}] 涓ょ缁撴瀯
+ */
+export function resolveReturnNos(source) {
+ if (!Array.isArray(source)) return [];
+ return source
+ .map(item =>
+ typeof item === "string" || typeof item === "number"
+ ? String(item)
+ : item?.returnNo || item?.returnOrderNo || ""
+ )
+ .filter(Boolean);
+}
diff --git a/src/utils/detailCache.js b/src/utils/detailCache.js
new file mode 100644
index 0000000..acd43fa
--- /dev/null
+++ b/src/utils/detailCache.js
@@ -0,0 +1,30 @@
+/**
+ * 璇︽儏鏁版嵁鍔犺浇锛氭帴鍙d紭鍏堬紝缂撳瓨鍏滃簳
+ *
+ * 鍒楄〃椤佃烦杞椂浼� `uni.setStorageSync('xxxDetail', row)`锛屼絾鍒楄〃琛屽彧鏈夊垪琛ㄥ垪瀛楁锛�
+ * 闄勪欢銆佸瓙琛ㄣ�佸綊妗f儏鍐佃繖浜涘彧鏈夎鎯呮帴鍙f墠鏈夈�傝嫢缂栬緫椤靛厛璇荤紦瀛樺氨鍥炲~琛ㄥ崟锛�
+ * 鎻愪氦鏃朵細鎶婄己澶辩殑闄勪欢涓�骞舵竻绌猴紙鏁版嵁涓㈠け锛夈��
+ *
+ * 鎵�浠ョ粺涓�璧般�屽厛璇锋眰璇︽儏鎺ュ彛锛屽け璐ユ椂鎵嶇敤缂撳瓨鍏滃簳銆嶏紝缂撳瓨鍙敤浜庢帴鍙d笉鍙敤鏃剁殑闄嶇骇銆�
+ */
+
+/**
+ * @param {object} options
+ * @param {string} options.cacheKey 鍒楄〃椤靛啓鍏ョ殑 storage key
+ * @param {string} options.id 褰撳墠璁板綍 id
+ * @param {Function} options.fetcher 鎺ユ敹 id銆佽繑鍥� Promise 鐨勮鎯呮帴鍙�
+ * @returns {Promise<object|null>} 璇︽儏瀵硅薄锛屽彇涓嶅埌杩斿洖 null
+ */
+export async function loadDetailData({ cacheKey, id, fetcher }) {
+ const cached = cacheKey ? uni.getStorageSync(cacheKey) : null;
+ const fallback =
+ cached && String(cached.id) === String(id) ? cached : null;
+
+ try {
+ const res = await fetcher(id);
+ return res?.data || fallback || null;
+ } catch (error) {
+ if (fallback) return fallback;
+ throw error;
+ }
+}
diff --git a/src/utils/file.js b/src/utils/file.js
new file mode 100644
index 0000000..903546e
--- /dev/null
+++ b/src/utils/file.js
@@ -0,0 +1,156 @@
+/**
+ * 闄勪欢瀛楁鍏煎宸ュ叿
+ *
+ * 鍚庣鍚勬ā鍧楃殑闄勪欢瀛楁鍚嶄笌褰㈡�佷笉缁熶竴锛�
+ * 瀵硅薄鏁扮粍锛歴torageBlobVOs(鍥炴樉) / attachmentList / fileList锛岄」鍚� url||previewURL||fileUrl||path
+ * 閫楀彿瀛楃涓诧細attachmentUrls / attachmentIds
+ * 椤甸潰缁熶竴杞垚 [{url}] 浜ょ粰 CommonUpload锛屾彁浜ゆ椂鍐嶆寜鍚勬ā鍧楃害瀹氳浆鍥炪��
+ */
+import config from "@/config";
+import { getToken } from "@/utils/auth";
+
+/**
+ * 浠绘剰褰㈡�佺殑闄勪欢鏁版嵁 鈫� 瀵硅薄鏁扮粍锛堟瘡椤逛繚璇佹湁鍙覆鏌撶殑 url锛�
+ */
+export function toFileList(source) {
+ if (!source) return [];
+ if (Array.isArray(source)) {
+ return source
+ .map(item => {
+ if (typeof item === "string") return item ? { url: item } : null;
+ const url = item?.url || item?.previewURL || item?.fileUrl || item?.path;
+ return url ? { ...item, url } : null;
+ })
+ .filter(Boolean);
+ }
+ if (typeof source === "string") {
+ return source
+ .split(",")
+ .map(part => part.trim())
+ .filter(Boolean)
+ .map(url => ({ url }));
+ }
+ return [];
+}
+
+/**
+ * 瀵硅薄鏁扮粍 / 瀛楃涓叉暟缁� 鈫� 閫楀彿鍒嗛殧瀛楃涓诧紙attachmentUrls 杩欑被鍚庣瀛楁瑕佺殑褰㈡�侊級
+ */
+export function toUrlString(fileList) {
+ return (fileList || [])
+ .map(item =>
+ typeof item === "string" ? item : item?.url || item?.previewURL || ""
+ )
+ .filter(Boolean)
+ .join(",");
+}
+
+/**
+ * 闄勪欢 url 鈫� 鍙笅杞界殑瀹屾暣鍦板潃
+ * 闄勪欢鎺ュ彛鍥炵殑甯告槸 /profile/upload/... 杩欑被鍚庣鐩稿璺緞锛岀洿鎺ヤ涪缁� uni.downloadFile 浼氬け璐ワ紝
+ * 鎸夌鐞嗙 getRealUrl 鐨勫仛娉曡ˉ涓� baseUrl銆�
+ */
+export function resolveFileUrl(url) {
+ const target = String(url ?? "").trim();
+ if (!target) return "";
+ if (/^https?:\/\//i.test(target)) return target;
+ return `${config.baseUrl}${target.startsWith("/") ? "" : "/"}${target}`;
+}
+
+/**
+ * 鎵撳紑闄勪欢锛歳equest.ts 鏈�忎紶 responseType锛宐lob 涓嬭浇涓嶅彲鐢紝
+ * 鍙兘 downloadFile 鍒颁复鏃舵枃浠跺啀 openDocument銆�
+ */
+export function openFile(url) {
+ const target = resolveFileUrl(url);
+ if (!target) {
+ uni.showToast({ title: "鏂囦欢鍦板潃鏃犳晥", icon: "none" });
+ return;
+ }
+ uni.showLoading({ title: "鎵撳紑涓�...", mask: true });
+ uni.downloadFile({
+ url: target,
+ header: { Authorization: "Bearer " + getToken() },
+ success: res => {
+ if (res.statusCode !== 200) {
+ uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" });
+ return;
+ }
+ uni.openDocument({
+ filePath: res.tempFilePath,
+ showMenu: true,
+ fail: () => uni.showToast({ title: "鏆備笉鏀寔棰勮璇ユ枃浠�", icon: "none" }),
+ });
+ },
+ fail: () => uni.showToast({ title: "鏂囦欢涓嬭浇澶辫触", icon: "none" }),
+ complete: () => uni.hideLoading(),
+ });
+}
+
+/**
+ * 鍚庣鐢熸垚鏂囦欢锛圥DF/Word锛夊苟涓嬭浇鎵撳紑銆�
+ *
+ * 杩欑被鎺ュ彛鏄� POST + body锛岃�� uni.downloadFile 鍙敮鎸� GET锛屾墍浠ュ彧鑳藉厛鐢�
+ * uni.request 鎷� arraybuffer锛屽啀钀藉埌 USER_DATA_PATH 鍚� openDocument銆�
+ * uni.getFileSystemManager 鍦� H5 涓嶅瓨鍦紝H5 涓嬩細鎻愮ず涓嶆敮鎸併��
+ *
+ * @param {string} path 鎺ュ彛璺緞锛屽 /quality/qualityInspect/downPdf
+ * @param {object} data 璇锋眰浣�
+ * @param {string} fileName 钀界洏鏂囦欢鍚嶏紙鍚悗缂�锛�
+ */
+export function downloadFileByPost(path, data = {}, fileName = "download.pdf") {
+ uni.showLoading({ title: "鐢熸垚涓�...", mask: true });
+ return new Promise((resolve, reject) => {
+ uni.request({
+ url: `${config.baseUrl}${path}`,
+ method: "POST",
+ data,
+ responseType: "arraybuffer",
+ header: {
+ Authorization: "Bearer " + getToken(),
+ "Content-Type": "application/json",
+ },
+ success: res => {
+ const contentType = String(
+ res.header?.["Content-Type"] || res.header?.["content-type"] || ""
+ );
+ // 鍚庣鍑洪敊鏃跺洖鐨勬槸 JSON锛坈ode/msg锛夛紝鐓у師鏍峰啓鎴� pdf 浼氬緱鍒颁竴涓墦涓嶅紑鐨勬枃浠�
+ if (res.statusCode !== 200 || contentType.includes("application/json")) {
+ uni.showToast({ title: "鐢熸垚鏂囦欢澶辫触", icon: "none" });
+ reject(new Error("鐢熸垚鏂囦欢澶辫触"));
+ return;
+ }
+ const fs = uni.getFileSystemManager && uni.getFileSystemManager();
+ if (!fs) {
+ uni.showToast({ title: "褰撳墠鐜涓嶆敮鎸佷笅杞芥枃浠�", icon: "none" });
+ reject(new Error("unsupported"));
+ return;
+ }
+ const filePath = `${uni.env.USER_DATA_PATH}/${fileName}`;
+ fs.writeFile({
+ filePath,
+ data: res.data,
+ encoding: "binary",
+ success: () => {
+ uni.openDocument({
+ filePath,
+ showMenu: true,
+ fail: () =>
+ uni.showToast({ title: "鏆備笉鏀寔棰勮璇ユ枃浠�", icon: "none" }),
+ });
+ resolve(filePath);
+ },
+ fail: () => {
+ uni.showToast({ title: "鏂囦欢淇濆瓨澶辫触", icon: "none" });
+ reject(new Error("鏂囦欢淇濆瓨澶辫触"));
+ },
+ });
+ },
+ fail: () => {
+ uni.showToast({ title: "鐢熸垚鏂囦欢澶辫触", icon: "none" });
+ reject(new Error("鐢熸垚鏂囦欢澶辫触"));
+ },
+ complete: () => uni.hideLoading(),
+ });
+ });
+}
diff --git a/src/utils/list.js b/src/utils/list.js
new file mode 100644
index 0000000..c2995a0
--- /dev/null
+++ b/src/utils/list.js
@@ -0,0 +1,48 @@
+/**
+ * 鍒楄〃鍝嶅簲瑙e寘宸ュ叿
+ *
+ * 鍚庣鍚� controller 鐨勫垎椤佃繑鍥炵粨鏋勪笉缁熶竴锛屽疄娴嬭嚦灏戞湁杩欏嚑绉嶏細
+ * { code, data: { records, total } }
+ * { code, data: [ ... ] } 锛堢函鏁扮粍锛�
+ * { code, records, total } 锛坮uoyi 椋庢牸锛宺ecords 鍦ㄥ灞傦級
+ * { code, rows, total } 锛坮uoyi 椋庢牸锛�
+ * 闆嗕腑鍦ㄨ繖閲屽厹搴曪紝閬垮厤姣忎釜椤甸潰鍚勫啓涓�閬� `res?.data?.records || res?.records || []`銆�
+ */
+
+function pickArray(source) {
+ if (!source) return null;
+ if (Array.isArray(source)) return source;
+ const candidates = ["records", "list", "rows", "data", "content", "items"];
+ for (const key of candidates) {
+ if (Array.isArray(source[key])) return source[key];
+ }
+ return null;
+}
+
+/**
+ * 浠庝换鎰忕粨鏋勭殑鍒嗛〉鍝嶅簲閲屽彇鍑哄垪琛ㄦ暟缁勶紝鍙栦笉鍒拌繑鍥� []
+ */
+export function unwrapList(res) {
+ if (!res) return [];
+ return pickArray(res.data) || pickArray(res) || [];
+}
+
+/**
+ * 鍙栧嚭鎬绘潯鏁帮紝鍙栦笉鍒拌繑鍥� 0
+ */
+export function unwrapTotal(res) {
+ if (!res) return 0;
+ const outer = Number(res.total);
+ if (Number.isFinite(outer) && outer > 0) return outer;
+ const inner = Number(res.data?.total);
+ if (Number.isFinite(inner) && inner > 0) return inner;
+ return 0;
+}
+
+/**
+ * 鍙栧嚭鍗曟潯璇︽儏锛屽彇涓嶅埌杩斿洖 null
+ */
+export function unwrapData(res) {
+ if (!res) return null;
+ return res.data ?? null;
+}
diff --git a/src/utils/paymentCycle.js b/src/utils/paymentCycle.js
new file mode 100644
index 0000000..a795295
--- /dev/null
+++ b/src/utils/paymentCycle.js
@@ -0,0 +1,33 @@
+/**
+ * 缂磋垂鍛ㄦ湡涓庣即璐规柟寮忛�夐」
+ *
+ * 瀹藉甫涓庨�氫俊绠$悊銆佹埧灞嬬璧佺鐞嗗叡鐢ㄥ悓涓�濂楀瓧鍏革細
+ * 缂磋垂鍛ㄦ湡 paymentCycle 鈥斺�� 瀛� Number锛�1 鏈堜粯 / 3 瀛d粯 / 6 鍗婂勾浠� / 12 骞翠粯锛夛紝鍚庣涔熺敤瀹冩帹绠椾笅娆$即璐规棩鏈�
+ * 缂磋垂鏂瑰紡 paymentMethod 鈥斺�� 瀛樹腑鏂囧瓧绗︿覆锛堢幇閲� / 閾惰杞处 / 鏀粯瀹� / 寰俊锛�
+ */
+
+export const PAYMENT_CYCLE_OPTIONS = [
+ { name: "鏈堜粯", value: 1 },
+ { name: "瀛d粯", value: 3 },
+ { name: "鍗婂勾浠�", value: 6 },
+ { name: "骞翠粯", value: 12 },
+];
+
+export const PAYMENT_METHOD_OPTIONS = [
+ { name: "鐜伴噾", value: "鐜伴噾" },
+ { name: "閾惰杞处", value: "閾惰杞处" },
+ { name: "鏀粯瀹�", value: "鏀粯瀹�" },
+ { name: "寰俊", value: "寰俊" },
+];
+
+const CYCLE_TEXT = {
+ 1: "鏈堜粯",
+ 3: "瀛d粯",
+ 6: "鍗婂勾浠�",
+ 12: "骞翠粯",
+};
+
+export function cycleText(cycle) {
+ if (cycle === null || cycle === undefined || cycle === "") return "-";
+ return CYCLE_TEXT[Number(cycle)] || "-";
+}
diff --git a/src/utils/prefill.js b/src/utils/prefill.js
new file mode 100644
index 0000000..c39eb66
--- /dev/null
+++ b/src/utils/prefill.js
@@ -0,0 +1,26 @@
+/**
+ * 璺ㄦā鍧椼�屽彂璧峰悎鍚屽綍鍏ャ�嶇殑棰勫~鍙傛暟
+ *
+ * 鎷涙姇鏍囦腑鏍囧悗锛堥渶姹備笁.6锛夐渶瑕佸甫鐫�椤圭洰淇℃伅璺冲埌閿�鍞彴璐︽柊澧為〉銆�
+ * 鎵嬫満绔攢鍞彴璐︽柊澧炶蛋 pages/sales/salesAccount/detail.vue锛�
+ * 璇ラ〉鍦� onMounted 璇诲彇鏈� key 瀹屾垚椤圭洰/瀹㈡埛鍥炲~銆�
+ * 椤圭洰瀛楁鍚嶄笌椤圭洰绠$悊鍚庣淇濇寔涓�鑷达紙title=椤圭洰鍚嶇О銆乧lientId/clientName=瀹㈡埛锛夛紝
+ * 鍦� setPrefillProject 閲岀粺涓�褰掍竴鍖栵紝璋冪敤鏂逛紶鍘熷琛屽嵆鍙��
+ */
+
+export const PREFILL_PROJECT_KEY = "prefillProject";
+
+export function setPrefillProject(project) {
+ uni.setStorageSync(PREFILL_PROJECT_KEY, {
+ projectId: project?.projectId ?? project?.id ?? "",
+ projectName: project?.projectName ?? project?.title ?? "",
+ customerId: project?.customerId ?? project?.clientId ?? "",
+ customerName: project?.customerName ?? project?.clientName ?? "",
+ });
+}
+
+export function takePrefillProject() {
+ const data = uni.getStorageSync(PREFILL_PROJECT_KEY);
+ uni.removeStorageSync(PREFILL_PROJECT_KEY);
+ return data || null;
+}
diff --git a/src/utils/projectOptions.js b/src/utils/projectOptions.js
new file mode 100644
index 0000000..af45238
--- /dev/null
+++ b/src/utils/projectOptions.js
@@ -0,0 +1,31 @@
+/**
+ * 椤圭洰涓嬫媺閫夐」
+ *
+ * 鎷涙姇鏍囥�佽瘔璁笺�侀攢鍞姤浠枫�侀攢鍞彴璐︾瓑澶氬闇�瑕併�岄�夋嫨鎵�灞為」鐩�嶏紝
+ * 鍚庣椤圭洰绠$悊杩斿洖鐨勫瓧娈靛悕涓嶇粺涓�锛堥」鐩悕绉板湪 title 涓婏紝id 鍦� id 涓婏級锛�
+ * 缁熶竴鍦ㄨ繖閲屽綊涓�鍖栨垚 FormPicker 璁よ瘑鐨� {name, value}锛屽苟淇濈暀 raw 渚涘洖濉娇鐢ㄣ��
+ */
+import { listProject } from "@/api/projectManagement/project";
+
+const pickProjectName = project =>
+ project?.title ?? project?.projectName ?? project?.name ?? "";
+
+export function normalizeProjectOptions(records = []) {
+ return (records || []).map(project => ({
+ name: pickProjectName(project),
+ value: project.id ?? project.projectId,
+ raw: project,
+ }));
+}
+
+/**
+ * 鎷夊彇椤圭洰鍒楄〃锛堜笉鍒嗛〉锛�
+ * @returns {Promise<Array<{name, value, raw}>>}
+ */
+export async function loadProjectOptions(params = {}) {
+ const res = await listProject({ current: -1, size: -1, ...params });
+ const records = res?.data?.records || res?.records || res?.data || [];
+ return normalizeProjectOptions(Array.isArray(records) ? records : []);
+}
+
+export { pickProjectName };
diff --git a/src/utils/warning.js b/src/utils/warning.js
new file mode 100644
index 0000000..b0976c4
--- /dev/null
+++ b/src/utils/warning.js
@@ -0,0 +1,137 @@
+/**
+ * 鍒版湡棰勮璁$畻宸ュ叿
+ *
+ * 鍚勪笟鍔℃ā鍧楃殑棰勮闃堝�间笉鍚岋細
+ * 杞﹁締骞存/淇濋櫓銆佹埧灞嬬璧佸悎鍚屻�佽澶囨牎鍑嗐�佹姤琛ㄦ湁鏁堟湡 鈫� 30 澶�
+ * 瀹藉甫涓庨�氫俊涓嬫缂磋垂 鈫� 15 澶�
+ * 閿�鍞彴璐﹀悎鍚屽師浠剁害瀹氬洖浼犳棩鏈� 鈫� 鎻愬墠 N 澶╋紙鐢遍」鐩鐞嗛厤缃喅瀹氾級
+ * 缁熶竴鐢� warnDays 浼犲叆锛岄伩鍏嶅悇椤甸潰鍚勫啓涓�濂楁棩鏈熷樊閫昏緫銆�
+ */
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+/**
+ * 瑙f瀽鍚庣杩斿洖鐨勬棩鏈燂紝鍏煎鏃堕棿鎴� / Date / 'YYYY-MM-DD' / 'YYYY/MM/DD' / 'YYYY-MM-DD HH:mm:ss'
+ */
+export function parseDate(value) {
+ if (value === undefined || value === null || value === "") return null;
+ if (value instanceof Date) {
+ return Number.isNaN(value.getTime()) ? null : new Date(value.getTime());
+ }
+ if (typeof value === "number") {
+ const d = new Date(value);
+ return Number.isNaN(d.getTime()) ? null : d;
+ }
+ const matched = String(value)
+ .trim()
+ .replace(/\//g, "-")
+ .replace("T", " ")
+ .match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
+ if (!matched) return null;
+ const date = new Date(
+ Number(matched[1]),
+ Number(matched[2]) - 1,
+ Number(matched[3])
+ );
+ return Number.isNaN(date.getTime()) ? null : date;
+}
+
+/**
+ * 璺濈鍒版湡鏃ョ殑鍓╀綑澶╂暟
+ * 姝f暟=杩樻湁 N 澶╋紝0=浠婂ぉ鍒版湡锛岃礋鏁�=宸查�炬湡 N 澶╋紝鏃犳硶瑙f瀽杩斿洖 null
+ */
+export function remainDays(dateStr) {
+ const target = parseDate(dateStr);
+ if (!target) return null;
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ target.setHours(0, 0, 0, 0);
+ return Math.round((target.getTime() - today.getTime()) / MS_PER_DAY);
+}
+
+/**
+ * 棰勮绾у埆锛歰verdue 宸查�炬湡 / warning 鍗冲皢鍒版湡 / normal 姝e父 / none 鏃犳棩鏈�
+ */
+export function warnLevel(dateStr, warnDays = 30) {
+ const days = remainDays(dateStr);
+ if (days === null) return "none";
+ if (days < 0) return "overdue";
+ if (days <= warnDays) return "warning";
+ return "normal";
+}
+
+export function warnColor(level) {
+ const colors = {
+ overdue: "#f56c6c",
+ warning: "#ff9900",
+ normal: "#19be6b",
+ none: "#909399",
+ };
+ return colors[level] || colors.none;
+}
+
+export function warnText(level, days) {
+ if (level === "overdue") return `宸查�炬湡${Math.abs(days)}澶ー;
+ if (level === "warning") return days === 0 ? "浠婂ぉ鍒版湡" : `鍗冲皢鍒版湡(${days}澶�)`;
+ if (level === "normal") return "姝e父";
+ return "";
+}
+
+/**
+ * 涓�娆℃嬁鍒版覆鏌撻璀︽爣璁版墍闇�鐨勫叏閮ㄤ俊鎭�
+ * @returns {{days: number|null, level: string, color: string, text: string}}
+ */
+export function getWarnInfo(dateStr, warnDays = 30) {
+ const days = remainDays(dateStr);
+ const level = warnLevel(dateStr, warnDays);
+ return {
+ days,
+ level,
+ color: warnColor(level),
+ text: warnText(level, days),
+ };
+}
+
+/**
+ * 鏄惁宸查�炬湡鎴栧嵆灏嗗埌鏈燂紙鐢ㄤ簬鍒楄〃绛涢�夈�侀珮浜級
+ */
+export function isExpiringSoon(dateStr, warnDays = 30) {
+ const level = warnLevel(dateStr, warnDays);
+ return level === "overdue" || level === "warning";
+}
+
+/**
+ * 鎸夊ぉ/鏈堟帹绠楁棩鏈燂紝鐢ㄤ簬"涓嬫缂磋垂鏃ユ湡 = 瀹夎鏃ユ湡 + 缂磋垂鍛ㄦ湡鏈�"杩欑被鑷姩璁$畻
+ * @returns {string} 'YYYY-MM-DD'锛屾棤娉曡绠楄繑鍥� ''
+ */
+export function addMonths(dateStr, months) {
+ const base = parseDate(dateStr);
+ if (!base || !Number.isFinite(Number(months))) return "";
+ const day = base.getDate();
+ base.setDate(1);
+ base.setMonth(base.getMonth() + Number(months));
+ // 澶勭悊 1/31 + 1 鏈堣繖绫绘孩鍑猴紝鍥為��鍒板綋鏈堟渶鍚庝竴澶�
+ const lastDay = new Date(base.getFullYear(), base.getMonth() + 1, 0).getDate();
+ base.setDate(Math.min(day, lastDay));
+ const pad = n => String(n).padStart(2, "0");
+ return `${base.getFullYear()}-${pad(base.getMonth() + 1)}-${pad(base.getDate())}`;
+}
+
+/**
+ * 鐢熸垚 [start, end] 涔嬮棿鐨勬湀浠藉垪琛紙鍚灏撅級锛岀敤浜庢埧灞嬬璧佺殑鏈堝害璐圭敤瀛愯〃
+ * @returns {string[]} ['2026-01', '2026-02', ...]
+ */
+export function monthRange(startStr, endStr) {
+ const start = parseDate(startStr);
+ const end = parseDate(endStr);
+ if (!start || !end || end < start) return [];
+ const months = [];
+ const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
+ const last = new Date(end.getFullYear(), end.getMonth(), 1);
+ while (cursor <= last && months.length < 600) {
+ const pad = n => String(n).padStart(2, "0");
+ months.push(`${cursor.getFullYear()}-${pad(cursor.getMonth() + 1)}`);
+ cursor.setMonth(cursor.getMonth() + 1);
+ }
+ return months;
+}
--
Gitblit v1.9.3