From fe167dd71a1300aeae07522db990d6b3fdb77a0e Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 12 三月 2026 13:26:02 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_New' into dev_中兴实强
---
src/api/basicData/product.js | 9
src/views/basicData/product/index.vue | 2
src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue | 226
src/api/projectManagement/project.js | 118
src/api/customerService/index.js | 57
src/api/projectManagement/projectType.js | 27
src/views/customerService/expiryAfterSales/components/formDia.vue | 42
src/views/productionManagement/productStructure/index.vue | 2
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue | 263
src/api/personnelManagement/class.js | 117
src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue | 181
src/views/procurementManagement/invoiceEntry/components/Modal.vue | 138
src/api/financialManagement/salesRefund.js | 41
multiple/assets/screen/login-background.png | 0
src/views/personnelManagement/socialSecuritySet/components/formDia.vue | 470 +
src/api/basicData/customerFile.js | 41
src/views/financialManagement/salesRefund/index.vue | 134
src/views/personnelManagement/monthlyStatistics/components/formDia.vue | 804 ++
src/layout/components/NotificationCenter/index.vue | 591
src/views/customerService/feedbackRegistration/index.vue | 641 +
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue | 146
src/views/procurementManagement/purchaseReturnOrder/New.vue | 618 +
src/api/personnelManagement/personalAttendanceRecords.js | 25
src/api/personnelManagement/bank.js | 34
src/views/qualityManagement/nonconformingManagement/index.vue | 2
src/views/personnelManagement/monthlyStatistics/components/auditDia.vue | 216
src/components/ProjectManagement/DiscussProgressDialog.vue | 141
src/api/personnelManagement/staffSalaryMain.js | 43
src/views/procurementManagement/procurementInvoiceLedger/index.vue | 31
src/views/productionManagement/processRoute/processRouteItem/index.vue | 2
src/views/procurementManagement/invoiceEntry/index.vue | 16
src/views/collaborativeApproval/approvalProcess/index.vue | 88
src/views/salesManagement/salesLedger/index.vue | 34
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue | 141
src/views/safeProduction/accidentReportingRecord/index.vue | 2
src/api/personnelManagement/socialSecuritySet.js | 46
src/views/customerService/afterSalesHandling/index.vue | 493
src/views/fileManagement/document/index.vue | 12
src/views/personnelManagement/socialSecuritySet/index.vue | 212
src/views/equipmentManagement/spareParts/index.vue | 122
src/views/salesManagement/receiptPayment/index.vue | 3
src/views/salesManagement/invoiceRegistration/index.vue | 62
src/views/collaborativeApproval/customerVisit/index.vue | 269
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue | 539
src/views/customerService/expiryAfterSales/index.vue | 72
src/views/personnelManagement/classsSheduling/index.vue | 1283 +++
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue | 316
src/api/collaborativeApproval/customerVisit.js | 10
src/views/personnelManagement/monthlyStatistics/index.vue | 407 +
src/api/personnelManagement/attendanceRules.js | 45
src/views/index.vue | 10
src/views/productionManagement/productionProcess/index.vue | 5
src/views/basicData/product/ProductSelectDialog.vue | 2
src/views/personnelManagement/attendanceCheckin/index.vue | 822 +-
multiple/assets/logo/PCDZLogo.png | 0
src/api/procurementManagement/purchase_return_order.js | 20
src/views/financialManagement/revenueManagement/Modal.vue | 2
src/views/productionManagement/productionProcess/Edit.vue | 18
src/views/salesManagement/returnOrder/index.vue | 295
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue | 515 +
pnpm-lock.yaml | 274
src/views/basicData/customerFile/index.vue | 1995 ++++-
index.html | 4
src/components/Upload/FileUpload.vue | 5
src/views/projectManagement/projectType/components/ProjectTypeDialog.vue | 471 +
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue | 150
src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue | 35
src/views/qualityManagement/processInspection/components/formDia.vue | 27
src/views/salesManagement/deliveryLedger/index.vue | 6
src/views/customerService/afterSalesHandling/components/formDia.vue | 36
src/views/personnelManagement/employeeRecord/index.vue | 67
src/views/salesManagement/salesQuotation/index.vue | 18
src/router/index.js | 13
src/views/procurementManagement/procurementLedger/index.vue | 2
src/views/qualityManagement/nonconformingManagement/components/formDia.vue | 49
src/views/productionManagement/productionReporting/index.vue | 5
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue | 26
src/views/productionManagement/productionOrder/index.vue | 2
src/views/projectManagement/roles/index.vue | 295
src/components/Dialog/FormDialog.vue | 17
src/views/productionManagement/productionProcess/New.vue | 20
src/views/customerService/feedbackRegistration/components/formDia.vue | 461 +
multiple/assets/favicon/PCDZico.ico | 0
src/views/projectManagement/projectType/index.vue | 516 +
src/components/SearchPanel/index.vue | 257
src/views/productionManagement/workOrder/index.vue | 244
src/views/reportAnalysis/productionAnalysis/components/center-center.vue | 8
src/components/ProjectManagement/ProgressReportDialog.vue | 282
src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue | 188
src/views/salesManagement/returnOrder/components/formDia.vue | 489 +
src/views/projectManagement/Management/components/formDia.vue | 1503 ++++
src/views/procurementManagement/purchaseReturnOrder/index.vue | 109
src/api/salesManagement/returnOrder.js | 82
src/views/basicData/product/ImportExcel/index.vue | 75
src/views/basicData/supplierManage/components/HomeTab.vue | 17
src/api/projectManagement/role.js | 35
src/views/qualityManagement/finalInspection/components/formDia.vue | 9
src/api/personnelManagement/monthlyStatistics.js | 65
src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue | 115
src/views/personnelManagement/dimission/components/formDia.vue | 18
src/views/system/user/index.vue | 82
src/views/salesManagement/indicatorStats/index.vue | 4
src/api/personnelManagement/staffOnJob.js | 9
src/views/projectManagement/Management/projectDetail.vue | 538 +
src/views/basicData/supplierManage/components/BlacklistTab.vue | 17
src/views/personnelManagement/dimission/index.vue | 4
src/views/safeProduction/hazardousMaterialsControl/index.vue | 10
src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue | 275
src/views/projectManagement/Management/index.vue | 430 +
109 files changed, 17,946 insertions(+), 2,436 deletions(-)
diff --git a/index.html b/index.html
index a684690..274352d 100644
--- a/index.html
+++ b/index.html
@@ -10,6 +10,10 @@
/>
<link rel="icon" href="/favicon.ico" />
<title>%VITE_APP_TITLE%</title>
+ <!-- 楂樺痉鍦板浘API -->
+ <script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
+ <!-- 楂樺痉鍦板浘鎼滅储鎻掍欢 -->
+ <script type="text/javascript" src="https://webapi.amap.com/loca?v=2.0.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
<!--[if lt IE 11
]><script>
window.location.href = "/html/ie.html";
diff --git a/multiple/assets/favicon/PCDZico.ico b/multiple/assets/favicon/PCDZico.ico
new file mode 100644
index 0000000..1d74c17
--- /dev/null
+++ b/multiple/assets/favicon/PCDZico.ico
Binary files differ
diff --git a/multiple/assets/logo/PCDZLogo.png b/multiple/assets/logo/PCDZLogo.png
new file mode 100644
index 0000000..b296504
--- /dev/null
+++ b/multiple/assets/logo/PCDZLogo.png
Binary files differ
diff --git a/multiple/assets/screen/login-background.png b/multiple/assets/screen/login-background.png
new file mode 100644
index 0000000..ace9d53
--- /dev/null
+++ b/multiple/assets/screen/login-background.png
Binary files differ
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 871e5c2..2b3e76c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,12 +11,21 @@
'@element-plus/icons-vue':
specifier: 2.3.1
version: 2.3.1(vue@3.4.31)
+ '@vue-office/docx':
+ specifier: ^1.6.3
+ version: 1.6.3(vue-demi@0.14.10(vue@3.4.31))(vue@3.4.31)
+ '@vue-office/excel':
+ specifier: ^1.7.14
+ version: 1.7.14(vue-demi@0.14.10(vue@3.4.31))(vue@3.4.31)
'@vueup/vue-quill':
specifier: 1.2.0
version: 1.2.0(vue@3.4.31)
'@vueuse/core':
specifier: 10.11.0
version: 10.11.0(vue@3.4.31)
+ autofit.js:
+ specifier: ^3.2.8
+ version: 3.2.8
axios:
specifier: 0.28.1
version: 0.28.1
@@ -53,6 +62,12 @@
pinia:
specifier: 2.1.7
version: 2.1.7(vue@3.4.31)
+ print-js:
+ specifier: ^1.6.0
+ version: 1.6.0
+ qrcode:
+ specifier: ^1.5.4
+ version: 1.5.4
sortablejs:
specifier: ^1.15.6
version: 1.15.6
@@ -65,6 +80,12 @@
vue-cropper:
specifier: 1.1.1
version: 1.1.1
+ vue-easy-lightbox:
+ specifier: ^1.19.0
+ version: 1.19.0(vue@3.4.31)
+ vue-esign:
+ specifier: ^1.1.4
+ version: 1.1.4
vue-router:
specifier: 4.4.0
version: 4.4.0(vue@3.4.31)
@@ -341,56 +362,67 @@
resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.40.2':
resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.40.2':
resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.40.2':
resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loongarch64-gnu@4.40.2':
resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-powerpc64le-gnu@4.40.2':
resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.40.2':
resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.40.2':
resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.40.2':
resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.40.2':
resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.40.2':
resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.40.2':
resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==}
@@ -442,6 +474,26 @@
vite: ^5.0.0
vue: ^3.2.25
+ '@vue-office/docx@1.6.3':
+ resolution: {integrity: sha512-Cs+3CAaRBOWOiW4XAhTwwxJ0dy8cPIf6DqfNvYcD3YACiLwO4kuawLF2IAXxyijhbuOeoFsfvoVbOc16A/4bZA==}
+ peerDependencies:
+ '@vue/composition-api': ^1.7.1
+ vue: ^2.0.0 || >=3.0.0
+ vue-demi: ^0.14.6
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+
+ '@vue-office/excel@1.7.14':
+ resolution: {integrity: sha512-pVUgt+emDQUnW7q22CfnQ+jl43mM/7IFwYzOg7lwOwPEbiVB4K4qEQf+y/bc4xGXz75w1/e3Kz3G6wAafmFBFg==}
+ peerDependencies:
+ '@vue/composition-api': ^1.7.1
+ vue: ^2.0.0 || >=3.0.0
+ vue-demi: ^0.14.6
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+
'@vue/compiler-core@3.4.31':
resolution: {integrity: sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==}
@@ -453,6 +505,9 @@
'@vue/compiler-dom@3.5.14':
resolution: {integrity: sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==}
+
+ '@vue/compiler-sfc@2.7.16':
+ resolution: {integrity: sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==}
'@vue/compiler-sfc@3.4.31':
resolution: {integrity: sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==}
@@ -592,6 +647,9 @@
engines: {node: '>= 4.5.0'}
hasBin: true
+ autofit.js@3.2.8:
+ resolution: {integrity: sha512-albZNwDIXvcRneEDyZLW3uAIOH0cUQG/TnCGQ7jpfnL0gPn/+1ZNVRuEz3ZuzZvVkQ4HQRplGHjUeMRtPNxjLQ==}
+
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -646,6 +704,10 @@
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'}
@@ -664,6 +726,9 @@
clipboard@2.0.11:
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
+
+ cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
clone@2.1.2:
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
@@ -766,6 +831,10 @@
supports-color:
optional: true
+ decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+
decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
@@ -800,6 +869,9 @@
delegate@3.2.0:
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
+
+ dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dom-serializer@0.2.2:
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
@@ -965,6 +1037,10 @@
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
@@ -1016,6 +1092,10 @@
fuse.js@6.6.2:
resolution: {integrity: sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==}
engines: {node: '>=10'}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
@@ -1351,6 +1431,10 @@
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'}
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
@@ -1514,6 +1598,18 @@
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@@ -1523,6 +1619,10 @@
pascalcase@0.1.1:
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
engines: {node: '>=0.10.0'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
@@ -1567,6 +1667,10 @@
pkg-types@2.1.0:
resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
+ pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+
posix-character-classes@0.1.1:
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
engines: {node: '>=0.10.0'}
@@ -1605,11 +1709,24 @@
resolution: {integrity: sha512-spBB5sgC4cv2YcW03f/IAUN1pgDJWNWD8FzkyY4mArLUMJW+KlQhlmUdKAHQuPfb00Jl5xIfImeOsf6YL8QK7Q==}
engines: {node: '>=0.10.0'}
+ prettier@2.8.8:
+ resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
+ print-js@1.6.0:
+ resolution: {integrity: sha512-BfnOIzSKbqGRtO4o0rnj/K3681BSd2QUrsIZy/+WdCIugjIswjmx3lDEZpXB2ruGf9d4b3YNINri81+J0FsBWg==}
+
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ qrcode@1.5.4:
+ resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
quansync@0.2.10:
resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
@@ -1658,6 +1775,13 @@
repeat-string@1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resolve-url@0.2.1:
resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
@@ -1712,6 +1836,9 @@
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
+
+ set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
@@ -2033,10 +2160,23 @@
'@vue/composition-api':
optional: true
+ vue-easy-lightbox@1.19.0:
+ resolution: {integrity: sha512-YxLXgjEn91UF3DuK1y8u3Pyx2sJ7a/MnBpkyrBSQkvU1glzEJASyAZ7N+5yDpmxBQDVMwCsL2VmxWGIiFrWCgA==}
+ engines: {node: '>=14.18.3'}
+ peerDependencies:
+ vue: ^3.0.0
+
+ vue-esign@1.1.4:
+ resolution: {integrity: sha512-7Ix5PdcyyhVfsvrT9a+yp5+36gbQ0/bpDO+QSLT58IgJ5t164PEptOy5Nslw8bZbk3n3Hc7SP5B8eXQ8X8W+OA==}
+
vue-router@4.4.0:
resolution: {integrity: sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==}
peerDependencies:
vue: ^3.2.0
+
+ vue@2.7.16:
+ resolution: {integrity: sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==}
+ deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.
vue@3.4.31:
resolution: {integrity: sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==}
@@ -2066,6 +2206,9 @@
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
engines: {node: '>= 0.4'}
+ which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+
which-typed-array@1.1.19:
resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
engines: {node: '>= 0.4'}
@@ -2075,6 +2218,10 @@
engines: {node: '>= 8'}
hasBin: true
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -2082,6 +2229,17 @@
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
+
+ y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+
+ yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+
+ yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
zrender@5.6.0:
resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==}
@@ -2314,6 +2472,16 @@
vite: 5.3.2(@types/node@22.15.18)(sass@1.77.5)
vue: 3.4.31
+ '@vue-office/docx@1.6.3(vue-demi@0.14.10(vue@3.4.31))(vue@3.4.31)':
+ dependencies:
+ vue: 3.4.31
+ vue-demi: 0.14.10(vue@3.4.31)
+
+ '@vue-office/excel@1.7.14(vue-demi@0.14.10(vue@3.4.31))(vue@3.4.31)':
+ dependencies:
+ vue: 3.4.31
+ vue-demi: 0.14.10(vue@3.4.31)
+
'@vue/compiler-core@3.4.31':
dependencies:
'@babel/parser': 7.27.2
@@ -2339,6 +2507,14 @@
dependencies:
'@vue/compiler-core': 3.5.14
'@vue/shared': 3.5.14
+
+ '@vue/compiler-sfc@2.7.16':
+ dependencies:
+ '@babel/parser': 7.27.2
+ postcss: 8.5.3
+ source-map: 0.6.1
+ optionalDependencies:
+ prettier: 2.8.8
'@vue/compiler-sfc@3.4.31':
dependencies:
@@ -2502,6 +2678,8 @@
atob@2.1.2: {}
+ autofit.js@3.2.8: {}
+
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -2586,6 +2764,8 @@
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
+ camelcase@5.3.1: {}
+
chalk@1.1.3:
dependencies:
ansi-styles: 2.2.1
@@ -2623,6 +2803,12 @@
good-listener: 1.2.2
select: 1.1.2
tiny-emitter: 2.1.0
+
+ cliui@6.0.0:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
clone@2.1.2: {}
@@ -2718,6 +2904,8 @@
dependencies:
ms: 2.1.3
+ decamelize@1.2.0: {}
+
decode-uri-component@0.2.2: {}
deep-equal@1.1.2:
@@ -2757,6 +2945,8 @@
delayed-stream@1.0.0: {}
delegate@3.2.0: {}
+
+ dijkstrajs@1.0.3: {}
dom-serializer@0.2.2:
dependencies:
@@ -3029,6 +3219,11 @@
dependencies:
to-regex-range: 5.0.1
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
follow-redirects@1.15.9: {}
for-each@0.3.5:
@@ -3076,6 +3271,8 @@
functions-have-names@1.2.3: {}
fuse.js@6.6.2: {}
+
+ get-caller-file@2.0.5: {}
get-intrinsic@1.3.0:
dependencies:
@@ -3423,6 +3620,10 @@
pkg-types: 2.1.0
quansync: 0.2.10
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
lodash-es@4.17.21: {}
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
@@ -3594,11 +3795,23 @@
object-keys: 1.1.1
safe-push-apply: 1.0.0
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
+ p-try@2.2.0: {}
+
package-json-from-dist@1.0.1: {}
parchment@1.1.4: {}
pascalcase@0.1.1: {}
+
+ path-exists@4.0.0: {}
path-key@3.1.1: {}
@@ -3634,6 +3847,8 @@
confbox: 0.2.2
exsolve: 1.0.5
pathe: 2.0.3
+
+ pngjs@5.0.0: {}
posix-character-classes@0.1.1: {}
@@ -3679,9 +3894,20 @@
posthtml-parser: 0.2.1
posthtml-render: 1.4.0
+ prettier@2.8.8:
+ optional: true
+
+ print-js@1.6.0: {}
+
proto-list@1.2.4: {}
proxy-from-env@1.1.0: {}
+
+ qrcode@1.5.4:
+ dependencies:
+ dijkstrajs: 1.0.3
+ pngjs: 5.0.0
+ yargs: 15.4.1
quansync@0.2.10: {}
@@ -3751,6 +3977,10 @@
repeat-element@1.1.4: {}
repeat-string@1.6.1: {}
+
+ require-directory@2.1.1: {}
+
+ require-main-filename@2.0.0: {}
resolve-url@0.2.1: {}
@@ -3824,6 +4054,8 @@
select@1.1.2: {}
semver@7.7.2: {}
+
+ set-blocking@2.0.0: {}
set-function-length@1.2.2:
dependencies:
@@ -4234,10 +4466,23 @@
dependencies:
vue: 3.4.31
+ vue-easy-lightbox@1.19.0(vue@3.4.31):
+ dependencies:
+ vue: 3.4.31
+
+ vue-esign@1.1.4:
+ dependencies:
+ vue: 2.7.16
+
vue-router@4.4.0(vue@3.4.31):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.4.31
+
+ vue@2.7.16:
+ dependencies:
+ '@vue/compiler-sfc': 2.7.16
+ csstype: 3.1.3
vue@3.4.31:
dependencies:
@@ -4285,6 +4530,8 @@
is-weakmap: 2.0.2
is-weakset: 2.0.4
+ which-module@2.0.1: {}
+
which-typed-array@1.1.19:
dependencies:
available-typed-arrays: 1.0.7
@@ -4299,6 +4546,12 @@
dependencies:
isexe: 2.0.0
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -4311,6 +4564,27 @@
string-width: 5.1.2
strip-ansi: 7.1.0
+ y18n@4.0.3: {}
+
+ yargs-parser@18.1.3:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+
+ yargs@15.4.1:
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+
zrender@5.6.0:
dependencies:
tslib: 2.3.0
diff --git a/src/api/basicData/customerFile.js b/src/api/basicData/customerFile.js
index c52b76e..6a5f049 100644
--- a/src/api/basicData/customerFile.js
+++ b/src/api/basicData/customerFile.js
@@ -50,3 +50,44 @@
})
}
+
+// 鏂板瀹㈡埛璺熻繘
+export function addCustomerFollow(data) {
+ return request({
+ url: '/basic/customer-follow/add',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼瀹㈡埛璺熻繘
+export function updateCustomerFollow(data) {
+ return request({
+ url: '/basic/customer-follow/edit',
+ method: 'put',
+ data: data,
+ })
+}
+// 鍒犻櫎瀹㈡埛璺熻繘
+export function delCustomerFollow(id) {
+ return request({
+ url: '/basic/customer-follow/'+id,
+ method: 'delete',
+ })
+}
+
+// 鍥炶鎻愰啋-鏂板/鏇存柊
+export function addReturnVisit(data) {
+ return request({
+ url: '/basic/customer-follow/return-visit',
+ method: 'post',
+ data: data
+ })
+}
+// 鑾峰彇鍥炶鎻愰啋璇︽儏
+export function getReturnVisit(id) {
+ return request({
+ url: '/basic/customer-follow/return-visit/' + id,
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/src/api/basicData/product.js b/src/api/basicData/product.js
index 10f8fd1..d29a064 100644
--- a/src/api/basicData/product.js
+++ b/src/api/basicData/product.js
@@ -56,3 +56,12 @@
params: query
})
}
+
+// 涓嬭浇浜у搧瀵煎叆妯℃澘
+export function downloadProductModelImportTemplate() {
+ return request({
+ url: '/basic/product/export',
+ method: 'get',
+ responseType: 'blob'
+ })
+}
\ No newline at end of file
diff --git a/src/api/collaborativeApproval/customerVisit.js b/src/api/collaborativeApproval/customerVisit.js
new file mode 100644
index 0000000..dce1fdf
--- /dev/null
+++ b/src/api/collaborativeApproval/customerVisit.js
@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 鑾峰彇鎷滆璁板綍鍒楄〃
+export function getVisitRecords(query) {
+ return request({
+ url: '/customerVisits/listPage',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/src/api/customerService/index.js b/src/api/customerService/index.js
index 9f458b6..571601a 100644
--- a/src/api/customerService/index.js
+++ b/src/api/customerService/index.js
@@ -59,27 +59,17 @@
})
}
// 鍞悗澶勭悊-闄勪欢鍒犻櫎
-export function afterSalesServiceFileDel(ids) {
+export function afterSalesServiceFileDel(id) {
return request({
- url: '/afterSalesService/file/del',
+ url: `/afterSalesService/file/del/${id}`,
method: 'delete',
- data: ids,
- })
-}
-
-// 鍞悗澶勭悊-缁翠慨璁板綍鍒楄〃
-export function afterSalesServiceRepairListPage(query) {
- return request({
- url: '/afterSalesService/repair/listPage',
- method: 'get',
- params: query,
})
}
// 涓存湡鍞悗绠$悊-鍒嗛〉鏌ヨ
export function expiryAfterSalesListPage(query) {
return request({
- url: '/expiryAfterSales/listPage',
+ url: '/afterSalesNearExpiryService/listPage',
method: 'get',
params: query,
})
@@ -88,7 +78,7 @@
// 涓存湡鍞悗绠$悊-鏂板
export function expiryAfterSalesAdd(query) {
return request({
- url: '/expiryAfterSales/add',
+ url: '/afterSalesNearExpiryService/add',
method: 'post',
data: query,
})
@@ -97,17 +87,46 @@
// 涓存湡鍞悗绠$悊-鏇存柊
export function expiryAfterSalesUpdate(query) {
return request({
- url: '/expiryAfterSales/update',
+ url: '/afterSalesNearExpiryService/update',
method: 'post',
data: query,
})
}
// 涓存湡鍞悗绠$悊-鍒犻櫎
-export function expiryAfterSalesDelete(query) {
+export function expiryAfterSalesDelete(ids) {
return request({
- url: '/expiryAfterSales/delete',
+ url: '/afterSalesNearExpiryService/delete?ids=' + ids,
method: 'delete',
- data: query,
})
-}
\ No newline at end of file
+}
+
+// 鏌ヨ鎵�鏈夊鎴蜂俊鎭�
+// /basic/customer/list
+export function getAllCustomerList(query) {
+ return request({
+ url: '/basic/customer/list',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鏍规嵁瀹㈡埛鏌ヨ閿�鍞鍗曞彿
+// afterSalesService/listSalesLedger
+export function getSalesLedger(query) {
+ return request({
+ url: '/afterSalesService/listSalesLedger',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鏍规嵁閿�鍞鍗曞彿鏌ヨ閿�鍞鍗曡鎯�
+// afterSalesService/count
+export function getSalesLedgerDetail(query) {
+ return request({
+ url: '/afterSalesService/count',
+ method: 'get',
+ params: query,
+ })
+}
diff --git a/src/api/financialManagement/salesRefund.js b/src/api/financialManagement/salesRefund.js
new file mode 100644
index 0000000..84b87fb
--- /dev/null
+++ b/src/api/financialManagement/salesRefund.js
@@ -0,0 +1,41 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍒楄〃
+// /salesRefundAmountOrder/page
+export const listPage = (params) => {
+ return request({
+ url: "/salesRefundAmountOrder/page",
+ method: "get",
+ params,
+ });
+};
+
+// 鏂板
+// /salesRefundAmountOrder/add
+export function add(data) {
+ return request({
+ url: "/salesRefundAmountOrder/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼
+// /salesRefundAmountOrder/update
+export function update(data) {
+ return request({
+ url: "/salesRefundAmountOrder/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎
+// /salesRefundAmountOrder/deleteByIds
+export function del(data) {
+ return request({
+ url: "/salesRefundAmountOrder/deleteByIds",
+ method: "delete",
+ data: data,
+ });
+}
\ No newline at end of file
diff --git a/src/api/personnelManagement/attendanceRules.js b/src/api/personnelManagement/attendanceRules.js
new file mode 100644
index 0000000..5b07b8b
--- /dev/null
+++ b/src/api/personnelManagement/attendanceRules.js
@@ -0,0 +1,45 @@
+import request from "@/utils/request";
+
+// 鑾峰彇鐝鍒楄〃
+export function getAttendanceRules(query) {
+ return request({
+ url: "/personalAttendanceLocationConfig/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鐝
+export function addAttendanceRule(data) {
+ return request({
+ url: "/personalAttendanceLocationConfig/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鏇存柊鐝
+export function updateAttendanceRule(data) {
+ return request({
+ url: "/attendanceRules/update",
+ method: "put",
+ data,
+ });
+}
+
+// 鍒犻櫎鐝
+export function deleteAttendanceRule(ids) {
+ return request({
+ url: `/personalAttendanceLocationConfig/del`,
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鑾峰彇鍗曚釜鐝璇︽儏
+export function getAttendanceRuleDetail(id) {
+ return request({
+ url: `/attendanceRules/detail/${id}`,
+ method: "get",
+ });
+}
diff --git a/src/api/personnelManagement/bank.js b/src/api/personnelManagement/bank.js
new file mode 100644
index 0000000..5e83e27
--- /dev/null
+++ b/src/api/personnelManagement/bank.js
@@ -0,0 +1,34 @@
+import request from "@/utils/request";
+
+// 閾惰绠$悊
+export function bankList() {
+ return request({
+ url: "/bank/list",
+ method: "get",
+ });
+}
+
+export function bankAdd(data) {
+ return request({
+ url: "/bank/add",
+ method: "post",
+ data,
+ });
+}
+
+export function bankUpdate(data) {
+ return request({
+ url: "/bank/update",
+ method: "post",
+ data,
+ });
+}
+
+export function bankDelete(ids) {
+ return request({
+ url: "/bank/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
diff --git a/src/api/personnelManagement/class.js b/src/api/personnelManagement/class.js
new file mode 100644
index 0000000..f5ae299
--- /dev/null
+++ b/src/api/personnelManagement/class.js
@@ -0,0 +1,117 @@
+// 鐝鐩稿叧鎺ュ彛
+
+import request from "@/utils/request";
+
+// 缁╂晥绠$悊-鐝-鍒嗛〉鏌ヨ
+export function page(query) {
+ return request({
+ url: "/personalShift/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-骞翠唤鍒嗛〉鏌ヨ
+export function pageYear(query) {
+ return request({
+ url: "/personalShift/pageYear",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鎺掔彮
+export function add(data) {
+ return request({
+ url: "/personalShift/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-鏌ヨ鏃堕棿閰嶇疆淇℃伅
+export function list(query) {
+ return request({
+ url: "/shiftTime/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-鏂板
+export function shiftAdd(data) {
+ return request({
+ url: "/shiftTime/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-淇敼
+export function shiftUpdate(data) {
+ return request({
+ url: "/shiftTime/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-鍒犻櫎
+export function shiftRemove(query) {
+ return request({
+ url: "/shiftTime/remove",
+ method: "delete",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-瀵煎嚭
+export function exportFile(query) {
+ return request({
+ url: "/personalShift/export",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-瀵煎嚭
+export function obtainItemParameterList(query) {
+ return request({
+ url: "/laboratoryScope/obtainItemParameterList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鐝鐘舵�佷慨鏀�
+export function update(data) {
+ return request({
+ url: "/personalShift/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鑾峰彇鐢ㄦ埛鍒楄〃
+// export function selectUserCondition(query) {
+// return request({
+// url: "/system/newUser/selectUserCondition",
+// method: "get",
+// params: query,
+// });
+// }
+export function selectUserCondition() {
+ return request({
+ url: '/system/user/userListNoPage',
+ method: 'get'
+ })
+}
+
+// 鏌ヨ鍦ㄨ亴鍛樺伐鍙拌处
+export function staffOnJobListPage(query) {
+ return request({
+ url: '/staff/staffOnJob/listPage',
+ method: 'get',
+ params: query,
+ })
+}
\ No newline at end of file
diff --git a/src/api/personnelManagement/monthlyStatistics.js b/src/api/personnelManagement/monthlyStatistics.js
new file mode 100644
index 0000000..a070d0f
--- /dev/null
+++ b/src/api/personnelManagement/monthlyStatistics.js
@@ -0,0 +1,65 @@
+import request from "@/utils/request";
+
+// 浜哄憳钖祫鍙拌处鍒楄〃
+export function monthlyStatisticsListPage(query) {
+ return request({
+ url: "/compensationPerformance/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 浜哄憳钖祫鍙拌处璇︽儏
+export function monthlyStatisticsGet(id) {
+ return request({
+ url: "/monthlyStatistics/get",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板浜哄憳钖祫鍙拌处
+export function monthlyStatisticsAdd(data) {
+ return request({
+ url: "/compensationPerformance/add",
+ method: "post",
+ data,
+ });
+}
+
+// 缂栬緫浜哄憳钖祫鍙拌处
+export function monthlyStatisticsUpdate(data) {
+ return request({
+ url: "/compensationPerformance/update",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎浜哄憳钖祫鍙拌处
+export function monthlyStatisticsDelete(ids) {
+ return request({
+ url: "/compensationPerformance/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 瀵煎嚭浜哄憳钖祫鍙拌处
+export function monthlyStatisticsExport(query) {
+ return request({
+ url: "/compensationPerformance/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// 浜哄憳鍒楄〃
+export function staffOnJobList(query) {
+ return request({
+ url: "/staff/staffOnJob/list",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/personnelManagement/personalAttendanceRecords.js b/src/api/personnelManagement/personalAttendanceRecords.js
new file mode 100644
index 0000000..bdd9e1c
--- /dev/null
+++ b/src/api/personnelManagement/personalAttendanceRecords.js
@@ -0,0 +1,25 @@
+import request from "@/utils/request.js";
+
+export function createPersonalAttendanceRecord(params) {
+ return request({
+ url: "/personalAttendanceRecords",
+ method: "post",
+ data: params,
+ });
+}
+
+export function findPersonalAttendanceRecords(query) {
+ return request({
+ url: "/personalAttendanceRecords/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function findTodayPersonalAttendanceRecord(query) {
+ return request({
+ url: "/personalAttendanceRecords/today",
+ method: "get",
+ params: query,
+ });
+}
\ No newline at end of file
diff --git a/src/api/personnelManagement/socialSecuritySet.js b/src/api/personnelManagement/socialSecuritySet.js
new file mode 100644
index 0000000..29637f0
--- /dev/null
+++ b/src/api/personnelManagement/socialSecuritySet.js
@@ -0,0 +1,46 @@
+// 绀句細淇濋櫓璁剧疆
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ鍒楄〃
+export function socialSecurityListPage(query) {
+ return request({
+ url: "/schemeApplicableStaff/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ璇︽儏
+export function socialSecurityInfo(id) {
+ return request({
+ url: "/schemeApplicableStaff/" + id,
+ method: "get",
+ });
+}
+
+// 鏂板
+export function socialSecurityAdd(data) {
+ return request({
+ url: "/schemeApplicableStaff/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼
+export function socialSecurityUpdate(data) {
+ return request({
+ url: "/schemeApplicableStaff/updateSchemeApplicableStaff",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎
+export function socialSecurityDelete(ids) {
+ return request({
+ url: "/schemeApplicableStaff/delete",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/api/personnelManagement/staffOnJob.js b/src/api/personnelManagement/staffOnJob.js
index 7da5469..7a14391 100644
--- a/src/api/personnelManagement/staffOnJob.js
+++ b/src/api/personnelManagement/staffOnJob.js
@@ -17,6 +17,15 @@
})
}
+// 鏌ヨ鍛樺伐鍏ヨ亴淇℃伅
+export function getStaffOnJobInfoByUserName(query) {
+ return request({
+ url: '/staff/staffOnJob/byUserName',
+ method: 'get',
+ params: query,
+ })
+}
+
// 鏂板鍛樺伐
export function createStaffOnJob(params) {
return request({
diff --git a/src/api/personnelManagement/staffSalaryMain.js b/src/api/personnelManagement/staffSalaryMain.js
new file mode 100644
index 0000000..a42f96b
--- /dev/null
+++ b/src/api/personnelManagement/staffSalaryMain.js
@@ -0,0 +1,43 @@
+import request from "@/utils/request";
+
+// 鍛樺伐宸ヨ祫涓昏〃
+export function staffSalaryMainListPage(params) {
+ return request({
+ url: "/staffSalaryMain/listPage",
+ method: "get",
+ params,
+ });
+}
+
+export function staffSalaryMainCalculateSalary(ids) {
+ return request({
+ url: "/staffSalaryMain/calculateSalary",
+ method: "post",
+ data: ids,
+ });
+}
+
+export function staffSalaryMainAdd(data) {
+ return request({
+ url: "/staffSalaryMain/add",
+ method: "post",
+ data,
+ });
+}
+
+export function staffSalaryMainUpdate(data) {
+ return request({
+ url: "/staffSalaryMain/update",
+ method: "post",
+ data,
+ });
+}
+
+export function staffSalaryMainDelete(ids) {
+ return request({
+ url: "/staffSalaryMain/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
diff --git a/src/api/procurementManagement/purchase_return_order.js b/src/api/procurementManagement/purchase_return_order.js
new file mode 100644
index 0000000..33ec33e
--- /dev/null
+++ b/src/api/procurementManagement/purchase_return_order.js
@@ -0,0 +1,20 @@
+import request from "@/utils/request";
+// 閲囪喘閫�璐у崟
+
+// 鍒嗛〉鏌ヨ
+export function findPurchaseReturnOrderListPage(query) {
+ return request({
+ url: "/purchaseReturnOrders/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板
+export function createPurchaseReturnOrder(data) {
+ return request({
+ url: "/purchaseReturnOrders/add",
+ method: "post",
+ data
+ });
+}
\ No newline at end of file
diff --git a/src/api/projectManagement/project.js b/src/api/projectManagement/project.js
new file mode 100644
index 0000000..2d6eff1
--- /dev/null
+++ b/src/api/projectManagement/project.js
@@ -0,0 +1,118 @@
+import request from '@/utils/request'
+
+export function listProject(data) {
+ return request({
+ url: '/projectManagement/info/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getProject(id) {
+ return request({
+ url: `/projectManagement/info/${id}`,
+ method: 'post'
+ })
+}
+
+export function addProject(data) {
+ return request({
+ url: '/projectManagement/info/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateProject(data) {
+ return request({
+ url: '/projectManagement/info/save',
+ method: 'post',
+ data: data
+ })
+}
+
+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: data
+ })
+}
+
+export function submitProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 0 }
+ })
+}
+
+export function auditProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 1 }
+ })
+}
+
+export function reverseAuditProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 0 }
+ })
+}
+
+// 闃舵
+export function saveStage(data) {
+ return request({
+ url: '/projectManagement/info/saveStage',
+ method: 'post',
+ data: 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 listPlan(data) {
+ return request({
+ url: '/projectManagement/plan/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+export function addPlan(data) {
+ return request({
+ url: '/projectManagement/plan/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function delPlan(id) {
+ return request({
+ url: `/projectManagement/plan/delete/${id}`,
+ method: 'post'
+ })
+}
diff --git a/src/api/projectManagement/projectType.js b/src/api/projectManagement/projectType.js
new file mode 100644
index 0000000..8777e59
--- /dev/null
+++ b/src/api/projectManagement/projectType.js
@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+// 鏌ヨ椤圭洰绫诲瀷鍒楄〃
+export function listPlan(data) {
+ return request({
+ url: '/projectManagement/plan/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇濆瓨椤圭洰绫诲瀷锛堟柊澧�/淇敼锛�
+export function savePlan(data) {
+ return request({
+ url: '/projectManagement/plan/save',
+ method: 'post',
+ data: data
+ })
+}
+
+// 鍒犻櫎椤圭洰绫诲瀷
+export function deletePlan(id) {
+ return request({
+ url: `/projectManagement/plan/delete/${id}`,
+ method: 'post'
+ })
+}
diff --git a/src/api/projectManagement/role.js b/src/api/projectManagement/role.js
new file mode 100644
index 0000000..8d97a2a
--- /dev/null
+++ b/src/api/projectManagement/role.js
@@ -0,0 +1,35 @@
+import request from "@/utils/request";
+// 椤圭洰瑙掕壊
+
+// 鍒嗛〉鏌ヨ
+export function findRoleListPage(query) {
+ return request({
+ url: "/projectManagement/roles/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function createRole(params) {
+ return request({
+ url: "/projectManagement/roles/add",
+ method: "post",
+ data: params,
+ });
+}
+
+export function updateRole(params) {
+ return request({
+ url: "/projectManagement/roles/update",
+ method: "post",
+ data: params,
+ });
+}
+
+export function deleteRoles(params) {
+ return request({
+ url: "/projectManagement/roles/delete",
+ method: "delete",
+ data: params,
+ });
+}
\ No newline at end of file
diff --git a/src/api/salesManagement/returnOrder.js b/src/api/salesManagement/returnOrder.js
new file mode 100644
index 0000000..f945fc9
--- /dev/null
+++ b/src/api/salesManagement/returnOrder.js
@@ -0,0 +1,82 @@
+import request from "@/utils/request";
+
+
+// 閿�鍞��璐�-鏌ヨ
+// /returnManagement/listPage
+export function returnManagementList(query) {
+ return request({
+ url: "/returnManagement/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閿�鍞��璐�-娣诲姞
+// /returnManagement/add
+export function returnManagementAdd(data) {
+ return request({
+ url: "/returnManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 閿�鍞��璐�-淇敼
+// /returnManagement/update
+export function returnManagementUpdate(data) {
+ return request({
+ url: "/returnManagement/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 閿�鍞��璐�-鍒犻櫎
+// /returnManagement/del
+export function returnManagementDel(data) {
+ return request({
+ url: "/returnManagement/del",
+ method: "delete",
+ data,
+ });
+}
+
+// 閿�鍞��璐�-鏌ヨ
+// /returnManagement/getById
+export function returnManagementGetById(query) {
+ return request({
+ url: "/returnManagement/getById",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閿�鍞��璐�-鏍规嵁鍑哄簱鍗曟煡璇㈤攢鍞鍗曚互鍙婁骇鍝佷俊鎭�
+// /returnManagement/getByShippingId
+export function returnManagementGetByShippingId(query) {
+ return request({
+ url: "/returnManagement/getByShippingId",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閫氳繃瀹㈡埛鍚嶇О鏌ヨ
+// /shippingInfo/getByCustomerName
+export function getSalesLedger(query) {
+ return request({
+ url: '/shippingInfo/getByCustomerName',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 澶勭悊
+// /returnManagement/handle
+export function returnManagementHandle(data) {
+ return request({
+ url: "/returnManagement/handle",
+ method: "get",
+ params: data,
+ });
+}
diff --git a/src/components/Dialog/FormDialog.vue b/src/components/Dialog/FormDialog.vue
index 5e21b1d..8b657de 100644
--- a/src/components/Dialog/FormDialog.vue
+++ b/src/components/Dialog/FormDialog.vue
@@ -8,8 +8,18 @@
<slot></slot>
<template #footer>
<div class="dialog-footer">
- <el-button type="primary" @click="handleConfirm">纭</el-button>
- <el-button @click="handleCancel">鍙栨秷</el-button>
+ <!-- 鑷畾涔夋寜閽彃妲� -->
+ <slot name="footer">
+ <!-- 榛樿鎸夐挳 -->
+ <el-button
+ v-if="showConfirm"
+ type="primary"
+ @click="handleConfirm"
+ >
+ 纭
+ </el-button>
+ <el-button @click="handleCancel">鍙栨秷</el-button>
+ </slot>
</div>
</template>
</el-dialog>
@@ -44,6 +54,9 @@
set: (val) => emit('update:modelValue', val)
})
+// 璇︽儏妯″紡涓嶅睍绀衡�滅‘璁も�濇寜閽紝鍏跺畠绫诲瀷姝e父鏄剧ず
+const showConfirm = computed(() => props.operationType !== 'detail')
+
const computedTitle = computed(() => {
if (typeof props.title === 'function') {
return props.title(props.operationType)
diff --git a/src/components/ProjectManagement/DiscussProgressDialog.vue b/src/components/ProjectManagement/DiscussProgressDialog.vue
new file mode 100644
index 0000000..e278a50
--- /dev/null
+++ b/src/components/ProjectManagement/DiscussProgressDialog.vue
@@ -0,0 +1,141 @@
+<template>
+ <el-dialog v-model="visible" title="娲借皥杩涘害" width="700px" top="10vh" append-to-body destroy-on-close @close="handleClose">
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px">
+ <el-form-item label="椤圭洰闃舵" prop="planNodeId">
+ <el-select v-model="form.planNodeId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="璇疯緭鍏�" />
+ </el-form-item>
+
+ <el-form-item label="闄勪欢" prop="attachmentIds">
+ <el-upload
+ v-model:file-list="fileList"
+ :action="upload.url"
+ :headers="upload.headers"
+ multiple
+ name="files"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :on-remove="handleRemove"
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ <el-button type="danger" @click="submit">鎻愪氦</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup name="DiscussProgressDialog">
+import { computed, reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ projectId: { type: [Number, String], default: undefined },
+ planNodes: { type: Array, default: () => [] },
+ defaultPlanNodeId: { type: [Number, String], default: undefined }
+})
+
+const emit = defineEmits(['update:modelValue', 'submitted'])
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: v => emit('update:modelValue', v)
+})
+
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
+ headers: { Authorization: 'Bearer ' + getToken() }
+})
+
+const formRef = ref()
+const fileList = ref([])
+const form = ref({
+ planNodeId: undefined,
+ remark: '',
+ attachmentIds: []
+})
+
+const rules = {
+ planNodeId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ remark: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }]
+}
+
+const stageOptions = computed(() => {
+ const list = Array.isArray(props.planNodes) ? props.planNodes : []
+ const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ return sorted
+ .map(n => ({
+ label: n.name || n.workContent || n.title || String(n.id ?? ''),
+ value: n.id
+ }))
+ .filter(i => i.value !== undefined && i.value !== null && i.value !== '')
+})
+
+watch(
+ () => props.modelValue,
+ v => {
+ if (v) {
+ form.value = { planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value, remark: '', attachmentIds: [] }
+ fileList.value = []
+ }
+ }
+)
+
+function handleClose() {
+ formRef.value?.resetFields?.()
+}
+
+function handleUploadError() {
+ ElMessage.error('涓婁紶鏂囦欢澶辫触')
+}
+
+function handleUploadSuccess(res, file) {
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || '涓婁紶澶辫触')
+ return
+ }
+ const attachmentId = res?.data?.id ?? res?.data?.tempId ?? ''
+ if (!attachmentId) return
+ form.value.attachmentIds.push(attachmentId)
+ try {
+ file.attachmentId = attachmentId
+ } catch (e) {}
+ ElMessage.success('涓婁紶鎴愬姛')
+}
+
+function handleRemove(file) {
+ const attachmentId = file?.attachmentId
+ if (!attachmentId) return
+ form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
+}
+
+async function submit() {
+ await formRef.value?.validate?.()
+ emit('submitted', {
+ projectId: props.projectId,
+ ...form.value
+ })
+ visible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+</style>
diff --git a/src/components/ProjectManagement/ProgressReportDialog.vue b/src/components/ProjectManagement/ProgressReportDialog.vue
new file mode 100644
index 0000000..d9402c2
--- /dev/null
+++ b/src/components/ProjectManagement/ProgressReportDialog.vue
@@ -0,0 +1,282 @@
+<template>
+ <el-dialog v-model="visible" title="杩涘害姹囨姤" width="900px" top="8vh" append-to-body destroy-on-close @close="handleClose">
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px">
+ <el-form-item label="椤圭洰闃舵" prop="planNodeId">
+ <el-select v-model="form.planNodeId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="璁″垝寮�濮嬫椂闂�" prop="planStartTime">
+ <el-date-picker
+ v-model="form.planStartTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璁″垝瀹屽伐鏃堕棿" prop="planEndTime">
+ <el-date-picker
+ v-model="form.planEndTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹為檯寮�宸ユ棩鏈�" prop="actualStartTime">
+ <el-date-picker
+ v-model="form.actualStartTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鏈杩涘害鏃ユ湡" prop="reportDate">
+ <el-date-picker
+ v-model="form.reportDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="涓婃杩涘害(%)" prop="lastProgress">
+ <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹屾垚杩涘害(%)" prop="completionProgress">
+ <div style="display: flex; gap: 8px; width: 100%;">
+ <el-input-number v-model="form.completionProgress" :min="0" :max="100" controls-position="right" style="flex: 1" />
+ <el-button type="danger" @click="markDone">瀹屾垚</el-button>
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="绱杩涘害(%)" prop="totalProgress">
+ <el-input-number v-model="form.totalProgress" :min="0" :max="100" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹為檯瀹屽伐鏃ユ湡" prop="actualEndTime">
+ <el-date-picker
+ v-model="form.actualEndTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璐熻矗浜�" prop="managerName">
+ <el-input v-model="form.managerName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閮ㄩ棬" prop="departmentName">
+ <el-input v-model="form.departmentName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="闄勪欢" prop="attachmentIds">
+ <el-upload
+ v-model:file-list="fileList"
+ :action="upload.url"
+ :headers="upload.headers"
+ multiple
+ name="files"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :on-remove="handleRemove"
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="submit">纭畾</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup name="ProgressReportDialog">
+import { computed, reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ projectId: { type: [Number, String], default: undefined },
+ projectInfo: { type: Object, default: () => ({}) },
+ planNodes: { type: Array, default: () => [] },
+ defaultPlanNodeId: { type: [Number, String], default: undefined }
+})
+
+const emit = defineEmits(['update:modelValue', 'submitted'])
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: v => emit('update:modelValue', v)
+})
+
+const formRef = ref()
+const fileList = ref([])
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
+ headers: { Authorization: 'Bearer ' + getToken() }
+})
+
+const form = ref({
+ planNodeId: undefined,
+ planStartTime: '',
+ planEndTime: '',
+ actualStartTime: '',
+ actualEndTime: '',
+ reportDate: '',
+ lastProgress: 0,
+ completionProgress: 0,
+ totalProgress: 0,
+ managerName: '',
+ departmentName: '',
+ remark: '',
+ attachmentIds: []
+})
+
+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: 'change' }]
+}
+
+const stageOptions = computed(() => {
+ const list = Array.isArray(props.planNodes) ? props.planNodes : []
+ const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ return sorted
+ .map(n => ({
+ label: n.name || n.workContent || n.title || String(n.id ?? ''),
+ value: n.id
+ }))
+ .filter(i => i.value !== undefined && i.value !== null && i.value !== '')
+})
+
+function resetFromProject() {
+ const info = props.projectInfo || {}
+ form.value = {
+ planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value,
+ planStartTime: info.planStartTime || '',
+ planEndTime: info.planEndTime || '',
+ actualStartTime: info.actualStartTime || '',
+ actualEndTime: info.actualEndTime || '',
+ reportDate: '',
+ lastProgress: Number(info.lastProgress ?? 0) || 0,
+ completionProgress: 0,
+ totalProgress: Number(info.totalProgress ?? info.progress ?? 0) || 0,
+ managerName: info.managerName || '',
+ departmentName: info.departmentName || '',
+ remark: '',
+ attachmentIds: []
+ }
+ fileList.value = []
+}
+
+watch(
+ () => props.modelValue,
+ v => {
+ if (v) resetFromProject()
+ }
+)
+
+function handleClose() {
+ formRef.value?.resetFields?.()
+}
+
+function markDone() {
+ form.value.completionProgress = 100
+ form.value.totalProgress = 100
+ if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || ''
+}
+
+function handleUploadError() {
+ ElMessage.error('涓婁紶鏂囦欢澶辫触')
+}
+
+function handleUploadSuccess(res, file) {
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || '涓婁紶澶辫触')
+ return
+ }
+ const attachmentId = res?.data?.id ?? res?.data?.tempId ?? ''
+ if (!attachmentId) return
+ form.value.attachmentIds.push(attachmentId)
+ try {
+ file.attachmentId = attachmentId
+ } catch (e) {}
+ ElMessage.success('涓婁紶鎴愬姛')
+}
+
+function handleRemove(file) {
+ const attachmentId = file?.attachmentId
+ if (!attachmentId) return
+ form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
+}
+
+async function submit() {
+ await formRef.value?.validate?.()
+ emit('submitted', {
+ projectId: props.projectId,
+ ...form.value
+ })
+ visible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+</style>
diff --git a/src/components/SearchPanel/index.vue b/src/components/SearchPanel/index.vue
new file mode 100644
index 0000000..a1859d2
--- /dev/null
+++ b/src/components/SearchPanel/index.vue
@@ -0,0 +1,257 @@
+<template>
+ <div class="search-panel-container">
+ <el-form
+ ref="formRef"
+ :model="modelValue"
+ class="search-form"
+ label-width="0"
+ >
+ <el-row :gutter="10" class="form-row">
+ <!-- 娓叉煋琛ㄥ崟椤� -->
+ <el-col
+ v-for="(item, index) in visibleSchema"
+ :key="item.prop || index"
+ :xs="24"
+ :sm="12"
+ :md="8"
+ :lg="4"
+ :xl="4"
+ class="search-col"
+ >
+ <el-form-item :prop="item.prop" :rules="item.rules" class="search-form-item">
+ <!-- 鑷畾涔夋彃妲� -->
+ <slot v-if="item.slot" :name="item.slot" :item="item"></slot>
+ <!-- 榛樿娓叉煋绫诲瀷 -->
+ <template v-else>
+ <!-- 杈撳叆妗� -->
+ <el-input
+ v-if="item.type === 'input'"
+ v-model="modelValue[item.prop]"
+ :placeholder="item.placeholder || '璇疯緭鍏�'"
+ clearable
+ class="full-width"
+ v-bind="item.props"
+ @keyup.enter="handleSearch"
+ />
+
+ <!-- 涓嬫媺妗� -->
+ <el-select
+ v-else-if="item.type === 'select'"
+ v-model="modelValue[item.prop]"
+ :placeholder="item.placeholder || '璇烽�夋嫨'"
+ clearable
+ class="full-width"
+ v-bind="item.props"
+ >
+ {{ item || '璇烽�夋嫨' }}
+ <!-- <el-option
+ v-for="(opt,idx) in getOptions(item)"
+ :key="idx"
+ :label="opt.label"
+ :value="opt.value"
+ /> -->
+ </el-select>
+
+ <!-- 鏃ユ湡閫夋嫨鍣� -->
+ <el-date-picker
+ v-else-if="item.type === 'date'"
+ v-model="modelValue[item.prop]"
+ type="date"
+ :placeholder="item.placeholder || '閫夋嫨鏃ユ湡'"
+ style="width: 100%"
+ value-format="YYYY-MM-DD"
+ class="full-width"
+ v-bind="item.props"
+ />
+
+ <!-- 鏃ユ湡鑼冨洿閫夋嫨鍣� -->
+ <el-date-picker
+ v-else-if="item.type === 'daterange'"
+ v-model="modelValue[item.prop]"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ class="full-width"
+ v-bind="item.props"
+ />
+ </template>
+ </el-form-item>
+ </el-col>
+
+ <!-- 鎸夐挳鍖哄煙 -->
+ <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" class="search-actions-col">
+ <el-form-item class="search-actions">
+ <el-button style="background: #002FA7; color: white;" icon="Search" @click="handleSearch">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 灞曞紑/鏀惰捣鎸夐挳 -->
+ <div v-if="schema.length > 5" class="expand-toggle" @click="toggleExpand">
+ <span>{{ isExpanded ? '鏀惰捣' : '灞曞紑' }}</span>
+ <el-icon :class="{ 'is-reverse': isExpanded }">
+ <ArrowDown />
+ </el-icon>
+ </div>
+ </el-form>
+ </div>
+</template>
+
+<script setup name="SearchPanel">
+import { ref, reactive, computed, getCurrentInstance, onMounted } from 'vue';
+import { ArrowDown, Search, Refresh } from '@element-plus/icons-vue';
+
+const { proxy } = getCurrentInstance();
+
+const props = defineProps({
+ // 琛ㄥ崟鏁版嵁瀵硅薄
+ modelValue: {
+ type: Object,
+ required: true
+ },
+ // 琛ㄥ崟閰嶇疆椤�
+ schema: {
+ type: Array,
+ default: () => []
+ }
+});
+
+const emit = defineEmits(['update:modelValue', 'search', 'reset']);
+
+// 鏄惁灞曞紑
+const isExpanded = ref(false);
+const formRef = ref(null);
+const dictMap = reactive({});
+
+// 璁$畻鍙鐨� schema 椤�
+const visibleSchema = computed(() => {
+ if (isExpanded.value || props.schema.length <= 5) {
+ return props.schema;
+ }
+ return props.schema.slice(0, 5);
+});
+
+// 鍒濆鍖栧瓧鍏告暟鎹�
+onMounted(() => {
+ const dicts = props.schema.filter(item => item.dict).map(item => item.dict);
+ if (dicts.length > 0 && proxy.useDict) {
+ const dictData = proxy.useDict(...dicts);
+ Object.keys(dictData).forEach(key => {
+ dictMap[key] = dictData[key];
+ });
+ }
+});
+
+// 鑾峰彇涓嬫媺閫夐」 (鏀寔闈欐�� options 鍜� 瀛楀吀 dict)
+function getOptions(item) {
+ if (item.options) return item.options;
+ if (item.dict && dictMap[item.dict]) {
+ return dictMap[item.dict].value || [];
+ }
+ return [];
+}
+
+// 鎼滅储
+function handleSearch() {
+ emit('search', props.modelValue);
+}
+
+// 閲嶇疆
+function handleReset() {
+ if (formRef.value) {
+ formRef.value.resetFields();
+ }
+ const keys = props.schema.map(item => item.prop).filter(Boolean);
+ keys.forEach(key => {
+ props.modelValue[key] = undefined;
+ });
+ emit('update:modelValue', props.modelValue);
+ emit('reset');
+}
+
+// 鍒囨崲灞曞紑/鏀惰捣
+function toggleExpand() {
+ isExpanded.value = !isExpanded.value;
+}
+</script>
+
+<style scoped lang="scss">
+.search-panel-container {
+ background: #fff;
+ padding: 15px 15px 5px;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ margin-bottom: 15px;
+
+ .search-form {
+ .form-row {
+ width: 100%;
+ }
+
+ .search-col {
+ margin-bottom: 10px;
+ }
+
+ .search-form-item {
+ margin-right: 0;
+ margin-bottom: 0;
+ width: 100%;
+
+ :deep(.el-form-item__content) {
+ width: 100%;
+ }
+ }
+
+ .full-width {
+ width: 100% !important;
+ }
+
+ .search-actions-col {
+ margin-left: auto;
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+
+ .search-actions {
+ margin-bottom: 0;
+ margin-right: 0;
+
+ :deep(.el-button--primary) {
+ background-color: #409eff;
+ border-color: #409eff;
+ }
+ }
+
+ .expand-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ font-size: 13px;
+ color: #909399;
+ cursor: pointer;
+ padding: 5px 0;
+ user-select: none;
+ width: 100%;
+ border-top: 1px solid #f0f2f5;
+ margin-top: 5px;
+
+ &:hover {
+ color: #409eff;
+ }
+
+ .el-icon {
+ transition: transform 0.3s;
+ &.is-reverse {
+ transform: rotate(180deg);
+ }
+ }
+ }
+ }
+}
+</style>
+
diff --git a/src/components/Upload/FileUpload.vue b/src/components/Upload/FileUpload.vue
index 1ac0344..8658621 100644
--- a/src/components/Upload/FileUpload.vue
+++ b/src/components/Upload/FileUpload.vue
@@ -49,9 +49,14 @@
emits("remove", file);
};
+const clearFiles = () => {
+ fileList.value = [];
+};
+
defineExpose({
fileList,
uploadApi,
+ clearFiles,
});
</script>
diff --git a/src/layout/components/NotificationCenter/index.vue b/src/layout/components/NotificationCenter/index.vue
index 6098153..2517c22 100644
--- a/src/layout/components/NotificationCenter/index.vue
+++ b/src/layout/components/NotificationCenter/index.vue
@@ -2,25 +2,30 @@
<div class="notification-popover-content">
<div class="popover-header">
<span class="popover-title">娑堟伅閫氱煡</span>
- <el-button type="primary" size="small" @click="handleMarkAllAsRead" :disabled="unreadCount === 0">
+ <el-button type="primary"
+ size="small"
+ @click="handleMarkAllAsRead"
+ :disabled="unreadCount === 0">
涓�閿凡璇�
</el-button>
</div>
-
<div class="notification-content">
- <el-tabs v-model="activeTab" @tab-change="handleTabChange">
- <el-tab-pane :label="`鏈(${unreadCount})`" name="unread">
- <div v-if="unreadList.length === 0" class="empty-state">
+ <el-tabs v-model="activeTab"
+ @tab-change="handleTabChange">
+ <el-tab-pane :label="`鏈(${unreadCount})`"
+ name="unread">
+ <div v-if="unreadList.length === 0"
+ class="empty-state">
<el-empty description="鏆傛棤鏈娑堟伅" />
</div>
- <div v-else class="notification-list">
- <div
- v-for="item in unreadList"
- :key="item.id"
- class="notification-item"
- >
+ <div v-else
+ class="notification-list">
+ <div v-for="item in unreadList"
+ :key="item.id"
+ class="notification-item">
<div class="notification-icon">
- <el-icon :size="24" color="#67C23A">
+ <el-icon :size="24"
+ color="#67C23A">
<Bell />
</el-icon>
</div>
@@ -30,25 +35,29 @@
<div class="notification-time">{{ item.createTime }}</div>
</div>
<div class="notification-action">
- <el-button type="primary" size="small" @click="handleConfirm(item)">
+ <el-button type="primary"
+ size="small"
+ @click="handleConfirm(item)">
纭
</el-button>
</div>
</div>
</div>
</el-tab-pane>
- <el-tab-pane label="宸茶" name="read">
- <div v-if="readList.length === 0" class="empty-state">
+ <el-tab-pane label="宸茶"
+ name="read">
+ <div v-if="readList.length === 0"
+ class="empty-state">
<el-empty description="鏆傛棤宸茶娑堟伅" />
</div>
- <div v-else class="notification-list">
- <div
- v-for="item in readList"
- :key="item.id"
- class="notification-item read"
- >
+ <div v-else
+ class="notification-list">
+ <div v-for="item in readList"
+ :key="item.id"
+ class="notification-item read">
<div class="notification-icon">
- <el-icon :size="24" color="#909399">
+ <el-icon :size="24"
+ color="#909399">
<Bell />
</el-icon>
</div>
@@ -61,312 +70,316 @@
</div>
</el-tab-pane>
</el-tabs>
-
<!-- 鍒嗛〉 -->
- <div class="pagination-wrapper" v-if="total > 0">
- <el-pagination
- v-model:current-page="pageNum"
- v-model:page-size="pageSize"
- :page-sizes="[10, 20, 50, 100]"
- :total="total"
- layout="prev, pager, next, sizes"
- @size-change="handleSizeChange"
- @current-change="handlePageChange"
- />
+ <div class="pagination-wrapper"
+ v-if="total > 0">
+ <el-pagination v-model:current-page="pageNum"
+ v-model:page-size="pageSize"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="total"
+ layout="prev, pager, next, sizes"
+ @size-change="handleSizeChange"
+ @current-change="handlePageChange" />
</div>
</div>
</div>
</template>
<script setup>
-import { Bell } from '@element-plus/icons-vue'
-import { listMessage, markAsRead, markAllAsRead, confirmMessage, getUnreadCount } from '@/api/system/message'
-import { ElMessage } from 'element-plus'
-import useUserStore from '@/store/modules/user'
-import { useRouter } from 'vue-router'
+ import { Bell } from "@element-plus/icons-vue";
+ import {
+ listMessage,
+ markAsRead,
+ markAllAsRead,
+ confirmMessage,
+ getUnreadCount,
+ } from "@/api/system/message";
+ import { ElMessage } from "element-plus";
+ import useUserStore from "@/store/modules/user";
+ import { useRouter } from "vue-router";
-const userStore = useUserStore()
-const router = useRouter()
-const emit = defineEmits(['unreadCountChange'])
+ const userStore = useUserStore();
+ const router = useRouter();
+ const emit = defineEmits(["unreadCountChange"]);
-const activeTab = ref('unread')
-const unreadList = ref([])
-const readList = ref([])
-const unreadCount = ref(0)
-const total = ref(0)
-const pageNum = ref(1)
-const pageSize = ref(10)
+ const activeTab = ref("unread");
+ const unreadList = ref([]);
+ const readList = ref([]);
+ const unreadCount = ref(0);
+ const total = ref(0);
+ const pageNum = ref(1);
+ const pageSize = ref(10);
-// 鍔犺浇娑堟伅鍒楄〃
-const loadMessages = async () => {
- try {
- const consigneeId = userStore.id
- if (!consigneeId) {
- console.warn('鏈幏鍙栧埌褰撳墠鐧诲綍鐢ㄦ埛ID')
- return
- }
- const params = {
- consigneeId: consigneeId,
- current: pageNum.value,
- size: pageSize.value,
- status: activeTab.value === 'read' ? 1 : 0
- }
- const res = await listMessage(params)
- if (res.code === 200) {
- if (activeTab.value === 'unread') {
- unreadList.value = res.data.records || []
- } else {
- readList.value = res.data.records || []
+ // 鍔犺浇娑堟伅鍒楄〃
+ const loadMessages = async () => {
+ try {
+ const consigneeId = userStore.id;
+ if (!consigneeId) {
+ console.warn("鏈幏鍙栧埌褰撳墠鐧诲綍鐢ㄦ埛ID");
+ return;
}
- total.value = res.data.total || 0
+ const params = {
+ consigneeId: consigneeId,
+ current: pageNum.value,
+ size: pageSize.value,
+ status: activeTab.value === "read" ? 1 : 0,
+ };
+ const res = await listMessage(params);
+ if (res.code === 200) {
+ if (activeTab.value === "unread") {
+ unreadList.value = res.data.records || [];
+ } else {
+ readList.value = res.data.records || [];
+ }
+ total.value = res.data.total || 0;
+ }
+ } catch (error) {
+ console.error("鍔犺浇娑堟伅鍒楄〃澶辫触:", error);
}
- } catch (error) {
- console.error('鍔犺浇娑堟伅鍒楄〃澶辫触:', error)
- }
-}
+ };
-// 鍔犺浇鏈鏁伴噺
-const loadUnreadCount = async () => {
- try {
- const consigneeId = userStore.id
- if (!consigneeId) {
- console.warn('鏈幏鍙栧埌褰撳墠鐧诲綍鐢ㄦ埛ID')
- return
+ // 鍔犺浇鏈鏁伴噺
+ const loadUnreadCount = async () => {
+ try {
+ const consigneeId = userStore.id;
+ if (!consigneeId) {
+ console.warn("鏈幏鍙栧埌褰撳墠鐧诲綍鐢ㄦ埛ID");
+ return;
+ }
+ const res = await getUnreadCount(consigneeId);
+ if (res.code === 200) {
+ unreadCount.value = res.data || 0;
+ emit("unreadCountChange", unreadCount.value);
+ }
+ } catch (error) {
+ console.error("鍔犺浇鏈鏁伴噺澶辫触:", error);
}
- const res = await getUnreadCount(consigneeId)
- if (res.code === 200) {
- unreadCount.value = res.data || 0
- emit('unreadCountChange', unreadCount.value)
- }
- } catch (error) {
- console.error('鍔犺浇鏈鏁伴噺澶辫触:', error)
- }
-}
+ };
-// 鏍囩椤靛垏鎹�
-const handleTabChange = (tab) => {
- pageNum.value = 1
- loadMessages()
-}
+ // 鏍囩椤靛垏鎹�
+ const handleTabChange = tab => {
+ pageNum.value = 1;
+ loadMessages();
+ };
-// 纭娑堟伅
-const handleConfirm = async (item) => {
- try {
- console.log('item', item)
- const res = await confirmMessage(item.noticeId, 1)
- if (res.code === 200) {
- ElMessage.success('纭鎴愬姛')
- // 閲嶆柊鍔犺浇鏁版嵁
- loadMessages()
- loadUnreadCount()
-
- // 鏍规嵁 jumpPath 杩涜椤甸潰璺宠浆
- if (item.jumpPath) {
- try {
- // 瑙f瀽 jumpPath锛屽垎绂昏矾寰勫拰鏌ヨ鍙傛暟
- const [path, queryString] = item.jumpPath.split('?')
- let query = {}
-
- if (queryString) {
- // 瑙f瀽鏌ヨ鍙傛暟
- queryString.split('&').forEach(param => {
- const [key, value] = param.split('=')
- if (key && value) {
- query[key] = decodeURIComponent(value)
- }
- })
+ // 纭娑堟伅
+ const handleConfirm = async item => {
+ try {
+ console.log("item", item);
+ const res = await confirmMessage(item.noticeId, 1);
+ if (res.code === 200) {
+ ElMessage.success("纭鎴愬姛");
+ // 閲嶆柊鍔犺浇鏁版嵁
+ loadMessages();
+ loadUnreadCount();
+
+ // 鏍规嵁 jumpPath 杩涜椤甸潰璺宠浆
+ if (item.jumpPath) {
+ try {
+ // 瑙f瀽 jumpPath锛屽垎绂昏矾寰勫拰鏌ヨ鍙傛暟
+ const [path, queryString] = item.jumpPath.split("?");
+ let query = {};
+
+ if (queryString) {
+ // 瑙f瀽鏌ヨ鍙傛暟
+ queryString.split("&").forEach(param => {
+ const [key, value] = param.split("=");
+ if (key && value) {
+ query[key] = decodeURIComponent(value);
+ }
+ });
+ }
+
+ // 璺宠浆鍒版寚瀹氶〉闈�
+ router.push({
+ path: path,
+ query: query,
+ });
+ } catch (error) {
+ console.error("椤甸潰璺宠浆澶辫触:", error);
}
-
- // 璺宠浆鍒版寚瀹氶〉闈�
- router.push({
- path: path,
- query: query
- })
- } catch (error) {
- console.error('椤甸潰璺宠浆澶辫触:', error)
}
}
+ } catch (error) {
+ console.error("纭娑堟伅澶辫触:", error);
+ ElMessage.error("纭澶辫触");
}
- } catch (error) {
- console.error('纭娑堟伅澶辫触:', error)
- ElMessage.error('纭澶辫触')
- }
-}
+ };
-// 涓�閿凡璇�
-const handleMarkAllAsRead = async () => {
- try {
- const res = await markAllAsRead()
- if (res.code === 200) {
- ElMessage.success('宸插叏閮ㄦ爣璁颁负宸茶')
- loadMessages()
- loadUnreadCount()
+ // 涓�閿凡璇�
+ const handleMarkAllAsRead = async () => {
+ try {
+ const res = await markAllAsRead();
+ if (res.code === 200) {
+ ElMessage.success("宸插叏閮ㄦ爣璁颁负宸茶");
+ loadMessages();
+ loadUnreadCount();
+ }
+ } catch (error) {
+ console.error("涓�閿凡璇诲け璐�:", error);
+ ElMessage.error("鎿嶄綔澶辫触");
}
- } catch (error) {
- console.error('涓�閿凡璇诲け璐�:', error)
- ElMessage.error('鎿嶄綔澶辫触')
- }
-}
+ };
-// 鍒嗛〉澶у皬鏀瑰彉
-const handleSizeChange = (size) => {
- pageSize.value = size
- pageNum.value = 1
- loadMessages()
-}
+ // 鍒嗛〉澶у皬鏀瑰彉
+ const handleSizeChange = size => {
+ pageSize.value = size;
+ pageNum.value = 1;
+ loadMessages();
+ };
-// 椤电爜鏀瑰彉
-const handlePageChange = (page) => {
- pageNum.value = page
- loadMessages()
-}
+ // 椤电爜鏀瑰彉
+ const handlePageChange = page => {
+ pageNum.value = page;
+ loadMessages();
+ };
-// 缁勪欢鎸傝浇鏃跺姞杞芥湭璇绘暟閲�
-onMounted(() => {
- loadUnreadCount()
-})
+ // 缁勪欢鎸傝浇鏃跺姞杞芥湭璇绘暟閲�
+ onMounted(() => {
+ loadUnreadCount();
+ });
-// 鐩戝惉鐖剁粍浠朵紶閫掔殑 visible 鐘舵�侊紙閫氳繃 watch 鍦� Navbar 涓鐞嗭級
-// 杩欓噷鍙礋璐f暟鎹姞杞斤紝涓嶆帶鍒舵樉绀�
+ // 鐩戝惉鐖剁粍浠朵紶閫掔殑 visible 鐘舵�侊紙閫氳繃 watch 鍦� Navbar 涓鐞嗭級
+ // 杩欓噷鍙礋璐f暟鎹姞杞斤紝涓嶆帶鍒舵樉绀�
-// 鏆撮湶鏂规硶渚涘閮ㄨ皟鐢�
-defineExpose({
- loadUnreadCount,
- loadMessages
-})
+ // 鏆撮湶鏂规硶渚涘閮ㄨ皟鐢�
+ defineExpose({
+ loadUnreadCount,
+ loadMessages,
+ });
</script>
<style lang="scss" scoped>
-.notification-popover-content {
- display: flex;
- flex-direction: column;
- width: 500px;
- padding: 16px;
-}
-
-.popover-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
- margin-bottom: 16px;
- padding-bottom: 12px;
- border-bottom: 1px solid #f0f0f0;
-
- .popover-title {
- font-size: 18px;
- font-weight: 500;
- color: #303133;
- }
-}
-
-.notification-content {
- max-height: 60vh;
- display: flex;
- flex-direction: column;
-
- :deep(.el-tabs) {
- flex: 1;
+ .notification-popover-content {
display: flex;
flex-direction: column;
- min-height: 0;
-
- .el-tabs__header {
- margin-bottom: 0;
- flex-shrink: 0;
- padding: 0;
- }
-
- .el-tabs__content {
- flex: 1;
- overflow-y: auto;
- min-height: 0;
- padding-top: 16px;
- }
-
- .el-tab-pane {
- height: 100%;
- }
+ width: 500px;
+ padding: 16px;
}
-}
-.empty-state {
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 300px;
- padding: 40px 0;
-}
-
-.notification-list {
- .notification-item {
+ .popover-header {
display: flex;
- padding: 12px 0;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
- transition: background-color 0.3s;
- &:hover {
- background-color: #f5f7fa;
- }
-
- &.read {
- opacity: 0.7;
- }
-
- .notification-icon {
- flex-shrink: 0;
- width: 40px;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #f0f9ff;
- border-radius: 50%;
- margin-right: 12px;
- }
-
- .notification-content-wrapper {
- flex: 1;
- min-width: 0;
-
- .notification-title {
- font-size: 14px;
- font-weight: 500;
- color: #303133;
- margin-bottom: 8px;
- }
-
- .notification-detail {
- font-size: 13px;
- color: #606266;
- line-height: 1.5;
- margin-bottom: 8px;
- word-break: break-all;
- }
-
- .notification-time {
- font-size: 12px;
- color: #909399;
- }
- }
-
- .notification-action {
- flex-shrink: 0;
- margin-left: 12px;
- display: flex;
- align-items: center;
+ .popover-title {
+ font-size: 18px;
+ font-weight: 500;
+ color: #303133;
}
}
-}
-.pagination-wrapper {
- margin-top: 16px;
- padding-top: 16px;
- border-top: 1px solid #f0f0f0;
- display: flex;
- justify-content: center;
- padding-left: 0;
- padding-right: 0;
-}
+ .notification-content {
+ max-height: 60vh;
+ display: flex;
+ flex-direction: column;
+
+ :deep(.el-tabs) {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+
+ .el-tabs__header {
+ margin-bottom: 0;
+ flex-shrink: 0;
+ padding: 0;
+ }
+
+ .el-tabs__content {
+ flex: 1;
+ overflow-y: auto;
+ min-height: 0;
+ padding-top: 16px;
+ }
+
+ .el-tab-pane {
+ height: 100%;
+ }
+ }
+ }
+
+ .empty-state {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 40px 0;
+ }
+
+ .notification-list {
+ .notification-item {
+ display: flex;
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f0f0;
+ transition: background-color 0.3s;
+
+ &:hover {
+ background-color: #f5f7fa;
+ }
+
+ &.read {
+ opacity: 0.7;
+ }
+
+ .notification-icon {
+ flex-shrink: 0;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #f0f9ff;
+ border-radius: 50%;
+ margin-right: 12px;
+ }
+
+ .notification-content-wrapper {
+ flex: 1;
+ min-width: 0;
+
+ .notification-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: #303133;
+ margin-bottom: 8px;
+ }
+
+ .notification-detail {
+ font-size: 13px;
+ color: #606266;
+ line-height: 1.5;
+ margin-bottom: 8px;
+ word-break: break-all;
+ }
+
+ .notification-time {
+ font-size: 12px;
+ color: #909399;
+ }
+ }
+
+ .notification-action {
+ flex-shrink: 0;
+ margin-left: 12px;
+ display: flex;
+ align-items: center;
+ }
+ }
+ }
+
+ .pagination-wrapper {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid #f0f0f0;
+ display: flex;
+ justify-content: center;
+ padding-left: 0;
+ padding-right: 0;
+ }
</style>
diff --git a/src/router/index.js b/src/router/index.js
index e5dc580..f342004 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -106,6 +106,19 @@
},
],
},
+ {
+ path: "/projectManagement/Management/detail",
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: ":id",
+ component: () => import("@/views/projectManagement/Management/projectDetail.vue"),
+ name: "ProjectManagementDetail",
+ meta: { title: "椤圭洰璇︽儏", activeMenu: "/projectManagement/Management" },
+ },
+ ],
+ },
];
// 鍔ㄦ�佽矾鐢憋紝鍩轰簬鐢ㄦ埛鏉冮檺鍔ㄦ�佸幓鍔犺浇
diff --git a/src/views/basicData/customerFile/index.vue b/src/views/basicData/customerFile/index.vue
index 0f39e25..ad1c5bb 100644
--- a/src/views/basicData/customerFile/index.vue
+++ b/src/views/basicData/customerFile/index.vue
@@ -3,244 +3,612 @@
<div class="search_form">
<div>
<span class="search_title">瀹㈡埛鍚嶇О锛�</span>
- <el-input
- v-model="searchForm.customerName"
- style="width: 240px;margin-right: 10px"
- placeholder="璇疯緭鍏�"
- @change="handleQuery"
- clearable
- :prefix-icon="Search"
- />
- <span class="search_title">瀹㈡埛鍒嗙被锛�</span>
- <el-select
- v-model="searchForm.customerType"
- placeholder="璇烽�夋嫨"
- style="width: 240px"
- clearable
- @change="handleQuery"
- >
- <el-option label="闆跺敭瀹㈡埛" value="闆跺敭瀹㈡埛" />
- <el-option label="杩涢攢鍟嗗鎴�" value="杩涢攢鍟嗗鎴�" />
+ <el-input v-model="searchForm.customerName"
+ style="width: 240px;margin-right: 10px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span class="search_title">瀹㈡埛鍒嗙被锛�</span>
+ <el-select v-model="searchForm.customerType"
+ placeholder="璇烽�夋嫨"
+ style="width: 240px"
+ clearable
+ @change="handleQuery">
+ <el-option label="闆跺敭瀹㈡埛"
+ value="闆跺敭瀹㈡埛" />
+ <el-option label="杩涢攢鍟嗗鎴�"
+ value="杩涢攢鍟嗗鎴�" />
</el-select>
- <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
- >鎼滅储</el-button
- >
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">鎼滅储</el-button>
</div>
<div>
- <el-button type="primary" @click="openForm('add')">鏂板瀹㈡埛</el-button>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板瀹㈡埛</el-button>
<el-button @click="handleOut">瀵煎嚭</el-button>
- <el-button type="info" plain icon="Upload" @click="handleImport"
- >瀵煎叆</el-button
- >
- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ <el-button type="info"
+ plain
+ icon="Upload"
+ @click="handleImport">瀵煎叆</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
</div>
</div>
<div class="table_list">
- <PIMTable
- rowKey="id"
- :column="tableColumn"
- :tableData="tableData"
- :page="page"
- :isSelection="true"
- @selection-change="handleSelectionChange"
- :tableLoading="tableLoading"
- @pagination="pagination"
- ></PIMTable>
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"></PIMTable>
</div>
- <el-dialog
- v-model="dialogFormVisible"
- :title="operationType === 'add' ? '鏂板瀹㈡埛淇℃伅' : '缂栬緫瀹㈡埛淇℃伅'"
- width="70%"
- @close="closeDia"
- >
- <el-form
- :model="form"
- label-width="140px"
- label-position="top"
- :rules="rules"
- ref="formRef"
- >
+ <el-dialog v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板瀹㈡埛淇℃伅' : '缂栬緫瀹㈡埛淇℃伅'"
+ width="70%"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerName">
- <el-input
- v-model="form.customerName"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="瀹㈡埛鍚嶇О锛�"
+ prop="customerName">
+ <el-input v-model="form.customerName"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item
- label="绾崇◣浜鸿瘑鍒彿锛�"
- prop="taxpayerIdentificationNumber"
- >
- <el-input
- v-model="form.taxpayerIdentificationNumber"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="绾崇◣浜鸿瘑鍒彿锛�"
+ prop="taxpayerIdentificationNumber">
+ <el-input v-model="form.taxpayerIdentificationNumber"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="鍏徃鍦板潃锛�" prop="companyAddress">
- <el-input
- v-model="form.companyAddress"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="鍏徃鍦板潃锛�"
+ prop="companyAddress">
+ <el-input v-model="form.companyAddress"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item label="鍏徃鐢佃瘽锛�" prop="companyPhone">
- <el-input
- v-model="form.companyPhone"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="鍏徃鐢佃瘽锛�"
+ prop="companyPhone">
+ <el-input v-model="form.companyPhone"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="閾惰鍩烘湰鎴凤細" prop="basicBankAccount">
- <el-input
- v-model="form.basicBankAccount"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="閾惰鍩烘湰鎴凤細"
+ prop="basicBankAccount">
+ <el-input v-model="form.basicBankAccount"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item label="閾惰璐﹀彿锛�" prop="bankAccount">
- <el-input
- v-model="form.bankAccount"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="閾惰璐﹀彿锛�"
+ prop="bankAccount">
+ <el-input v-model="form.bankAccount"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="寮�鎴疯鍙凤細" prop="bankCode">
- <el-input
- v-model="form.bankCode"
- placeholder="璇疯緭鍏�"
- clearable
- />
+ <el-form-item label="寮�鎴疯鍙凤細"
+ prop="bankCode">
+ <el-input v-model="form.bankCode"
+ placeholder="璇疯緭鍏�"
+ clearable />
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item label="瀹㈡埛鍒嗙被锛�" prop="customerType">
- <el-select v-model="form.customerType" placeholder="璇烽�夋嫨" clearable>
- <el-option label="闆跺敭瀹㈡埛" value="闆跺敭瀹㈡埛" />
- <el-option label="杩涢攢鍟嗗鎴�" value="杩涢攢鍟嗗鎴�" />
+ <el-form-item label="瀹㈡埛鍒嗙被锛�"
+ prop="customerType">
+ <el-select v-model="form.customerType"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option label="闆跺敭瀹㈡埛"
+ value="闆跺敭瀹㈡埛" />
+ <el-option label="杩涢攢鍟嗗鎴�"
+ value="杩涢攢鍟嗗鎴�" />
</el-select>
</el-form-item>
</el-col>
</el-row>
- <el-row :gutter="30" v-for="(contact, index) in formYYs.contactList" :key="index">
- <el-col :span="12">
- <el-form-item label="鑱旂郴浜猴細" prop="contactPerson">
- <el-input v-model="contact.contactPerson" placeholder="璇疯緭鍏�" clearable />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鑱旂郴鐢佃瘽锛�" prop="contactPhone">
- <div style="display: flex; align-items: center;width: 100%;">
- <el-input v-model="contact.contactPhone" placeholder="璇疯緭鍏�" clearable />
- <el-button @click="removeContact(index)" type="danger" circle style="margin-left: 5px;">
- <el-icon><Close /></el-icon>
- </el-button>
- </div>
- </el-form-item>
- </el-col>
- </el-row>
- <el-button @click="addNewContact" style="margin-bottom: 10px;">+ 鏂板鑱旂郴浜�</el-button>
+ <el-row :gutter="30"
+ v-for="(contact, index) in formYYs.contactList"
+ :key="index">
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴浜猴細"
+ prop="contactPerson">
+ <el-input v-model="contact.contactPerson"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽锛�"
+ prop="contactPhone">
+ <div style="display: flex; align-items: center;width: 100%;">
+ <el-input v-model="contact.contactPhone"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ <el-button @click="removeContact(index)"
+ type="danger"
+ circle
+ style="margin-left: 5px;">
+ <el-icon>
+ <Close />
+ </el-icon>
+ </el-button>
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-button @click="addNewContact"
+ style="margin-bottom: 10px;">+ 鏂板鑱旂郴浜�</el-button>
<el-row :gutter="30">
<el-col :span="12">
- <el-form-item label="缁存姢浜猴細" prop="maintainer">
- <el-select
- v-model="form.maintainer"
- placeholder="璇烽�夋嫨"
- clearable
- disabled
- >
- <el-option
- v-for="item in userList"
- :key="item.nickName"
- :label="item.nickName"
- :value="item.nickName"
- />
+ <el-form-item label="缁存姢浜猴細"
+ prop="maintainer">
+ <el-select v-model="form.maintainer"
+ placeholder="璇烽�夋嫨"
+ clearable
+ disabled>
+ <el-option v-for="item in userList"
+ :key="item.nickName"
+ :label="item.nickName"
+ :value="item.nickName" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
- <el-form-item label="缁存姢鏃堕棿锛�" prop="maintenanceTime">
- <el-date-picker
- style="width: 100%"
- v-model="form.maintenanceTime"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- type="date"
- placeholder="璇烽�夋嫨"
- clearable
- />
+ <el-form-item label="缁存姢鏃堕棿锛�"
+ prop="maintenanceTime">
+ <el-date-picker style="width: 100%"
+ v-model="form.maintenanceTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
- <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
<el-button @click="closeDia">鍙栨秷</el-button>
</div>
</template>
</el-dialog>
<!-- 鐢ㄦ埛瀵煎叆瀵硅瘽妗� -->
- <el-dialog
- :title="upload.title"
- v-model="upload.open"
- width="400px"
- append-to-body
- >
- <el-upload
- ref="uploadRef"
- :limit="1"
- accept=".xlsx, .xls"
- :headers="upload.headers"
- :action="upload.url + '?updateSupport=' + upload.updateSupport"
- :disabled="upload.isUploading"
- :before-upload="upload.beforeUpload"
- :on-progress="upload.onProgress"
- :on-success="upload.onSuccess"
- :on-error="upload.onError"
- :on-change="upload.onChange"
- :auto-upload="false"
- drag
- >
+ <el-dialog :title="upload.title"
+ v-model="upload.open"
+ width="400px"
+ append-to-body>
+ <el-upload ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url + '?updateSupport=' + upload.updateSupport"
+ :disabled="upload.isUploading"
+ :before-upload="upload.beforeUpload"
+ :on-progress="upload.onProgress"
+ :on-success="upload.onSuccess"
+ :on-error="upload.onError"
+ :on-change="upload.onChange"
+ :auto-upload="false"
+ drag>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
<template #tip>
<div class="el-upload__tip text-center">
<span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
- <el-link
- type="primary"
- :underline="false"
- style="font-size: 12px; vertical-align: baseline"
- @click="importTemplate"
- >涓嬭浇妯℃澘</el-link
- >
+ <el-link type="primary"
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ @click="importTemplate">涓嬭浇妯℃澘</el-link>
</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
- <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button type="primary"
+ @click="submitFileForm">纭� 瀹�</el-button>
<el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 鍥炶鎻愰啋瀵硅瘽妗� -->
+ <el-dialog title="鍥炶鎻愰啋"
+ v-model="reminderDialogVisible"
+ width="500px"
+ @close="closeReminderDialog">
+ <el-form :model="reminderForm"
+ label-width="100px"
+ :rules="reminderRules"
+ ref="reminderFormRef">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�">
+ <el-input v-model="reminderForm.customerName"
+ disabled />
+ </el-form-item>
+ <el-form-item label="鎻愰啋寮�鍏筹細">
+ <el-switch v-model="reminderForm.reminderSwitch" />
+ </el-form-item>
+ <el-form-item label="鎻愰啋鍐呭锛�"
+ prop="reminderContent">
+ <el-input v-model="reminderForm.reminderContent"
+ type="textarea"
+ :maxlength="100"
+ show-word-limit
+ placeholder="璇疯緭鍏ユ彁閱掑唴瀹�" />
+ </el-form-item>
+ <el-form-item label="鎻愰啋鏃堕棿锛�"
+ prop="reminderTime">
+ <el-date-picker v-model="reminderForm.reminderTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm:ss"
+ placeholder="璇烽�夋嫨鎻愰啋鏃堕棿"
+ style="width: 100%" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeReminderDialog">鍙栨秷</el-button>
+ <el-button type="primary"
+ @click="submitReminderForm">鎻愪氦</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 娣诲姞/淇敼娲借皥杩涘害瀵硅瘽妗� -->
+ <el-dialog :title="negotiationForm.editIndex !== undefined ? '淇敼杩涘害' : '娣诲姞杩涘害'"
+ v-model="negotiationDialogVisible"
+ width="600px"
+ @close="closeNegotiationDialog">
+ <el-form :model="negotiationForm"
+ label-width="100px"
+ :rules="negotiationRules"
+ ref="negotiationFormRef">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璺熻繘鏂瑰紡锛�"
+ prop="followUpMethod">
+ <el-select v-model="negotiationForm.followUpMethod"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%">
+ <el-option label="鐢佃瘽"
+ value="鐢佃瘽" />
+ <el-option label="閭欢"
+ value="閭欢" />
+ <el-option label="涓婇棬"
+ value="涓婇棬" />
+ <el-option label="寰俊"
+ value="寰俊" />
+ <el-option label="鍏朵粬"
+ value="鍏朵粬" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璺熻繘绋嬪害锛�"
+ prop="followUpLevel">
+ <el-select v-model="negotiationForm.followUpLevel"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%">
+ <el-option label="娼滃湪瀹㈡埛"
+ value="娼滃湪瀹㈡埛" />
+ <el-option label="鍒濇鎷滆"
+ value="鍒濇鎷滆" />
+ <el-option label="澶氭鎷滆"
+ value="澶氭鎷滆" />
+ <el-option label="鎰忓悜瀹㈡埛"
+ value="鎰忓悜瀹㈡埛" />
+ <el-option label="宸茬绾﹀鎴�"
+ value="宸茬绾﹀鎴�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璺熻繘鏃堕棿锛�"
+ prop="followUpTime">
+ <el-date-picker v-model="negotiationForm.followUpTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm:ss"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璺熻繘浜猴細">
+ <el-input v-model="negotiationForm.followerUserName"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍐呭锛�"
+ prop="content">
+ <el-input v-model="negotiationForm.content"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeNegotiationDialog">鍙栨秷</el-button>
+ <el-button type="primary"
+ @click="submitNegotiationForm">鎻愪氦</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 瀹㈡埛璇︽儏瀵硅瘽妗� -->
+ <el-dialog title="瀹㈡埛璇︽儏"
+ v-model="detailDialogVisible"
+ width="1000px"
+ @close="closeDetailDialog">
+ <!-- 瀹㈡埛鍩烘湰淇℃伅 -->
+ <div class="detail-section">
+ <h3 class="section-title">瀹㈡埛鍩烘湰淇℃伅</h3>
+ <div class="info-display">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">瀹㈡埛鍚嶇О锛�</span>
+ <span class="info-value">{{ detailForm.customerName }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">瀹㈡埛鍒嗙被锛�</span>
+ <span class="info-value">{{ detailForm.customerType }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">绾崇◣浜鸿瘑鍒彿锛�</span>
+ <span class="info-value">{{ detailForm.taxpayerIdentificationNumber }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鍏徃鐢佃瘽锛�</span>
+ <span class="info-value">{{ detailForm.companyPhone }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鍏徃鍦板潃锛�</span>
+ <span class="info-value">{{ detailForm.companyAddress }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">閾惰鍩烘湰鎴凤細</span>
+ <span class="info-value">{{ detailForm.basicBankAccount }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">閾惰璐﹀彿锛�</span>
+ <span class="info-value">{{ detailForm.bankAccount }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">寮�鎴疯鍙凤細</span>
+ <span class="info-value">{{ detailForm.bankCode }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鑱旂郴浜猴細</span>
+ <span class="info-value">{{ detailForm.contactPerson }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鑱旂郴鐢佃瘽锛�</span>
+ <span class="info-value">{{ detailForm.contactPhone }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">缁存姢浜猴細</span>
+ <span class="info-value">{{ detailForm.maintainer }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">缁存姢鏃堕棿锛�</span>
+ <span class="info-value">{{ detailForm.maintenanceTime }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+ <!-- 娲借皥杩涘害璁板綍 -->
+ <div class="detail-section">
+ <div class="section-header">
+ <h3 class="section-title">娲借皥杩涘害璁板綍</h3>
+ <el-button type="primary"
+ size="small"
+ @click="openNegotiationDialog(detailForm)">
+ 娣诲姞杩涘害
+ </el-button>
+ </div>
+ <el-table :data="negotiationRecords"
+ border
+ style="width: 100%">
+ <el-table-column prop="followUpTime"
+ label="璺熻繘鏃堕棿"
+ width="160" />
+ <el-table-column prop="followUpMethod"
+ label="璺熻繘鏂瑰紡"
+ width="100" />
+ <el-table-column prop="followUpLevel"
+ label="璺熻繘绋嬪害" />
+ <el-table-column prop="followerUserName"
+ label="璺熻繘浜�"
+ width="100" />
+ <el-table-column prop="content"
+ label="鍐呭"
+ show-overflow-tooltip />
+ <el-table-column label="闄勪欢"
+ width="100"
+ align="center">
+ <template #default="{ row }">
+ <el-button type="info"
+ link
+ size="small"
+ @click="openAttachmentDialog(row)">
+ <el-icon>
+ <Paperclip />
+ </el-icon>
+ 闄勪欢
+ <!-- {{ row.fileList && row.fileList.length > 0 ? row.fileList.length : '涓婁紶' }} -->
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="150"
+ align="center">
+ <template #default="{ row, $index }">
+ <el-button type="primary"
+ link
+ size="small"
+ @click="editNegotiationRecord(row, $index)">
+ 淇敼
+ </el-button>
+ <el-button type="danger"
+ link
+ size="small"
+ @click="deleteNegotiationRecord(row, $index)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div v-if="negotiationRecords.length === 0"
+ class="no-records">
+ 鏆傛棤娲借皥杩涘害璁板綍
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDetailDialog">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 闄勪欢涓婁紶寮圭獥 -->
+ <el-dialog title="闄勪欢绠$悊"
+ v-model="attachmentDialogVisible"
+ width="600px"
+ @close="closeAttachmentDialog">
+ <div class="attachment-section">
+ <div class="upload-area">
+ <el-upload ref="attachmentUploadRef"
+ :action="getAttachmentUploadUrl()"
+ :headers="attachmentUploadHeaders"
+ :file-list="currentAttachmentList"
+ :on-success="handleAttachmentSuccess"
+ :on-error="handleAttachmentError"
+ :on-remove="handleAttachmentRemove"
+ :before-upload="beforeAttachmentUpload"
+ multiple
+ :limit="10"
+ name="files">
+ <el-button type="primary">
+ <el-icon>
+ <Upload />
+ </el-icon>
+ 涓婁紶闄勪欢
+ </el-button>
+ <template #tip>
+ <div class="el-upload__tip">
+ 鏀寔涓婁紶鍥剧墖銆佹枃妗g瓑鏂囦欢锛屽崟涓枃浠朵笉瓒呰繃50MB
+ </div>
+ </template>
+ </el-upload>
+ </div>
+ <div v-if="currentAttachmentList.length > 0"
+ class="attachment-list">
+ <h4>宸蹭笂浼犻檮浠讹細</h4>
+ <el-table :data="currentAttachmentList"
+ border
+ size="small">
+ <el-table-column prop="name"
+ label="鏂囦欢鍚�"
+ show-overflow-tooltip />
+ <el-table-column prop="size"
+ label="澶у皬"
+ width="100">
+ <template #default="{ row }">
+ {{ formatFileSize(row.size) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="120"
+ align="center">
+ <template #default="{ row, $index }">
+ <el-button type="primary"
+ link
+ size="small"
+ @click="downloadAttachment(row)">
+ 涓嬭浇
+ </el-button>
+ <el-button type="danger"
+ link
+ size="small"
+ @click="deleteAttachment(row, $index)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <div v-else
+ class="no-attachment">
+ 鏆傛棤闄勪欢
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeAttachmentDialog">鍏抽棴</el-button>
</div>
</template>
</el-dialog>
@@ -248,384 +616,965 @@
</template>
<script setup>
-import {onMounted, ref} from "vue";
-import { Search } from "@element-plus/icons-vue";
-import {
- addCustomer,
- delCustomer,
- getCustomer,
- listCustomer,
- updateCustomer,
-} from "@/api/basicData/customerFile.js";
-import { ElMessageBox } from "element-plus";
-import { userListNoPage } from "@/api/system/user.js";
-import useUserStore from "@/store/modules/user";
-import { getToken } from "@/utils/auth.js";
-const { proxy } = getCurrentInstance();
-const userStore = useUserStore();
+ import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
+ import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
+ import {
+ addCustomer,
+ delCustomer,
+ getCustomer,
+ listCustomer,
+ updateCustomer,
+ addCustomerFollow,
+ updateCustomerFollow,
+ delCustomerFollow,
+ addReturnVisit,
+ getReturnVisit,
+ } from "@/api/basicData/customerFile.js";
+ import { ElMessageBox } from "element-plus";
+ import { userListNoPage } from "@/api/system/user.js";
+ import useUserStore from "@/store/modules/user";
+ import { getToken } from "@/utils/auth.js";
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
-const tableColumn = ref([
- {
- label: "瀹㈡埛鍒嗙被",
- prop: "customerType",
- width: 120,
- },
- {
- label: "瀹㈡埛鍚嶇О",
- prop: "customerName",
- width: 220,
- },
- {
- label: "绾崇◣浜鸿瘑鍒爜",
- prop: "taxpayerIdentificationNumber",
- width: 220,
- },
- {
- label: "鍦板潃鍙婅仈绯绘柟寮�",
- prop: "addressPhone",
- width: 250,
- },
- {
- label: "鑱旂郴浜�",
- prop: "contactPerson",
- },
- {
- label: "鑱旂郴鐢佃瘽",
- prop: "contactPhone",
- width:150
- },
- {
- label: "閾惰鍩烘湰鎴�",
- prop: "basicBankAccount",
- width: 220,
- },
- {
- label: "閾惰璐﹀彿",
- prop: "bankAccount",
- width: 220,
- },
- {
- label: "寮�鎴疯鍙�",
- prop: "bankCode",
- width:220
- },
- {
- label: "缁存姢浜�",
- prop: "maintainer",
- },
- {
- label: "缁存姢鏃堕棿",
- prop: "maintenanceTime",
- width: 100,
- },
- {
- dataType: "action",
- label: "鎿嶄綔",
- align: "center",
- fixed: 'right',
- operation: [
- {
- name: "缂栬緫",
- type: "text",
- clickFun: (row) => {
- openForm("edit", row);
- },
- },
+ // 鍥炶鎻愰啋鐩稿叧
+ const reminderDialogVisible = ref(false);
+ const reminderFormRef = ref();
+ const currentCustomerId = ref();
+ const reminderForm = reactive({
+ customerName: "",
+ reminderSwitch: false,
+ reminderContent: "",
+ reminderTime: "",
+ });
+ const reminderRules = {
+ reminderContent: [
+ { required: true, message: "璇疯緭鍏ユ彁閱掑唴瀹�", trigger: "blur" },
],
- },
-]);
-const tableData = ref([]);
-const selectedRows = ref([]);
-const userList = ref([]);
-const tableLoading = ref(false);
-const page = reactive({
- current: 1,
- size: 100,
- total: 0,
-});
-const total = ref(0);
+ reminderTime: [
+ { required: true, message: "璇烽�夋嫨鎻愰啋鏃堕棿", trigger: "change" },
+ ],
+ };
-// 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
-const operationType = ref("");
-const dialogFormVisible = ref(false);
-const formYYs = ref({ // 鍏朵粬瀛楁...
- contactList: [
- {
- contactPerson: "",
- contactPhone: ""
- }
- ]
-});
-const data = reactive({
- searchForm: {
+ // 娲借皥杩涘害鐩稿叧
+ const negotiationDialogVisible = ref(false);
+ const negotiationFormRef = ref();
+ const negotiationForm = reactive({
+ customerName: "",
+ customerId: "",
+ followUpMethod: "",
+ followUpLevel: "",
+ followUpTime: "",
+ followerUserName: "",
+ content: "",
+ });
+ const negotiationRules = {
+ followUpMethod: [
+ { required: true, message: "璇烽�夋嫨璺熻繘鏂瑰紡", trigger: "change" },
+ ],
+ followUpLevel: [
+ { required: true, message: "璇烽�夋嫨璺熻繘绋嬪害", trigger: "change" },
+ ],
+ followUpTime: [
+ { required: true, message: "璇烽�夋嫨璺熻繘鏃堕棿", trigger: "change" },
+ ],
+ content: [{ required: true, message: "璇疯緭鍏ュ唴瀹�", trigger: "blur" }],
+ };
+
+ // 璇︽儏鐩稿叧
+ const detailDialogVisible = ref(false);
+ const detailForm = reactive({
customerName: "",
customerType: "",
- },
- form: {
- customerName: "",
taxpayerIdentificationNumber: "",
- companyAddress: "",
companyPhone: "",
+ companyAddress: "",
+ basicBankAccount: "",
+ bankAccount: "",
+ bankCode: "",
contactPerson: "",
contactPhone: "",
maintainer: "",
maintenanceTime: "",
- basicBankAccount: "",
- bankAccount: "",
- bankCode: "",
- customerType: "",
- },
- rules: {
- customerName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- taxpayerIdentificationNumber: [
- { required: true, message: "璇疯緭鍏�", trigger: "blur" },
- ],
- companyAddress: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- companyPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- // contactPerson: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- // contactPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- maintainer: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
- maintenanceTime: [
- { required: false, message: "璇烽�夋嫨", trigger: "change" },
- ],
- basicBankAccount: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- bankAccount: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- bankCode: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- customerType: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
- },
-});
-const upload = reactive({
- // 鏄惁鏄剧ず寮瑰嚭灞傦紙瀹㈡埛瀵煎叆锛�
- open: false,
- // 寮瑰嚭灞傛爣棰橈紙瀹㈡埛瀵煎叆锛�
- title: "",
- // 鏄惁绂佺敤涓婁紶
- isUploading: false,
- // 璁剧疆涓婁紶鐨勮姹傚ご閮�
- headers: { Authorization: "Bearer " + getToken() },
- // 涓婁紶鐨勫湴鍧�
- url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData",
- // 鏂囦欢涓婁紶鍓嶇殑鍥炶皟
- beforeUpload: (file) => {
- console.log('鏂囦欢鍗冲皢涓婁紶', file);
- // 鍙互鍦ㄦ澶勫仛鏂囦欢绫诲瀷鎴栧ぇ灏忔牎楠�
- const isValid = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
- if (!isValid) {
- proxy.$modal.msgError("鍙兘涓婁紶 Excel 鏂囦欢");
- }
- return isValid;
- },
- // 鏂囦欢鐘舵�佹敼鍙樻椂鐨勫洖璋�
- onChange: (file, fileList) => {
- console.log('鏂囦欢鐘舵�佹敼鍙�', file, fileList);
- },
- // 鏂囦欢涓婁紶鎴愬姛鏃剁殑鍥炶皟
- onSuccess: (response, file, fileList) => {
- console.log('涓婁紶鎴愬姛', response, file, fileList);
- upload.isUploading = false;
- if(response.code === 200){
- proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
- upload.open = false;
- proxy.$refs["uploadRef"].clearFiles();
- getList();
- }else if(response.code === 500){
- proxy.$modal.msgError(response.msg);
- }else{
- proxy.$modal.msgWarning(response.msg);
- }
- },
- // 鏂囦欢涓婁紶澶辫触鏃剁殑鍥炶皟
- onError: (error, file, fileList) => {
- console.error('涓婁紶澶辫触', error, file, fileList);
- upload.isUploading = false;
- proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
- },
- // 鏂囦欢涓婁紶杩涘害鍥炶皟
- onProgress: (event, file, fileList) => {
- console.log('涓婁紶涓�...', event.percent);
- }
-});
-const { searchForm, form, rules } = toRefs(data);
-const addNewContact = () => {
- formYYs.value.contactList.push({
- contactPerson: "",
- contactPhone: ""
});
-};
+ const negotiationRecords = ref([]);
-const removeContact = (index) => {
- if (formYYs.value.contactList.length > 1) {
- formYYs.value.contactList.splice(index, 1);
- }
-};
-// 鏌ヨ鍒楄〃
-/** 鎼滅储鎸夐挳鎿嶄綔 */
-const handleQuery = () => {
- page.current = 1;
- getList();
-};
-const pagination = (obj) => {
- page.current = obj.page;
- page.size = obj.limit;
- getList();
-};
-const getList = () => {
- tableLoading.value = true;
- listCustomer({ ...searchForm.value, ...page }).then((res) => {
- tableLoading.value = false;
- tableData.value = res.records;
- page.total = res.total;
- });
-};
-// 琛ㄦ牸閫夋嫨鏁版嵁
-const handleSelectionChange = (selection) => {
- selectedRows.value = selection;
-};
-/** 鎻愪氦涓婁紶鏂囦欢 */
-function submitFileForm() {
- upload.isUploading = true;
- proxy.$refs["uploadRef"].submit();
-}
-/** 瀵煎叆鎸夐挳鎿嶄綔 */
-function handleImport() {
- upload.title = "瀹㈡埛瀵煎叆";
- upload.open = true;
-}
-/** 涓嬭浇妯℃澘 */
-function importTemplate() {
- proxy.download("/basic/customer/downloadTemplate", {}, "瀹㈡埛瀵煎叆妯℃澘.xlsx");
-}
-// 鎵撳紑寮规
-const openForm = (type, row) => {
- operationType.value = type;
- form.value = {};
- form.value.maintainer = userStore.nickName;
- formYYs.value.contactList = [
+ // 闄勪欢鐩稿叧
+ const attachmentDialogVisible = ref(false);
+ const attachmentUploadRef = ref();
+ const currentAttachmentList = ref([]);
+ const currentFollowRecord = ref({});
+ const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() };
+
+ // 鍔ㄦ�佹瀯寤轰笂浼燯RL
+ const getAttachmentUploadUrl = () => {
+ const baseUrl =
+ import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload";
+ return currentFollowRecord.value.id
+ ? `${baseUrl}/${currentFollowRecord.value.id}`
+ : baseUrl;
+ };
+
+ const tableColumn = ref([
{
- contactPerson: "",
- contactPhone: ""
- }
- ];
- form.value.maintenanceTime = getCurrentDate();
- userListNoPage().then((res) => {
- userList.value = res.data;
+ label: "瀹㈡埛鍒嗙被",
+ prop: "customerType",
+ width: 120,
+ },
+ {
+ label: "瀹㈡埛鍚嶇О",
+ prop: "customerName",
+ width: 220,
+ },
+ {
+ label: "绾崇◣浜鸿瘑鍒爜",
+ prop: "taxpayerIdentificationNumber",
+ width: 220,
+ },
+ {
+ label: "鍦板潃鍙婅仈绯绘柟寮�",
+ prop: "addressPhone",
+ width: 250,
+ },
+ {
+ label: "鑱旂郴浜�",
+ prop: "contactPerson",
+ },
+ {
+ label: "鑱旂郴鐢佃瘽",
+ prop: "contactPhone",
+ width: 150,
+ },
+ {
+ label: "璺熻繘杩涘害",
+ prop: "followUpLevel",
+ width: 120,
+ },
+ {
+ label: "璺熻繘鏃堕棿",
+ prop: "followUpTime",
+ width: 120,
+ },
+ {
+ label: "閾惰鍩烘湰鎴�",
+ prop: "basicBankAccount",
+ width: 220,
+ },
+ {
+ label: "閾惰璐﹀彿",
+ prop: "bankAccount",
+ width: 220,
+ },
+ {
+ label: "寮�鎴疯鍙�",
+ prop: "bankCode",
+ width: 220,
+ },
+ {
+ label: "缁存姢浜�",
+ prop: "maintainer",
+ },
+ {
+ label: "缁存姢鏃堕棿",
+ prop: "maintenanceTime",
+ width: 100,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 250,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: row => {
+ openDetailDialog(row);
+ },
+ },
+ {
+ name: "鍥炶鎻愰啋",
+ type: "text",
+ clickFun: row => {
+ openReminderDialog(row);
+ },
+ },
+ {
+ name: "娣诲姞娲借皥杩涘害",
+ type: "text",
+ clickFun: row => {
+ openNegotiationDialog(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const userList = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
});
- if (type === "edit") {
- getCustomer(row.id).then((res) => {
- form.value = { ...res.data };
- formYYs.value.contactList = res.data.contactPerson.split(",").map((item, index) => {
- return {
- contactPerson: item,
- contactPhone: res.data.contactPhone.split(",")[index]
- }
- });
+ const total = ref(0);
- });
- }
- dialogFormVisible.value = true;
-};
-// 鎻愪氦琛ㄥ崟
-const submitForm = () => {
- proxy.$refs["formRef"].validate((valid) => {
- if (valid) {
- if (operationType.value === "edit") {
- submitEdit();
- } else {
- submitAdd();
+ // 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+ const operationType = ref("");
+ const dialogFormVisible = ref(false);
+ const formYYs = ref({
+ // 鍏朵粬瀛楁...
+ contactList: [
+ {
+ contactPerson: "",
+ contactPhone: "",
+ },
+ ],
+ });
+ const data = reactive({
+ searchForm: {
+ customerName: "",
+ customerType: "",
+ },
+ form: {
+ customerName: "",
+ taxpayerIdentificationNumber: "",
+ companyAddress: "",
+ companyPhone: "",
+ contactPerson: "",
+ contactPhone: "",
+ maintainer: "",
+ maintenanceTime: "",
+ basicBankAccount: "",
+ bankAccount: "",
+ bankCode: "",
+ customerType: "",
+ },
+ rules: {
+ customerName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ taxpayerIdentificationNumber: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ companyAddress: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ companyPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ // contactPerson: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ // contactPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ maintainer: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
+ maintenanceTime: [
+ { required: false, message: "璇烽�夋嫨", trigger: "change" },
+ ],
+ basicBankAccount: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ bankAccount: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ bankCode: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ customerType: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙瀹㈡埛瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙瀹㈡埛瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData",
+ // 鏂囦欢涓婁紶鍓嶇殑鍥炶皟
+ beforeUpload: file => {
+ console.log("鏂囦欢鍗冲皢涓婁紶", file);
+ // 鍙互鍦ㄦ澶勫仛鏂囦欢绫诲瀷鎴栧ぇ灏忔牎楠�
+ const isValid =
+ file.type ===
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
+ file.name.endsWith(".xlsx") ||
+ file.name.endsWith(".xls");
+ if (!isValid) {
+ proxy.$modal.msgError("鍙兘涓婁紶 Excel 鏂囦欢");
}
- }
+ return isValid;
+ },
+ // 鏂囦欢鐘舵�佹敼鍙樻椂鐨勫洖璋�
+ onChange: (file, fileList) => {
+ console.log("鏂囦欢鐘舵�佹敼鍙�", file, fileList);
+ },
+ // 鏂囦欢涓婁紶鎴愬姛鏃剁殑鍥炶皟
+ onSuccess: (response, file, fileList) => {
+ console.log("涓婁紶鎴愬姛", response, file, fileList);
+ upload.isUploading = false;
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ upload.open = false;
+ proxy.$refs["uploadRef"].clearFiles();
+ getList();
+ } else if (response.code === 500) {
+ proxy.$modal.msgError(response.msg);
+ } else {
+ proxy.$modal.msgWarning(response.msg);
+ }
+ },
+ // 鏂囦欢涓婁紶澶辫触鏃剁殑鍥炶皟
+ onError: (error, file, fileList) => {
+ console.error("涓婁紶澶辫触", error, file, fileList);
+ upload.isUploading = false;
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ },
+ // 鏂囦欢涓婁紶杩涘害鍥炶皟
+ onProgress: (event, file, fileList) => {
+ console.log("涓婁紶涓�...", event.percent);
+ },
});
-};
-// 鎻愪氦鏂板
-const submitAdd = () => {
- if(formYYs.value.contactList.length < 1){
- return proxy.$modal.msgWarning("璇疯嚦灏戞坊鍔犱竴涓仈绯讳汉");
- }
- form.value.contactPerson = formYYs.value.contactList.map(item => item.contactPerson).join(",");
- form.value.contactPhone = formYYs.value.contactList.map(item => item.contactPhone).join(",");
- addCustomer(form.value).then((res) => {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- closeDia();
- getList();
- });
-};
-// 鎻愪氦淇敼
-const submitEdit = () => {
- form.value.contactPerson = formYYs.value.contactList.map(item => item.contactPerson).join(",");
- form.value.contactPhone = formYYs.value.contactList.map(item => item.contactPhone).join(",");
- updateCustomer(form.value).then((res) => {
- proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
- closeDia();
- getList();
- });
-};
-// 鍏抽棴寮规
-const closeDia = () => {
- proxy.resetForm("formRef");
- dialogFormVisible.value = false;
-};
-// 瀵煎嚭
-const handleOut = () => {
- ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
- confirmButtonText: "纭",
- cancelButtonText: "鍙栨秷",
- type: "warning",
- })
- .then(() => {
- proxy.download("/basic/customer/export", {}, "瀹㈡埛妗f.xlsx");
- })
- .catch(() => {
- proxy.$modal.msg("宸插彇娑�");
+ const { searchForm, form, rules } = toRefs(data);
+ const addNewContact = () => {
+ formYYs.value.contactList.push({
+ contactPerson: "",
+ contactPhone: "",
});
-};
-// 鍒犻櫎
-const handleDelete = () => {
- let ids = [];
- if (selectedRows.value.length > 0) {
- // 妫�鏌ユ槸鍚︽湁浠栦汉缁存姢鐨勬暟鎹�
- const unauthorizedData = selectedRows.value.filter(item => item.maintainer !== userStore.nickName);
- if (unauthorizedData.length > 0) {
- proxy.$modal.msgWarning("涓嶅彲鍒犻櫎浠栦汉缁存姢鐨勬暟鎹�");
+ };
+
+ const removeContact = index => {
+ if (formYYs.value.contactList.length > 1) {
+ formYYs.value.contactList.splice(index, 1);
+ }
+ };
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ listCustomer({ ...searchForm.value, ...page }).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.records;
+ page.total = res.total;
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+ /** 鎻愪氦涓婁紶鏂囦欢 */
+ function submitFileForm() {
+ upload.isUploading = true;
+ proxy.$refs["uploadRef"].submit();
+ }
+ /** 瀵煎叆鎸夐挳鎿嶄綔 */
+ function handleImport() {
+ upload.title = "瀹㈡埛瀵煎叆";
+ upload.open = true;
+ }
+ /** 涓嬭浇妯℃澘 */
+ function importTemplate() {
+ proxy.download("/basic/customer/downloadTemplate", {}, "瀹㈡埛瀵煎叆妯℃澘.xlsx");
+ }
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ operationType.value = type;
+ form.value = {};
+ form.value.maintainer = userStore.nickName;
+ formYYs.value.contactList = [
+ {
+ contactPerson: "",
+ contactPhone: "",
+ },
+ ];
+ form.value.maintenanceTime = getCurrentDate();
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ if (type === "edit") {
+ getCustomer(row.id).then(res => {
+ form.value = { ...res.data };
+ formYYs.value.contactList = res.data.contactPerson
+ .split(",")
+ .map((item, index) => {
+ return {
+ contactPerson: item,
+ contactPhone: res.data.contactPhone.split(",")[index],
+ };
+ });
+ });
+ }
+ dialogFormVisible.value = true;
+ };
+ // 鎻愪氦琛ㄥ崟
+ const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "edit") {
+ submitEdit();
+ } else {
+ submitAdd();
+ }
+ }
+ });
+ };
+ // 鎻愪氦鏂板
+ const submitAdd = () => {
+ if (formYYs.value.contactList.length < 1) {
+ return proxy.$modal.msgWarning("璇疯嚦灏戞坊鍔犱竴涓仈绯讳汉");
+ }
+ form.value.contactPerson = formYYs.value.contactList
+ .map(item => item.contactPerson)
+ .join(",");
+ form.value.contactPhone = formYYs.value.contactList
+ .map(item => item.contactPhone)
+ .join(",");
+ addCustomer(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ };
+ // 鎻愪氦淇敼
+ const submitEdit = () => {
+ form.value.contactPerson = formYYs.value.contactList
+ .map(item => item.contactPerson)
+ .join(",");
+ form.value.contactPhone = formYYs.value.contactList
+ .map(item => item.contactPhone)
+ .join(",");
+ updateCustomer(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ };
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/basic/customer/export", {}, "瀹㈡埛妗f.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ // 妫�鏌ユ槸鍚︽湁浠栦汉缁存姢鐨勬暟鎹�
+ const unauthorizedData = selectedRows.value.filter(
+ item => item.maintainer !== userStore.nickName
+ );
+ if (unauthorizedData.length > 0) {
+ proxy.$modal.msgWarning("涓嶅彲鍒犻櫎浠栦汉缁存姢鐨勬暟鎹�");
+ return;
+ }
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
return;
}
- ids = selectedRows.value.map((item) => item.id);
- } else {
- proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
- return;
- }
- ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
- confirmButtonText: "纭",
- cancelButtonText: "鍙栨秷",
- type: "warning",
- })
- .then(() => {
- tableLoading.value = true;
- delCustomer(ids)
- .then((res) => {
- proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
- getList();
- })
- .finally(() => {
- tableLoading.value = false;
- });
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
})
- .catch(() => {
- proxy.$modal.msg("宸插彇娑�");
+ .then(() => {
+ tableLoading.value = true;
+ delCustomer(ids)
+ .then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鎵撳紑鍥炶鎻愰啋寮圭獥
+ const openReminderDialog = row => {
+ currentCustomerId.value = row.id;
+ reminderForm.customerName = row.customerName;
+ reminderForm.reminderSwitch = false;
+ reminderForm.reminderContent = "";
+ reminderForm.reminderTime = "";
+
+ // 灏濊瘯鑾峰彇宸叉湁鐨勫洖璁挎彁閱�
+ getReturnVisit(row.id)
+ .then(res => {
+ if (res.code === 200 && res.data) {
+ reminderForm.reminderSwitch = res.data.isEnabled === 1;
+ reminderForm.reminderContent = res.data.content;
+ reminderForm.reminderTime = res.data.reminderTime;
+ reminderForm.id = res.data.id;
+ }
+ })
+ .catch(error => {
+ console.error("鑾峰彇鍥炶鎻愰啋澶辫触:", error);
+ });
+
+ reminderDialogVisible.value = true;
+ };
+
+ // 鍏抽棴鍥炶鎻愰啋寮圭獥
+ const closeReminderDialog = () => {
+ proxy.resetForm("reminderFormRef");
+ reminderDialogVisible.value = false;
+ };
+ const submitvalue = ref({});
+
+ // 鎻愪氦鍥炶鎻愰啋
+ const submitReminderForm = () => {
+ console.log("鎻愪氦鍥炶鎻愰啋鏁版嵁:", userStore.id, userStore);
+ proxy.$refs.reminderFormRef.validate(valid => {
+ if (valid) {
+ if (reminderForm.id) {
+ submitvalue.value = {
+ id: reminderForm.id,
+ customerId: currentCustomerId.value,
+ isEnabled: reminderForm.reminderSwitch ? 1 : 0,
+ content: reminderForm.reminderContent,
+ reminderTime: reminderForm.reminderTime,
+ remindUserId: userStore.id,
+ };
+ } else {
+ submitvalue.value = {
+ customerId: currentCustomerId.value,
+ isEnabled: reminderForm.reminderSwitch ? 1 : 0,
+ content: reminderForm.reminderContent,
+ reminderTime: reminderForm.reminderTime,
+ remindUserId: userStore.id,
+ };
+ }
+
+ console.log("鎻愪氦鍥炶鎻愰啋鏁版嵁:", submitvalue.value);
+
+ // 璋冪敤鎺ュ彛
+ addReturnVisit(submitvalue.value)
+ .then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鍥炶鎻愰啋璁剧疆鎴愬姛");
+ closeReminderDialog();
+ } else {
+ proxy.$modal.msgError(res.msg || "璁剧疆澶辫触");
+ }
+ })
+ .catch(error => {
+ console.error("璁剧疆鍥炶鎻愰啋澶辫触:", error);
+ proxy.$modal.msgError("璁剧疆澶辫触");
+ });
+ }
});
-};
+ };
-// 鑾峰彇褰撳墠鏃ユ湡骞舵牸寮忓寲涓� YYYY-MM-DD
-function getCurrentDate() {
- const today = new Date();
- const year = today.getFullYear();
- const month = String(today.getMonth() + 1).padStart(2, "0"); // 鏈堜唤浠�0寮�濮�
- const day = String(today.getDate()).padStart(2, "0");
- return `${year}-${month}-${day}`;
-}
+ // 鎵撳紑娲借皥杩涘害寮圭獥
+ const openNegotiationDialog = row => {
+ negotiationForm.customerName = row.customerName;
+ negotiationForm.customerId = row.id;
+ negotiationForm.followUpMethod = "";
+ negotiationForm.followUpLevel = "";
+ negotiationForm.followUpTime = "";
+ negotiationForm.followerUserName = userStore.nickName; // 榛樿褰撳墠鐧诲綍浜�
+ negotiationForm.content = "";
+ // {
+ // "customerId": 152,
+ // "followUpMethod": "鐢佃瘽娌熼��",
+ // "followUpLevel": "娌℃湁鎰忓悜",
+ // "followUpTime": "2026-03-04T15:30:00",
+ // "followerUserName": "绠$悊鍛樿处鍙�",
+ // "content": "111"
+ // }
+ negotiationDialogVisible.value = true;
+ };
-onMounted(() => {
- getList();
-});
+ // 鍏抽棴娲借皥杩涘害寮圭獥
+ const closeNegotiationDialog = () => {
+ proxy.resetForm("negotiationFormRef");
+ // 娓呴櫎缂栬緫鐘舵��
+ delete negotiationForm.editIndex;
+ delete negotiationForm.id;
+ negotiationDialogVisible.value = false;
+ };
+
+ // 鎻愪氦娲借皥杩涘害
+ const submitNegotiationForm = () => {
+ proxy.$refs.negotiationFormRef.validate(valid => {
+ if (valid) {
+ // 鍒ゆ柇鏄柊澧炶繕鏄慨鏀�
+ const isEdit = negotiationForm.editIndex !== undefined;
+
+ if (isEdit) {
+ // 淇敼鎿嶄綔
+ console.log("淇敼娲借皥杩涘害鏁版嵁:", negotiationForm);
+ // 杩欓噷鍙互璋冪敤鏇存柊鎺ュ彛
+ // 瀹為檯椤圭洰涓渶瑕佹牴鎹悗绔帴鍙h繘琛岃皟鏁�
+ // 绀轰緥锛歶pdateCustomerFollow(negotiationForm).then(res => {
+ // // 鏇存柊鏈湴鏁版嵁
+ // const index = negotiationForm.editIndex;
+ // negotiationRecords.value[index] = {
+ // followUpTime: negotiationForm.followUpTime,
+ // followUpMethod: negotiationForm.followUpMethod,
+ // followUpLevel: negotiationForm.followUpLevel,
+ // followerUserName: negotiationForm.followerUserName,
+ // content: negotiationForm.content,
+ // id: negotiationForm.id,
+ // };
+ // proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ // closeNegotiationDialog();
+ // });
+ updateCustomerFollow(negotiationForm).then(res => {
+ // 鏇存柊鏈湴鏁版嵁
+ getCustomer(negotiationForm.customerId).then(res => {
+ // 鏇存柊鏈湴鏁版嵁
+ negotiationRecords.value = res.data.followUpList || [];
+ });
+ });
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeNegotiationDialog();
+ } else {
+ // 鏂板鎿嶄綔
+ console.log("鎻愪氦娲借皥杩涘害鏁版嵁:", negotiationForm);
+ addCustomerFollow(negotiationForm).then(res => {
+ // 娣诲姞鎴愬姛鍚庢洿鏂拌鎯呴〉闈㈢殑杩涘害璁板綍
+ const newRecord = {
+ followUpTime: negotiationForm.followUpTime,
+ followUpMethod: negotiationForm.followUpMethod,
+ followUpLevel: negotiationForm.followUpLevel,
+ followerUserName: negotiationForm.followerUserName,
+ content: negotiationForm.content,
+ };
+ negotiationRecords.value.unshift(newRecord);
+
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeNegotiationDialog();
+ getList();
+ });
+ }
+ }
+ });
+ };
+
+ // 鎵撳紑璇︽儏寮圭獥
+ const openDetailDialog = row => {
+ // 璋冪敤getCustomer鎺ュ彛鑾峰彇瀹㈡埛璇︽儏
+ getCustomer(row.id).then(res => {
+ // 濉厖瀹㈡埛鍩烘湰淇℃伅
+ Object.assign(detailForm, res.data);
+
+ // 鑾峰彇娲借皥杩涘害璁板綍
+ negotiationRecords.value = res.data.followUpList || [];
+
+ detailDialogVisible.value = true;
+ });
+ };
+
+ // 鍏抽棴璇︽儏寮圭獥
+ const closeDetailDialog = () => {
+ detailDialogVisible.value = false;
+ };
+
+ // 淇敼娲借皥璁板綍
+ const editNegotiationRecord = (row, index) => {
+ // 灏嗗綋鍓嶈褰曟暟鎹~鍏呭埌琛ㄥ崟
+ Object.assign(negotiationForm, {
+ customerName: row.customerName,
+ customerId: row.customerId,
+ followUpMethod: row.followUpMethod,
+ followUpLevel: row.followUpLevel,
+ followUpTime: row.followUpTime,
+ followerUserName: row.followerUserName,
+ content: row.content,
+ id: row.id, // 璁板綍ID鐢ㄤ簬鏇存柊
+ editIndex: index, // 璁板綍绱㈠紩鐢ㄤ簬鏈湴鏇存柊
+ });
+ negotiationDialogVisible.value = true;
+ };
+
+ // 鍒犻櫎娲借皥璁板綍
+ const deleteNegotiationRecord = (row, index) => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ繖鏉℃唇璋堣褰曞悧锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 杩欓噷鍙互璋冪敤鍒犻櫎鎺ュ彛
+ // 瀹為檯椤圭洰涓渶瑕佹牴鎹悗绔帴鍙h繘琛岃皟鏁�
+ // 绀轰緥锛歞eleteCustomerFollow(row.id).then(() => {
+ // negotiationRecords.value.splice(index, 1);
+ // proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ // });
+ delCustomerFollow(row.id).then(() => {
+ // 鍒犻櫎鎴愬姛鍚庢洿鏂版湰鍦版暟鎹�
+ getCustomer(row.customerId).then(res => {
+ // 鏇存柊鏈湴鏁版嵁
+ negotiationRecords.value = res.data.followUpList || [];
+ });
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ });
+ // 鏈湴鍒犻櫎锛堟ā鎷燂級
+ negotiationRecords.value.splice(index, 1);
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑堝垹闄�");
+ });
+ };
+
+ // 鎵撳紑闄勪欢寮圭獥
+ const openAttachmentDialog = row => {
+ currentFollowRecord.value = row;
+ // 杞崲涓虹鍚圗lement Plus fileList鏍煎紡鐨勬暟缁�
+ currentAttachmentList.value = (row.fileList || []).map((file, index) => ({
+ name: file.fileName,
+ url: file.fileUrl,
+ size: file.fileSize,
+ id: file.id,
+ uid: file.id || index,
+ status: "success",
+ }));
+
+ attachmentDialogVisible.value = true;
+ };
+
+ // 鍏抽棴闄勪欢寮圭獥
+ const closeAttachmentDialog = () => {
+ attachmentDialogVisible.value = false;
+ currentFollowRecord.value = {};
+ currentAttachmentList.value = [];
+ };
+
+ // 闄勪欢涓婁紶鎴愬姛
+ const handleAttachmentSuccess = (response, file, fileList) => {
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+ // 鏇存柊褰撳墠璁板綍鐨勯檮浠跺垪琛�
+ currentAttachmentList.value = fileList.map(item => ({
+ name: item.name,
+ size: item.size,
+ url: item.response?.data?.url || item.url,
+ id: item.response?.data?.id,
+ uid: item.uid,
+ status: "success",
+ }));
+ // 鏇存柊鍘熻褰曚腑鐨刦iles瀛楁
+ if (currentFollowRecord.value) {
+ currentFollowRecord.value.files = [...currentAttachmentList.value];
+ }
+ } else {
+ proxy.$modal.msgError(response.msg || "涓婁紶澶辫触");
+ }
+ };
+
+ // 闄勪欢涓婁紶澶辫触
+ const handleAttachmentError = (error, file, fileList) => {
+ console.error("涓婁紶澶辫触:", error);
+ proxy.$modal.msgError("涓婁紶澶辫触");
+ };
+
+ // 闄勪欢绉婚櫎
+ const handleAttachmentRemove = (file, fileList) => {
+ currentAttachmentList.value = fileList;
+ // 鏇存柊鍘熻褰曚腑鐨刦iles瀛楁
+ if (currentFollowRecord.value) {
+ currentFollowRecord.value.files = [...fileList];
+ }
+ };
+
+ // 闄勪欢涓婁紶鍓嶆牎楠�
+ const beforeAttachmentUpload = file => {
+ const maxSize = 50 * 1024 * 1024; // 50MB
+ if (file.size > maxSize) {
+ proxy.$modal.msgError("鏂囦欢澶у皬涓嶈兘瓒呰繃50MB");
+ return false;
+ }
+ return true;
+ };
+
+ // 鏍煎紡鍖栨枃浠跺ぇ灏�
+ const formatFileSize = size => {
+ if (size < 1024) {
+ return size + " B";
+ } else if (size < 1024 * 1024) {
+ return (size / 1024).toFixed(2) + " KB";
+ } else {
+ return (size / (1024 * 1024)).toFixed(2) + " MB";
+ }
+ };
+
+ // 涓嬭浇闄勪欢
+ const downloadAttachment = row => {
+ if (row.url) {
+ // proxy.download(row.url, {}, row.name);
+ proxy.$download.name(row.url);
+ } else {
+ proxy.$modal.msgError("涓嬭浇閾炬帴涓嶅瓨鍦�");
+ }
+ };
+
+ // 鍒犻櫎闄勪欢
+ const deleteAttachment = (row, index) => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ繖涓檮浠跺悧锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 璋冪敤鍚庣鎺ュ彛鍒犻櫎闄勪欢
+ const deleteUrl =
+ import.meta.env.VITE_APP_BASE_API +
+ "/basic/customer-follow/file/" +
+ row.id;
+ fetch(deleteUrl, {
+ method: "DELETE",
+ headers: {
+ Authorization: "Bearer " + getToken(),
+ "Content-Type": "application/json",
+ },
+ })
+ .then(response => response.json())
+ .then(res => {
+ if (res.code === 200) {
+ // 鍒犻櫎鎴愬姛鍚庢洿鏂版湰鍦版枃浠跺垪琛�
+ currentAttachmentList.value.splice(index, 1);
+ // 鏇存柊鍘熻褰曚腑鐨刦iles瀛楁
+ if (currentFollowRecord.value) {
+ currentFollowRecord.value.files = [
+ ...currentAttachmentList.value,
+ ];
+ }
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(error => {
+ console.error("鍒犻櫎闄勪欢澶辫触:", error);
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑堝垹闄�");
+ });
+ };
+
+ // 鑾峰彇褰撳墠鏃ユ湡骞舵牸寮忓寲涓� YYYY-MM-DD
+ function getCurrentDate() {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, "0"); // 鏈堜唤浠�0寮�濮�
+ const day = String(today.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ }
+
+ onMounted(() => {
+ getList();
+ });
</script>
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+ .detail-section {
+ margin-bottom: 20px;
+ padding: 15px;
+ background-color: #f9f9f9;
+ border-radius: 4px;
+ }
+
+ .section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+
+ .section-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin: 0 0 15px 0;
+ color: #333;
+ }
+
+ .info-display {
+ background-color: #fff;
+ padding: 15px;
+ border-radius: 4px;
+ }
+
+ .info-item {
+ margin-bottom: 12px;
+ display: flex;
+ align-items: flex-start;
+ }
+
+ .info-label {
+ width: 120px;
+ font-weight: 500;
+ color: #606266;
+ margin-right: 10px;
+ }
+
+ .info-value {
+ flex: 1;
+ color: #303133;
+ word-break: break-word;
+ }
+
+ .no-records {
+ text-align: center;
+ padding: 30px;
+ color: #999;
+ font-size: 14px;
+ }
+
+ .attachment-section {
+ .upload-area {
+ margin-bottom: 20px;
+ padding: 20px;
+ background-color: #f9f9f9;
+ border-radius: 4px;
+ border: 1px dashed #d9d9d9;
+
+ .el-upload__tip {
+ margin-top: 10px;
+ color: #909399;
+ }
+ }
+
+ .attachment-list {
+ h4 {
+ margin: 0 0 10px 0;
+ font-size: 14px;
+ color: #606266;
+ }
+ }
+
+ .no-attachment {
+ text-align: center;
+ padding: 40px;
+ color: #909399;
+ font-size: 14px;
+ }
+ }
+</style>
diff --git a/src/views/basicData/product/ImportExcel/index.vue b/src/views/basicData/product/ImportExcel/index.vue
index c25d254..2afbb64 100644
--- a/src/views/basicData/product/ImportExcel/index.vue
+++ b/src/views/basicData/product/ImportExcel/index.vue
@@ -2,16 +2,10 @@
<el-button type="info" plain icon="Upload" @click="handleImport">
瀵煎叆
</el-button>
- <el-dialog v-model="upload.open" :title="upload.title">
- <FileUpload
- ref="fileUploadRef"
- accept=".xlsx, .xls"
- :headers="upload.headers"
- :action="upload.url + '?updateSupport=' + upload.updateSupport"
- :disabled="upload.isUploading"
- :showTip="false"
- @success="handleFileSuccess"
- />
+ <el-dialog v-model="upload.open" :title="upload.title" @close="handleDialogClose">
+ <FileUpload ref="fileUploadRef" accept=".xlsx, .xls" :headers="upload.headers" :action="uploadUrl"
+ :disabled="upload.isUploading" :showTip="true" @success="handleFileSuccess"
+ :downloadTemplate="handleDownloadTemplate" />
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
@@ -22,15 +16,19 @@
</template>
<script setup>
-import { reactive } from "vue";
+import { reactive, computed } from "vue";
import { getToken } from "@/utils/auth.js";
import { FileUpload } from "@/components/Upload";
import { ElMessage } from "element-plus";
+import { downloadProductModelImportTemplate } from "@/api/basicData/product.js";
defineOptions({
name: "浜у搧缁存姢瀵煎叆",
});
+const props = defineProps({
+ productId: { type: [String, Number], default: "" },
+});
const emits = defineEmits(["uploadSuccess"]);
const fileUploadRef = ref();
const upload = reactive({
@@ -42,11 +40,20 @@
isUploading: false,
// 璁剧疆涓婁紶鐨勮姹傚ご閮�
headers: { Authorization: "Bearer " + getToken() },
- // 涓婁紶鐨勫湴鍧�
- url: import.meta.env.VITE_APP_BASE_API + "/system/supplier/import",
});
+// 涓婁紶鐨勫湴鍧�锛堟惡甯� productId 鍙傛暟锛屼紶缁欏悗绔殑 importProduct 鎺ュ彛锛�
+const uploadUrl = computed(
+ () =>
+ import.meta.env.VITE_APP_BASE_API +
+ "/basic/product/import" +
+ (props.productId ? `?productId=${props.productId}` : "")
+);
// 鐐瑰嚮瀵煎叆
const handleImport = () => {
+ if (!props.productId) {
+ ElMessage({ message: "璇峰厛閫夋嫨浜у搧", type: "warning" });
+ return;
+ }
upload.open = true;
upload.title = "浜у搧瀵煎叆";
};
@@ -55,14 +62,54 @@
fileUploadRef.value.uploadApi();
};
+// 鍏抽棴寮圭獥鏃舵竻闄ゅ凡閫夋枃浠�
+const handleDialogClose = () => {
+ fileUploadRef.value?.clearFiles?.();
+};
+
const handleFileSuccess = (response) => {
const { code, msg } = response;
if (code == 200) {
- ElMessage({ message: "瀵煎叆鎴愬姛", type: "success" });
+ ElMessage({ message: msg || "瀵煎叆鎴愬姛", type: "success" });
upload.open = false;
emits("uploadSuccess");
} else {
ElMessage({ message: msg, type: "error" });
}
};
+
+// 涓嬭浇 Excel 瀵煎叆妯℃澘
+const handleDownloadTemplate = () => {
+ downloadProductModelImportTemplate()
+ .then((blobData) => {
+ const blob =
+ blobData instanceof Blob
+ ? blobData
+ : new Blob([blobData], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = "浜у搧瀵煎叆妯℃澘.xlsx";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ ElMessage({ message: "妯℃澘涓嬭浇鎴愬姛", type: "success" });
+ })
+ .catch(() => {
+ ElMessage({ message: "妯℃澘涓嬭浇澶辫触", type: "error" });
+ });
+};
</script>
+
+<style scoped>
+.import-tip {
+ margin-top: 12px;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+}
+
+.import-tip .el-button {
+ margin-left: 8px;
+}
+</style>
diff --git a/src/views/basicData/product/ProductSelectDialog.vue b/src/views/basicData/product/ProductSelectDialog.vue
index 70dbb16..ded23cc 100644
--- a/src/views/basicData/product/ProductSelectDialog.vue
+++ b/src/views/basicData/product/ProductSelectDialog.vue
@@ -32,10 +32,10 @@
</div>
<template #footer>
- <el-button @click="close()">鍙栨秷</el-button>
<el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm">
纭畾
</el-button>
+ <el-button @click="close()">鍙栨秷</el-button>
</template>
</el-dialog>
</template>
diff --git a/src/views/basicData/product/index.vue b/src/views/basicData/product/index.vue
index c9058aa..7b2a819 100644
--- a/src/views/basicData/product/index.vue
+++ b/src/views/basicData/product/index.vue
@@ -73,7 +73,7 @@
<el-button type="primary" @click="openModelDia('add')">
鏂板瑙勬牸鍨嬪彿
</el-button>
- <ImportExcel @uploadSuccess="getModelList" />
+ <ImportExcel :product-id="currentId" @uploadSuccess="getModelList" />
<el-button
type="danger"
@click="handleDelete"
diff --git a/src/views/basicData/supplierManage/components/BlacklistTab.vue b/src/views/basicData/supplierManage/components/BlacklistTab.vue
index 51df93c..8f6204b 100644
--- a/src/views/basicData/supplierManage/components/BlacklistTab.vue
+++ b/src/views/basicData/supplierManage/components/BlacklistTab.vue
@@ -162,6 +162,16 @@
</el-row>
<el-row :gutter="30">
<el-col :span="12">
+ <el-form-item label="渚涘簲鍟嗙被鍨嬶細" prop="supplierType">
+ <el-select v-model="form.supplierType" placeholder="璇烽�夋嫨" clearable>
+ <el-option label="鐢�" value="鐢�" />
+ <el-option label="涔�" value="涔�" />
+ <el-option label="涓�" value="涓�" />
+ <el-option label="涓�" value="涓�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
<el-form-item label="鏄惁鐧藉悕鍗曪細" prop="isWhite">
<el-select v-model="form.isWhite" placeholder="璇烽�夋嫨" clearable>
<el-option label="鏄�" :value="0" />
@@ -248,6 +258,11 @@
label: "渚涘簲鍟嗗悕绉�",
prop: "supplierName",
width: 250,
+ },
+ {
+ label: "渚涘簲鍟嗙被鍨�",
+ prop: "supplierType",
+ width: 120,
},
{
label: "绾崇◣浜鸿瘑鍒彿",
@@ -346,6 +361,7 @@
contactUserPhone: "",
maintainUserId: "",
maintainTime: "",
+ supplierType: "",
isWhite: "",
},
rules: {
@@ -361,6 +377,7 @@
contactUserPhone: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
maintainUserId: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
maintainTime: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
+ supplierType: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟嗙被鍨�", trigger: "change" }],
},
});
const { searchForm, form, rules } = toRefs(data);
diff --git a/src/views/basicData/supplierManage/components/HomeTab.vue b/src/views/basicData/supplierManage/components/HomeTab.vue
index 7b90272..85c3265 100644
--- a/src/views/basicData/supplierManage/components/HomeTab.vue
+++ b/src/views/basicData/supplierManage/components/HomeTab.vue
@@ -168,6 +168,16 @@
</el-row>
<el-row :gutter="30">
<el-col :span="12">
+ <el-form-item label="渚涘簲鍟嗙被鍨嬶細" prop="supplierType">
+ <el-select v-model="form.supplierType" placeholder="璇烽�夋嫨" clearable>
+ <el-option label="鐢�" value="鐢�" />
+ <el-option label="涔�" value="涔�" />
+ <el-option label="涓�" value="涓�" />
+ <el-option label="涓�" value="涓�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
<el-form-item label="鏄惁鐧藉悕鍗曪細" prop="isWhite">
<el-select v-model="form.isWhite" placeholder="璇烽�夋嫨" clearable>
<el-option label="鏄�" :value="0" />
@@ -254,6 +264,11 @@
label: "渚涘簲鍟嗗悕绉�",
prop: "supplierName",
width: 250,
+ },
+ {
+ label: "渚涘簲鍟嗙被鍨�",
+ prop: "supplierType",
+ width: 120,
},
{
label: "绾崇◣浜鸿瘑鍒彿",
@@ -352,6 +367,7 @@
contactUserPhone: "",
maintainUserId: "",
maintainTime: "",
+ supplierType: "",
isWhite: "",
},
rules: {
@@ -367,6 +383,7 @@
contactUserPhone: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
maintainUserId: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
maintainTime: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
+ supplierType: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟嗙被鍨�", trigger: "change" }],
},
});
const { searchForm, form, rules } = toRefs(data);
diff --git a/src/views/collaborativeApproval/approvalProcess/index.vue b/src/views/collaborativeApproval/approvalProcess/index.vue
index 65941e1..33bde47 100644
--- a/src/views/collaborativeApproval/approvalProcess/index.vue
+++ b/src/views/collaborativeApproval/approvalProcess/index.vue
@@ -162,7 +162,6 @@
{
label: isQuotationType ? "鎶ヤ环鍗曞彿" : isPurchaseType ? "閲囪喘鍚堝悓鍙�" : "瀹℃壒浜嬬敱",
prop: "approveReason",
- width: 200
},
{
label: "鐢宠浜�",
@@ -202,50 +201,61 @@
});
// 鎿嶄綔鍒�
+ const actionOperations = [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ disabled: (row) =>
+ currentApproveType.value === 5 ||
+ currentApproveType.value === 6 ||
+ currentApproveType.value === 7 ||
+ row.approveStatus == 2 ||
+ row.approveStatus == 1 ||
+ row.approveStatus == 4
+ },
+ {
+ name: "瀹℃牳",
+ type: "text",
+ clickFun: (row) => {
+ openApprovalDia("approval", row);
+ },
+ disabled: (row) =>
+ row.approveUserCurrentId == null ||
+ row.approveStatus == 2 ||
+ row.approveStatus == 3 ||
+ row.approveStatus == 4 ||
+ row.approveUserCurrentId !== userStore.id
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => {
+ openApprovalDia("view", row);
+ },
+ },
+ ];
+
+ // 鎶ヤ环瀹℃壒锛堢被鍨� 6锛変笉灞曠ず鈥滈檮浠垛�濇搷浣�
+ if (!isQuotationType) {
+ actionOperations.push({
+ name: "闄勪欢",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ });
+ }
+
baseColumns.push({
dataType: "action",
label: "鎿嶄綔",
align: "center",
fixed: "right",
width: 230,
- operation: [
- {
- name: "缂栬緫",
- type: "text",
- clickFun: (row) => {
- openForm("edit", row);
- },
- disabled: (row) =>
- currentApproveType.value === 5 ||
- currentApproveType.value === 6 ||
- currentApproveType.value === 7 ||
- row.approveStatus == 2 ||
- row.approveStatus == 1 ||
- row.approveStatus == 4
- },
- {
- name: "瀹℃牳",
- type: "text",
- clickFun: (row) => {
- openApprovalDia("approval", row);
- },
- disabled: (row) => row.approveUserCurrentId == null || row.approveStatus == 2 || row.approveStatus == 3 || row.approveStatus == 4 || row.approveUserCurrentId !== userStore.id
- },
- {
- name: "璇︽儏",
- type: "text",
- clickFun: (row) => {
- openApprovalDia('view', row);
- },
- },
- {
- name: "闄勪欢",
- type: "text",
- clickFun: (row) => {
- downLoadFile(row);
- },
- },
- ],
+ operation: actionOperations,
});
return baseColumns;
diff --git a/src/views/collaborativeApproval/customerVisit/index.vue b/src/views/collaborativeApproval/customerVisit/index.vue
new file mode 100644
index 0000000..3122f3f
--- /dev/null
+++ b/src/views/collaborativeApproval/customerVisit/index.vue
@@ -0,0 +1,269 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�">
+ <el-input
+ v-model="searchForm.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎷滆浜猴細">
+ <el-input
+ v-model="searchForm.visitingPeople"
+ placeholder="璇疯緭鍏ユ嫓璁夸汉"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)"
+ >
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" width="150" show-overflow-tooltip />
+ <el-table-column label="鑱旂郴浜�" prop="contact" width="120" show-overflow-tooltip />
+ <el-table-column label="鑱旂郴鐢佃瘽" prop="contactPhone" width="140" show-overflow-tooltip />
+ <el-table-column label="鎷滆鐩殑" prop="purposeVisit" width="150" show-overflow-tooltip />
+ <el-table-column label="鎷滆鏃堕棿" prop="purposeDate" width="180" show-overflow-tooltip />
+ <el-table-column label="鎷滆鍦扮偣" prop="visitAddress" min-width="200" show-overflow-tooltip />
+ <el-table-column label="鎷滆浜�" prop="visitingPeople" width="120" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" width="100" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="viewDetail(scope.row)">鏌ョ湅</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+
+ <!-- 璇︽儏寮圭獥 -->
+ <el-dialog
+ v-model="detailVisible"
+ title="瀹㈡埛鎷滆璁板綍璇︽儏"
+ width="600px"
+ @close="closeDetail"
+ >
+ <div class="content-container">
+ <!-- 瀹㈡埛淇℃伅 -->
+ <div class="section">
+ <div class="section-title">瀹㈡埛淇℃伅</div>
+ <div class="info-item">
+ <span class="info-label">瀹㈡埛鍚嶇О</span>
+ <span class="info-value">{{ detailForm.customerName || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鑱旂郴浜�</span>
+ <span class="info-value">{{ detailForm.contact || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鑱旂郴鐢佃瘽</span>
+ <span class="info-value">{{ detailForm.contactPhone || '-' }}</span>
+ </div>
+ </div>
+
+ <!-- 鎷滆淇℃伅 -->
+ <div class="section">
+ <div class="section-title">鎷滆淇℃伅</div>
+ <div class="info-item">
+ <span class="info-label">鎷滆鐩殑</span>
+ <span class="info-value">{{ detailForm.purposeVisit || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鎷滆鏃堕棿</span>
+ <span class="info-value">{{ detailForm.purposeDate || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鎷滆鍦扮偣</span>
+ <span class="info-value multi-line">{{ detailForm.visitAddress || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鎷滆浜�</span>
+ <span class="info-value">{{ detailForm.visitingPeople || '-' }}</span>
+ </div>
+ <div class="info-item" v-if="detailForm.latitude && detailForm.longitude">
+ <span class="info-label">缁忕含搴�</span>
+ <span class="info-value">{{ detailForm.latitude }}, {{ detailForm.longitude }}</span>
+ </div>
+ </div>
+
+ <!-- 澶囨敞淇℃伅 -->
+ <div class="section">
+ <div class="section-title">澶囨敞淇℃伅</div>
+ <div class="info-item remark-item">
+ <span class="info-label">澶囨敞</span>
+ <span class="info-value multi-line">{{ detailForm.remark || '-' }}</span>
+ </div>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDetail">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
+import pagination from '@/components/PIMTable/Pagination.vue'
+import { getVisitRecords } from '@/api/collaborativeApproval/customerVisit.js'
+
+const { proxy } = getCurrentInstance()
+const tableData = ref([])
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 10,
+})
+const total = ref(0)
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ customerName: '',
+ visitingPeople: '',
+})
+
+// 璇︽儏鐩稿叧
+const detailVisible = ref(false)
+const detailForm = ref({})
+
+// 鏌ヨ鍒楄〃
+const handleQuery = () => {
+ page.current = 1
+ getList()
+}
+
+// 鍒嗛〉鍙樺寲
+const paginationChange = (obj) => {
+ page.current = obj.page
+ page.size = obj.limit
+ getList()
+}
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = () => {
+ tableLoading.value = true
+ getVisitRecords({ ...searchForm, ...page })
+ .then((res) => {
+ tableLoading.value = false
+ if (res.code === 200) {
+ tableData.value = res.data?.records || res.records || []
+ total.value = res.data?.total || res.total || 0
+ } else {
+ proxy.$modal.msgError(res.msg || '鑾峰彇鏁版嵁澶辫触')
+ }
+ })
+ .catch(() => {
+ tableLoading.value = false
+ })
+}
+
+// 鏌ョ湅璇︽儏
+const viewDetail = (row) => {
+ detailForm.value = { ...row }
+ detailVisible.value = true
+}
+
+// 鍏抽棴璇︽儏
+const closeDetail = () => {
+ detailVisible.value = false
+ detailForm.value = {}
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+.table_list {
+ margin-top: unset;
+}
+
+.content-container {
+ padding: 10px;
+}
+
+.section {
+ margin-bottom: 24px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.section-title {
+ font-size: 16px;
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 16px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #e4e7ed;
+}
+
+.info-item {
+ display: flex;
+ margin-bottom: 12px;
+ line-height: 1.6;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &.remark-item {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .info-label {
+ margin-bottom: 8px;
+ }
+
+ .info-value {
+ width: 100%;
+ }
+ }
+}
+
+.info-label {
+ font-weight: 500;
+ color: #606266;
+ min-width: 100px;
+ margin-right: 12px;
+ flex-shrink: 0;
+}
+
+.info-value {
+ color: #303133;
+ flex: 1;
+ word-break: break-all;
+
+ &.multi-line {
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
index 4fc7088..b2fb88d 100644
--- a/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
+++ b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
@@ -90,7 +90,7 @@
style="width: 100%"
>
<el-option
- v-for="time in timeOptions"
+ v-for="time in startTimeOptions"
:key="time.value"
:label="time.label"
:value="time.value"
@@ -106,7 +106,7 @@
style="width: 100%"
>
<el-option
- v-for="time in timeOptions"
+ v-for="time in endTimeOptions"
:key="time.value"
:label="time.label"
:value="time.value"
@@ -152,7 +152,7 @@
</template>
<script setup>
-import {ref, reactive, onMounted} from 'vue'
+import {ref, reactive, onMounted, computed, watch} from 'vue'
import {ElMessage} from 'element-plus'
import {Plus, Document, Promotion, Bell} from '@element-plus/icons-vue'
import {getRoomEnum, saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
@@ -196,17 +196,6 @@
description: ''
})
-// 琛ㄥ崟鏍¢獙瑙勫垯
-const rules = {
- title: [{required: true, message: '璇疯緭鍏ヤ細璁富棰�', trigger: 'blur'}],
- roomId: [{required: true, message: '璇烽�夋嫨浼氳瀹�', trigger: 'change'}],
- host: [{required: true, message: '璇疯緭鍏ヤ富鎸佷汉', trigger: 'blur'}],
- meetingDate: [{required: true, message: '璇烽�夋嫨浼氳鏃ユ湡', trigger: 'change'}],
- startTime: [{required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change'}],
- endTime: [{required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change'}],
- participants: [{required: true, message: '璇烽�夋嫨鍙備細浜哄憳', trigger: 'change'}]
-}
-
// 琛ㄥ崟寮曠敤
const meetingFormRef = ref(null)
@@ -219,10 +208,108 @@
// 鏃堕棿閫夐」锛堜互鍗婂皬鏃朵负闂撮殧锛�
const timeOptions = ref([])
+const getTimeInMinutes = (time) => {
+ if (!time) return -1
+ const [hour, minute] = time.split(':').map(Number)
+ return hour * 60 + minute
+}
+
+const isToday = (dateText) => {
+ if (!dateText) return false
+ const [year, month, day] = dateText.split('-').map(Number)
+ const now = new Date()
+ return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate()
+}
+
+const validateStartTime = (_rule, value, callback) => {
+ if (!value) {
+ callback()
+ return
+ }
+
+ if (isToday(meetingForm.meetingDate)) {
+ const now = new Date()
+ const currentMinutes = now.getHours() * 60 + now.getMinutes()
+ if (getTimeInMinutes(value) > currentMinutes) {
+ callback(new Error('褰撳ぉ寮�濮嬫椂闂翠笉鑳芥櫄浜庡綋鍓嶆椂闂�'))
+ return
+ }
+ }
+
+ callback()
+}
+
+const validateEndTime = (_rule, value, callback) => {
+ if (!value || !meetingForm.startTime) {
+ callback()
+ return
+ }
+
+ if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) {
+ callback(new Error('缁撴潫鏃堕棿蹇呴』澶т簬寮�濮嬫椂闂�'))
+ return
+ }
+
+ callback()
+}
+
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const rules = {
+ title: [{required: true, message: '璇疯緭鍏ヤ細璁富棰�', trigger: 'blur'}],
+ roomId: [{required: true, message: '璇烽�夋嫨浼氳瀹�', trigger: 'change'}],
+ host: [{required: true, message: '璇疯緭鍏ヤ富鎸佷汉', trigger: 'blur'}],
+ meetingDate: [{required: true, message: '璇烽�夋嫨浼氳鏃ユ湡', trigger: 'change'}],
+ startTime: [
+ {required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change'},
+ {validator: validateStartTime, trigger: 'change'}
+ ],
+ endTime: [
+ {required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change'},
+ {validator: validateEndTime, trigger: 'change'}
+ ],
+ participants: [{required: true, message: '璇烽�夋嫨鍙備細浜哄憳', trigger: 'change'}]
+}
+
+const startTimeOptions = computed(() => {
+ if (!isToday(meetingForm.meetingDate)) {
+ return timeOptions.value
+ }
+ const now = new Date()
+ const currentMinutes = now.getHours() * 60 + now.getMinutes()
+ return timeOptions.value.filter(item => getTimeInMinutes(item.value) <= currentMinutes)
+})
+
+const endTimeOptions = computed(() => {
+ if (!meetingForm.startTime) {
+ return timeOptions.value
+ }
+ const startMinutes = getTimeInMinutes(meetingForm.startTime)
+ return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes)
+})
+
// 鍒濆鍖栨椂闂撮�夐」
const initTimeOptions = () => {
const options = []
+ const now = new Date()
+ const currentHour = now.getHours()
+ const currentMinute = now.getMinutes()
+ // meetingDate 鏄� "yyyy-MM-dd"
+ const meetingDate = new Date(meetingForm.meetingDate)
+
+ const isSameDay =
+ now.getFullYear() === meetingDate.getFullYear() &&
+ now.getMonth() === meetingDate.getMonth() &&
+ now.getDate() === meetingDate.getDate()
+
+ console.log('鏄惁鍚屼竴澶�:', isSameDay)
for (let hour = 8; hour <= 18; hour++) {
+ // 寮�濮嬫椂闂村繀椤绘櫄浜庡綋鍓嶆椂闂�
+ if (hour < currentHour && isSameDay) {
+ continue
+ }
+ if (hour === currentHour && currentMinute > 30 && isSameDay) {
+ continue
+ }
// 姣忎釜灏忔椂娣诲姞涓や釜閫夐」锛氭暣鐐瑰拰鍗婄偣
options.push({
value: `${hour.toString().padStart(2, '0')}:00`,
@@ -239,6 +326,32 @@
timeOptions.value = options
}
+watch(() => meetingForm.meetingDate, () => {
+ if (meetingForm.startTime && !startTimeOptions.value.some(item => item.value === meetingForm.startTime)) {
+ meetingForm.startTime = ''
+ }
+ if (meetingForm.endTime && !endTimeOptions.value.some(item => item.value === meetingForm.endTime)) {
+ meetingForm.endTime = ''
+ }
+ if (meetingForm.startTime) {
+ meetingFormRef.value?.validateField('startTime')
+ }
+ if (meetingForm.endTime) {
+ meetingFormRef.value?.validateField('endTime')
+ }
+ initTimeOptions()
+})
+
+watch(() => meetingForm.startTime, () => {
+ if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) {
+ meetingForm.endTime = ''
+ }
+ if (meetingForm.endTime) {
+ meetingFormRef.value?.validateField('endTime')
+ }
+
+})
+
// 绂佺敤鏃ユ湡锛堢鐢ㄤ粖澶╀箣鍓嶇殑鏃ユ湡锛�
const disabledDate = (time) => {
// 绂佺敤浠婂ぉ涔嬪墠鐨勬棩鏈�
diff --git a/src/views/customerService/afterSalesHandling/components/formDia.vue b/src/views/customerService/afterSalesHandling/components/formDia.vue
index daccaca..4b2964b 100644
--- a/src/views/customerService/afterSalesHandling/components/formDia.vue
+++ b/src/views/customerService/afterSalesHandling/components/formDia.vue
@@ -10,7 +10,7 @@
:model="form"
label-width="140px"
label-position="top"
- :rules="rules"
+ :rules="operationType === 'view' ? {} : rules"
ref="formRef"
>
<el-row :gutter="30">
@@ -63,7 +63,7 @@
v-model="form.proDesc"
placeholder="璇疯緭鍏�"
clearable
- disabled
+ :disabled="operationType === 'view'"
type="textarea"
/>
</el-form-item>
@@ -118,8 +118,9 @@
</el-row>
<template #footer>
<div class="dialog-footer">
- <el-button type="primary" @click="submitForm">纭</el-button>
- <el-button @click="closeDia">鍙栨秷</el-button>
+ <el-button v-if="operationType === 'approve'" type="primary" @click="submitForm">纭</el-button>
+ <el-button v-if="operationType === 'approve'" @click="closeDia">鍙栨秷</el-button>
+ <el-button v-else type="primary" @click="closeDia">鍏抽棴</el-button>
</div>
</template>
</el-dialog>
@@ -169,8 +170,14 @@
userList.value = res.data;
});
form.value = {...row}
- form.value.disposeUserId = userStore.id;
- form.value.disDate = getCurrentDate();
+ if (type === 'approve') {
+ if (!form.value.disposeUserId) {
+ form.value.disposeUserId = userStore.id;
+ }
+ if (!form.value.disDate) {
+ form.value.disDate = getCurrentDate();
+ }
+ }
}
// const setName = (code) => {
// const index = userList.value.findIndex(item => item.deviceModel === code);
@@ -180,13 +187,16 @@
// }
// }
const submitForm = () => {
+ if (operationType.value === 'view') {
+ closeDia();
+ return;
+ }
proxy.$refs["formRef"].validate(valid => {
- if (valid) {
- afterSalesServiceDispose(form.value).then(response => {
- proxy.$modal.msgSuccess("鏂板鎴愬姛")
- closeDia()
- })
- }
+ if (!valid) return;
+ afterSalesServiceDispose(form.value).then(() => {
+ proxy.$modal.msgSuccess("澶勭悊鎴愬姛")
+ closeDia()
+ })
})
}
// 鍏抽棴寮规
@@ -202,4 +212,4 @@
<style scoped>
-</style>
\ No newline at end of file
+</style>
diff --git a/src/views/customerService/afterSalesHandling/index.vue b/src/views/customerService/afterSalesHandling/index.vue
index c3a19b4..57cc2eb 100644
--- a/src/views/customerService/afterSalesHandling/index.vue
+++ b/src/views/customerService/afterSalesHandling/index.vue
@@ -1,38 +1,94 @@
<template>
<div class="app-container">
- <div class="search_form">
- <div>
- <span class="search_title">鍙嶉鏃ユ湡锛�</span>
- <el-date-picker
- v-model="searchForm.feedbackDate"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- type="date"
- placeholder="璇烽�夋嫨"
- clearable
- @change="handleQuery"
- />
- <span class="search_title ml10">澶勭悊鏃ユ湡锛�</span>
- <el-date-picker
- v-model="searchForm.disDate"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- type="date"
- placeholder="璇烽�夋嫨"
- clearable
- @change="handleQuery"
- />
- <span style = "margin-left: 10px;" class="search_title">澶勭悊鐘舵�侊細</span>
- <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" @change="handleQuery" style="width: 140px" clearable>
- <el-option label="寰呭鐞�" :value="1"></el-option>
- <el-option label="宸插鐞�" :value="2"></el-option>
- </el-select>
- <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
- >鎼滅储</el-button
- >
- <el-button @click="handleOut" style="margin-left: 10px">瀵煎嚭</el-button>
- </div>
- </div>
+ <div class="search-wrapper">
+ <el-form
+ :model="searchForm"
+ class="demo-form-inline"
+ >
+ <el-row :gutter="20">
+ <el-col :span="4">
+ <el-form-item>
+ <el-input
+ v-model="searchForm.afterSalesServiceNo"
+ placeholder="璇疯緭鍏ュ伐鍗曠紪鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-select
+ v-model="searchForm.status"
+ placeholder="璇烽�夋嫨宸ュ崟鐘舵��"
+ clearable
+ >
+ <el-option
+ v-for="dict in workOrderStatusOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-select
+ v-model="searchForm.urgency"
+ placeholder="璇烽�夋嫨绱ф�ョ▼搴�"
+ clearable
+ >
+ <el-option
+ v-for="dict in degreeOfUrgencyOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-select
+ v-model="searchForm.serviceType"
+ placeholder="璇烽�夋嫨鍞悗绫诲瀷"
+ clearable
+ >
+ <el-option
+ v-for="dict in classificationOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-input
+ v-model="searchForm.orderNo"
+ placeholder="璇疯緭鍏ラ攢鍞崟鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+
+
+
+ <!-- 鎸夐挳 -->
+ <el-col :span="4">
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">
+ 鎼滅储
+ </el-button>
+ <el-button @click="handleReset">
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
<div class="table_list">
<PIMTable
rowKey="id"
@@ -55,41 +111,10 @@
:upload-method="handleFileUpload"
:delete-method="handleFileDelete"
/>
- <el-dialog
- v-model="repairDialogVisible"
- title="缁翠慨璁板綍"
- width="700px"
- destroy-on-close
- @close="repairRecordList = []"
- >
- <el-table
- :data="repairRecordList"
- border
- v-loading="repairRecordLoading"
- max-height="400"
- >
- <el-table-column type="index" label="搴忓彿" width="55" align="center" />
- <el-table-column label="缁翠慨鏃ユ湡" prop="maintenanceTime" min-width="120" show-overflow-tooltip>
- <template #default="{ row }">
- {{ row.maintenanceTime || row.repairTime || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="缁翠慨浜�" prop="maintenanceName" min-width="100" show-overflow-tooltip>
- <template #default="{ row }">
- {{ row.maintenanceName || row.repairName || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="缁翠慨缁撴灉" prop="maintenanceResult" min-width="180" show-overflow-tooltip />
- </el-table>
- <template #footer>
- <el-button @click="repairDialogVisible = false">鍏抽棴</el-button>
- </template>
- </el-dialog>
</div>
</template>
<script setup>
-import {Search} from "@element-plus/icons-vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import FormDia from "@/views/customerService/afterSalesHandling/components/formDia.vue";
import FileListDialog from "@/components/Dialog/FileListDialog.vue";
@@ -97,12 +122,9 @@
import request from "@/utils/request";
import { getToken } from "@/utils/auth";
import {
- afterSalesServiceDelete,
afterSalesServiceListPage,
afterSalesServiceFileListPage,
- afterSalesServiceFileAdd,
afterSalesServiceFileDel,
- afterSalesServiceRepairListPage,
} from "@/api/customerService/index.js";
import useUserStore from "@/store/modules/user.js";
const { proxy } = getCurrentInstance();
@@ -115,69 +137,112 @@
},
});
const { searchForm } = toRefs(data);
+/*
+post_sale_waiting_list 鏂板鐨勫敭鍚庡垎绫�
+degree_of_urgency 鏂板鐨勭揣鎬ョ▼搴�
+work_order_status 涓婚〉鐨勫伐鍗曠姸鎬�
+*/
+const { post_sale_waiting_list, degree_of_urgency, work_order_status } = proxy.useDict(
+ "post_sale_waiting_list",
+ "degree_of_urgency",
+ "work_order_status",
+);
+
+const classificationOptions = computed(() => post_sale_waiting_list?.value || []);
+const degreeOfUrgencyOptions = computed(() => degree_of_urgency?.value || []);
+const workOrderStatusOptions = computed(() => work_order_status?.value || []);
const tableColumn = ref([
{
- label: "澶勭悊鐘舵��",
- prop: "status",
- dataType: "tag",
- formatData: (params) => {
- if (params == 1) {
- return "寰呭鐞�";
- } else if (params == 2) {
- return "宸插鐞�";
- } else {
- return null;
- }
- },
- formatType: (params) => {
- if (params == 1) {
- return "danger";
- } else if (params == 2) {
- return "success";
- } else {
- return null;
- }
- },
- },
+ label: "宸ュ崟缂栧彿",
+ prop:"afterSalesServiceNo",
+ width: 150,
+ align: "center"
+ },
+ {
+ label: "閿�鍞崟鍙�",
+ prop:"salesContractNo",
+ width: 150,
+ align: "center"
+ },
+ {
+ label: "澶勭悊鐘舵��",
+ prop: "status",
+ dataType: "tag",
+
+ formatData: (params) => {
+ if (params === 1) {
+ return "寰呭鐞�";
+ } else if (params === 2) {
+ return "宸插鐞�";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params === 1) {
+ return "danger";
+ } else if (params === 2) {
+ return "success";
+ } else {
+ return null;
+ }
+ },
+ align: "center"
+ },
+ {
+ label: "鍙嶉鏃ユ湡",
+ prop: "feedbackDate",
+ width: 150,
+ align: "center"
+ },
+ {
+ label: "鐧昏浜�",
+ prop: "checkNickName",
+ align: "center"
+ },
+ {
+ label: "绱ф�ョ▼搴�",
+ prop: "urgency",
+ // 鏍规嵁degreeOfUrgencyOptions瀛楀吀鍘昏嚜鍔ㄥ尮閰�
+ formatData: (params) => {
+ if (params) {
+ const item = degreeOfUrgencyOptions.value.find(item => item.value === params);
+ return item?.label || params;
+ }
+ return null;
+ },
+ align: "center"
+ },
+ {
+ label: "鍞悗绫诲瀷",
+ prop: "serviceType",
+ // 鏍规嵁classificationOptions瀛楀吀鍘昏嚜鍔ㄥ尮閰�
+ formatData: (params) => {
+ if (params) {
+ const item = classificationOptions.value.find(item => item.value === params);
+ return item?.label || params;
+ }
+ return null;
+ },
+ align: "center"
+ },
{
- label: "鍙嶉鏃ユ湡",
- prop: "feedbackDate",
- width: 150,
- },
- {
- label: "鐧昏浜�",
- prop: "checkNickName",
- },
- {
- label: "瀹㈡埛鍚嶇О",
- prop: "customerName",
- width: 200,
- },
- {
- label: "闂鎻忚堪",
- prop: "proDesc",
- width:300
- },
- {
- label: "鍏宠仈閮ㄩ棬",
- prop: "deptName",
- width: 200,
- },
- {
- label: "澶勭悊浜�",
- prop: "disposeNickName",
- },
- {
- label: "澶勭悊缁撴灉",
- prop: "disRes",
- width: 200,
- },
- {
- label: "澶勭悊鏃ユ湡",
- prop: "disDate",
- width: 150,
- },
+ label: "闂鎻忚堪",
+ prop: "proDesc",
+ width:300,
+ },
+ {
+ label: "澶勭悊缁撴灉",
+ prop: "disRes",
+ width:300,
+ },
+ {
+ label: "鍏宠仈閮ㄩ棬",
+ prop: "deptName",
+ width: 200,
+ align: "center"
+ },
{
dataType: "action",
label: "鎿嶄綔",
@@ -210,14 +275,6 @@
openFilesFormDia(row);
},
},
- // TODO 涓哄啓鎶ュ憡娣诲姞鐨�
- {
- name: "缁翠慨璁板綍",
- type: "text",
- clickFun: (row) => {
- openRepairDialog(row);
- },
- },
],
},
]);
@@ -238,32 +295,15 @@
const fileListRef = ref(null)
const fileListDialogVisible = ref(false)
const currentFileRow = ref(null)
-const repairDialogVisible = ref(false)
-const repairRecordList = ref([])
-const repairRecordLoading = ref(false)
-// 鎵撳紑缁翠慨璁板綍寮规
-const openRepairDialog = async (row) => {
- repairDialogVisible.value = true
- repairRecordLoading.value = true
- repairRecordList.value = []
- try {
- const res = await afterSalesServiceRepairListPage({
- afterSalesServiceId: row.id,
- current: 1,
- size: 100,
- })
- if (res.code === 200 && res.data?.records) {
- repairRecordList.value = res.data.records
- }
- } catch (error) {
- proxy.$modal.msgError("鑾峰彇缁翠慨璁板綍澶辫触")
- } finally {
- repairRecordLoading.value = false
- }
+// 閲嶇疆
+const handleReset = () => {
+ Object.keys(searchForm.value).forEach(key => {
+ searchForm.value[key] = ""
+ })
}
-// 鎵撳紑闄勪欢寮规----- TODO锛氭帴鍙f槸娌℃湁瀵规帴鐨勶紝闇�瑕佹柊澧炴帴鍙o紝涓哄啓鎶ュ憡娣诲姞鐨�
+// 鎵撳紑闄勪欢寮规
const openFilesFormDia = async (row) => {
currentFileRow.value = row
try {
@@ -311,8 +351,9 @@
try {
const formData = new FormData()
formData.append("file", file)
+ formData.append("id", currentFileRow.value.id)
const uploadRes = await request({
- url: "/file/upload",
+ url: "/afterSalesService/file/upload",
method: "post",
data: formData,
headers: {
@@ -321,33 +362,23 @@
},
})
if (uploadRes.code === 200) {
- const fileData = {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛")
+ // 閲嶆柊鑾峰彇鏂囦欢鍒楄〃
+ const listRes = await afterSalesServiceFileListPage({
afterSalesServiceId: currentFileRow.value.id,
- name: uploadRes.data?.originalName || file.name,
- url: uploadRes.data?.tempPath || uploadRes.data?.url,
+ current: 1,
+ size: 100,
+ })
+ if (listRes.code === 200 && fileListRef.value) {
+ const fileList = (listRes.data?.records || []).map((item) => ({
+ name: item.fileName,
+ url: item.fileUrl,
+ id: item.id,
+ ...item,
+ }))
+ fileListRef.value.setList(fileList)
}
- const saveRes = await afterSalesServiceFileAdd(fileData)
- if (saveRes.code === 200) {
- proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛")
- const listRes = await afterSalesServiceFileListPage({
- afterSalesServiceId: currentFileRow.value.id,
- current: 1,
- size: 100,
- })
- if (listRes.code === 200 && fileListRef.value) {
- const fileList = (listRes.data?.records || []).map((item) => ({
- name: item.name || item.fileName,
- url: item.url || item.fileUrl,
- id: item.id,
- ...item,
- }))
- fileListRef.value.setList(fileList)
- }
- resolve({ name: fileData.name, url: fileData.url, id: saveRes.data?.id })
- } else {
- proxy.$modal.msgError(saveRes.msg || "鏂囦欢淇濆瓨澶辫触")
- resolve(null)
- }
+ resolve({ name: file.name, url: "", id: null })
} else {
proxy.$modal.msgError(uploadRes.msg || "鏂囦欢涓婁紶澶辫触")
resolve(null)
@@ -367,31 +398,47 @@
// 鍒犻櫎闄勪欢
const handleFileDelete = async (row) => {
try {
- const res = await afterSalesServiceFileDel([row.id])
- if (res.code === 200) {
- proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
- if (currentFileRow.value && fileListRef.value) {
- const listRes = await afterSalesServiceFileListPage({
- afterSalesServiceId: currentFileRow.value.id,
- current: 1,
- size: 100,
- })
- if (listRes.code === 200) {
- const fileList = (listRes.data?.records || []).map((item) => ({
- name: item.name || item.fileName,
- url: item.url || item.fileUrl,
- id: item.id,
- ...item,
- }))
- fileListRef.value.setList(fileList)
- }
+ // 娣诲姞纭瀵硅瘽妗�
+ const confirmResult = await ElMessageBox.confirm(
+ '纭畾瑕佸垹闄よ繖涓檮浠跺悧锛�',
+ '鍒犻櫎纭',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
}
- } else {
- proxy.$modal.msgError(res.msg || "鍒犻櫎澶辫触")
- return false
+ )
+
+ if (confirmResult === 'confirm') {
+ const res = await afterSalesServiceFileDel(row.id)
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ if (currentFileRow.value && fileListRef.value) {
+ const listRes = await afterSalesServiceFileListPage({
+ afterSalesServiceId: currentFileRow.value.id,
+ current: 1,
+ size: 100,
+ })
+ if (listRes.code === 200) {
+ const fileList = (listRes.data?.records || []).map((item) => ({
+ name: item.fileName,
+ url: item.fileUrl,
+ id: item.id,
+ ...item,
+ }))
+ fileListRef.value.setList(fileList)
+ }
+ }
+ } else {
+ proxy.$modal.msgError(res.msg || "鍒犻櫎澶辫触")
+ return false
+ }
}
} catch (error) {
- proxy.$modal.msgError("鍒犻櫎澶辫触")
+ // 濡傛灉鐢ㄦ埛鍙栨秷鍒犻櫎锛屼笉鏄剧ず閿欒淇℃伅
+ if (error !== 'cancel') {
+ proxy.$modal.msgError("鍒犻櫎澶辫触")
+ }
return false
}
}
@@ -423,35 +470,6 @@
})
};
-const handleDelete = () => {
- let ids = [];
- if (selectedRows.value.length > 0) {
- ids = selectedRows.value.map((item) => item.id);
- } else {
- proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
- return;
- }
- ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
- confirmButtonText: "纭",
- cancelButtonText: "鍙栨秷",
- type: "warning",
- })
- .then(() => {
- tableLoading.value = true;
- afterSalesServiceDelete(ids)
- .then((res) => {
- proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
- getList();
- })
- .finally(() => {
- tableLoading.value = false;
- });
- })
- .catch(() => {
- proxy.$modal.msg("宸插彇娑�");
- });
-};
-
// 瀵煎嚭
const handleOut = () => {
ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
@@ -473,5 +491,10 @@
</script>
<style scoped>
-
+.search-wrapper {
+ background: white;
+ padding: 1rem 1rem 0 1rem;
+ border: 8px;
+ border-radius: 16px;
+}
</style>
\ No newline at end of file
diff --git a/src/views/customerService/expiryAfterSales/components/formDia.vue b/src/views/customerService/expiryAfterSales/components/formDia.vue
index e8293d6..2fe603b 100644
--- a/src/views/customerService/expiryAfterSales/components/formDia.vue
+++ b/src/views/customerService/expiryAfterSales/components/formDia.vue
@@ -161,8 +161,8 @@
import {ref, computed} from "vue";
import useUserStore from "@/store/modules/user.js";
import { getCurrentDate } from "@/utils/index.js";
-// import {userListNoPageByTenantId} from "@/api/system/user.js"; // 鏆傛椂娉ㄩ噴鎺夛紝浣跨敤鍋囨暟鎹�
-// import {expiryAfterSalesAdd, expiryAfterSalesUpdate} from "@/api/customerService/index.js"; // 鏆傛椂娉ㄩ噴鎺夛紝浣跨敤鍋囨暟鎹�
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+import {expiryAfterSalesAdd, expiryAfterSalesUpdate} from "@/api/customerService/index.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
@@ -218,14 +218,10 @@
operationType.value = type;
dialogFormVisible.value = true;
- // 妯℃嫙鑾峰彇鐢ㄦ埛鍒楄〃
- userList.value = [
- { userId: 1, nickName: "寮犱笁" },
- { userId: 2, nickName: "鏉庡洓" },
- { userId: 3, nickName: "鐜嬩簲" },
- { userId: 4, nickName: "璧靛叚" },
- { userId: 5, nickName: "瀛欏叓" }
- ];
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ userListNoPageByTenantId().then(res => {
+ userList.value = res.data;
+ });
if (type === 'add') {
// 鏂板鏃堕噸缃〃鍗�
@@ -256,12 +252,30 @@
const submitForm = () => {
proxy.$refs["formRef"].validate(valid => {
if (valid) {
- // 妯℃嫙鎻愪氦鎿嶄綔
- setTimeout(() => {
- console.log("妯℃嫙鎻愪氦鐨勬暟鎹�:", form.value);
+ const submitData = {
+ id: form.value.id,
+ productName: form.value.productName,
+ batchNumber: form.value.batchNumber,
+ expireDate: form.value.expiryDate,
+ stockQuantity: form.value.stockQuantity,
+ customerName: form.value.customerName,
+ contactPhone: form.value.contactPhone,
+ disRes: form.value.problemDesc,
+ status: form.value.status,
+ disposeUserId: form.value.handlerId,
+ disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName,
+ disposeResult: form.value.handleResult,
+ disDate: form.value.handleDate
+ };
+
+ const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate;
+ apiCall(submitData).then(() => {
proxy.$modal.msgSuccess(operationType.value === 'add' ? "鏂板鎴愬姛" : "鏇存柊鎴愬姛");
closeDia();
- }, 300);
+ }).catch(error => {
+ console.error('鎻愪氦鏁版嵁澶辫触:', error);
+ proxy.$modal.msgError('鎻愪氦鏁版嵁澶辫触锛岃绋嶅悗閲嶈瘯');
+ });
}
});
}
diff --git a/src/views/customerService/expiryAfterSales/index.vue b/src/views/customerService/expiryAfterSales/index.vue
index df44504..ee5395d 100644
--- a/src/views/customerService/expiryAfterSales/index.vue
+++ b/src/views/customerService/expiryAfterSales/index.vue
@@ -72,7 +72,7 @@
import {onMounted, ref} from "vue";
import FormDia from "@/views/customerService/expiryAfterSales/components/formDia.vue";
import {ElMessageBox} from "element-plus";
-// import {expiryAfterSalesDelete, expiryAfterSalesListPage} from "@/api/customerService/index.js"; // 鏆傛椂娉ㄩ噴鎺夛紝浣跨敤鍋囨暟鎹�
+import {expiryAfterSalesDelete, expiryAfterSalesListPage} from "@/api/customerService/index.js";
import useUserStore from "@/store/modules/user.js";
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
@@ -127,7 +127,8 @@
label: "澶勭悊鐘舵��",
prop: "status",
width: "",
- slot: true,
+ dataType: "slot",
+ slot: "status",
},
{
label: "澶勭悊浜�",
@@ -142,7 +143,8 @@
{
label: "鎿嶄綔",
prop: "operation",
- slot: true,
+ dataType: "slot",
+ slot: "operation",
width: "200",
},
],
@@ -190,21 +192,39 @@
// 鑾峰彇鍒楄〃鏁版嵁
const getList = () => {
tableLoading.value = true;
- // 鍙栨秷娉ㄩ噴骞朵娇鐢ㄧ湡瀹濧PI
- // expiryAfterSalesListPage({
- // ...searchForm.value,
- // current: page.value.current,
- // size: page.value.size
- // }).then(res => {
- // tableData.value = res.data.records;
- // page.value.total = res.data.total;
- // tableLoading.value = false;
- // });
+ // 鏋勯�犳煡璇㈠弬鏁帮紝鏄犲皠鍓嶇瀛楁鍒板悗绔瓧娈�
+ const queryParams = {
+ expireDate: searchForm.value.expiryDate,
+ disDate: searchForm.value.handleDate,
+ status: searchForm.value.status,
+ current: page.value.current,
+ size: page.value.size
+ };
- // 鏆傛椂杩斿洖绌烘暟鎹�
- tableData.value = [];
- page.value.total = 0;
- tableLoading.value = false;
+ expiryAfterSalesListPage(queryParams).then(res => {
+ // 鏄犲皠鍚庣杩斿洖鏁版嵁鍒板墠绔〃鏍�
+ tableData.value = res.data.records.map(item => ({
+ id: item.id,
+ productName: item.productName,
+ batchNumber: item.batchNumber,
+ expiryDate: item.expireDate,
+ stockQuantity: item.stockQuantity,
+ customerName: item.customerName,
+ contactPhone: item.contactPhone,
+ problemDesc: item.disRes,
+ status: item.status,
+ handlerId: item.disposeUserId,
+ handlerName: item.disposeNickName,
+ handleResult: item.disposeResult,
+ handleDate: item.disDate
+ }));
+ page.value.total = res.data.total;
+ tableLoading.value = false;
+ }).catch(error => {
+ console.error('鑾峰彇鍒楄〃鏁版嵁澶辫触:', error);
+ tableLoading.value = false;
+ proxy.$modal.msgError('鑾峰彇鏁版嵁澶辫触锛岃绋嶅悗閲嶈瘯');
+ });
};
// 鎵撳紑寮规
@@ -230,18 +250,12 @@
})
.then(() => {
tableLoading.value = true;
- // 鍙栨秷娉ㄩ噴骞朵娇鐢ㄧ湡瀹濧PI
- // expiryAfterSalesDelete(ids).then(() => {
- // proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
- // getList();
- // }).finally(() => {
- // tableLoading.value = false;
- // });
-
- // 鏆傛椂妯℃嫙鍒犻櫎鎴愬姛
- tableLoading.value = false;
- proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
- getList();
+ expiryAfterSalesDelete(ids).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ }).finally(() => {
+ tableLoading.value = false;
+ });
})
.catch(() => {
proxy.$modal.msg("宸插彇娑�");
diff --git a/src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue b/src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue
new file mode 100644
index 0000000..37e21ef
--- /dev/null
+++ b/src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue
@@ -0,0 +1,275 @@
+<template>
+ <el-dialog v-model="visible" title="閫夋嫨浜у搧" width="900px" destroy-on-close :close-on-click-modal="false">
+ <el-form :inline="true" :model="query" class="mb-2">
+ <el-form-item label="浜у搧鍒嗙被">
+ <el-input v-model="query.productCategory" placeholder="杈撳叆浜у搧鍒嗙被" clearable @keyup.enter="onSearch" />
+ </el-form-item>
+
+ <el-form-item label="鍩烘湰鍗曚綅">
+ <el-input v-model="query.unit" placeholder="杈撳叆鍩烘湰鍗曚綅" clearable @keyup.enter="onSearch" />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="columnsDialogVisible = true">鍒楄〃瀛楁</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鍒楄〃 -->
+ <el-table ref="tableRef" v-loading="loading" :data="tableData" height="420" highlight-current-row :row-key="getRowKey"
+ @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column type="index" label="搴忓彿" width="60" />
+ <template v-for="column in visibleColumns" :key="column.prop">
+ <el-table-column :prop="column.prop" :label="column.label" :min-width="column.minWidth" show-overflow-tooltip align="center" />
+ </template>
+ </el-table>
+
+ <div class="mt-3" style="margin-top: 10px;display: flex; justify-content: flex-end;">
+
+
+ <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
+ v-model:page-size="page.pageSize" v-model:current-page="page.pageNum" :page-sizes="[10, 20, 50, 100]"
+ @size-change="onPageChange" @current-change="onPageChange" />
+ </div>
+
+ <template #footer>
+ <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm">
+ 纭畾
+ </el-button>
+ <el-button @click="close()">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+
+ <el-dialog v-model="columnsDialogVisible" title="鑷畾涔夋樉绀哄垪椤�" width="600px">
+ <el-checkbox-group v-model="selectedColumns">
+ <el-checkbox v-for="column in allColumns" :key="column.prop" :label="column.prop" :disabled="column.disabled">
+ {{ column.label }}
+ </el-checkbox>
+ </el-checkbox-group>
+ <template #footer>
+ <el-button @click="resetColumns">鎭㈠榛樿</el-button>
+ <el-button type="primary" @click="saveColumns">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({
+ modelValue: Boolean,
+ single: Boolean, // 鏄惁鍙兘閫夋嫨涓�涓紝榛樿false锛堝彲閫夋嫨澶氫釜锛�
+ products: {
+ type: Array,
+ default: () => []
+ },
+ selectedIds: {
+ type: Array,
+ default: () => []
+ }
+});
+
+const emit = defineEmits(['update:modelValue', 'confirm']);
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: (v) => emit("update:modelValue", v),
+});
+
+const query = reactive({
+ productCategory: "",
+ unit: "",
+});
+
+const page = reactive({
+ pageNum: 1,
+ pageSize: 10,
+});
+
+const loading = ref(false);
+const tableData = ref([]);
+const total = ref(0);
+const multipleSelection = ref([]);
+const selectedRowMap = ref(new Map());
+const tableRef = ref();
+
+const columnsDialogVisible = ref(false);
+
+const allColumns = ref([
+ { prop: 'productCategory', label: '浜у搧鍒嗙被', selected: true, disabled: false },
+ { prop: 'unit', label: '鍩烘湰鍗曚綅', selected: true, disabled: false },
+]);
+
+const selectedColumns = ref(allColumns.value.filter(c => c.selected).map(c => c.prop));
+
+const visibleColumns = computed(() => {
+ return allColumns.value.filter(c => selectedColumns.value.includes(c.prop));
+});
+
+const getRowKey = (row) => {
+ return row?.id ?? row?.productModelId ?? `${row?.productCategory || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}`;
+};
+
+const syncMultipleSelection = () => {
+ multipleSelection.value = Array.from(selectedRowMap.value.values());
+};
+
+const initSelectionFromProps = () => {
+ const selectedIdSet = new Set((props.selectedIds || []).map((id) => String(id)));
+ selectedRowMap.value = new Map();
+ if (!selectedIdSet.size) {
+ syncMultipleSelection();
+ return;
+ }
+ (props.products || []).forEach((row) => {
+ if (selectedIdSet.has(String(row?.id))) {
+ selectedRowMap.value.set(getRowKey(row), row);
+ }
+ });
+ syncMultipleSelection();
+};
+
+const resetColumns = () => {
+ selectedColumns.value = allColumns.value.filter(c => c.selected).map(c => c.prop);
+};
+
+const saveColumns = () => {
+ if (selectedColumns.value.length < 1) {
+ ElMessage.warning("鍒楄〃椤规樉绀轰笉寰楀皯浜�1椤�");
+ return;
+ }
+ columnsDialogVisible.value = false;
+};
+
+function close() {
+ visible.value = false;
+}
+
+const handleSelectionChange = (val) => {
+ const currentPageKeys = new Set(tableData.value.map((item) => getRowKey(item)));
+ currentPageKeys.forEach((key) => selectedRowMap.value.delete(key));
+
+ if (props.single && val.length > 1) {
+ const lastSelected = val[val.length - 1];
+ selectedRowMap.value = new Map();
+ if (lastSelected) {
+ selectedRowMap.value.set(getRowKey(lastSelected), lastSelected);
+ }
+ syncMultipleSelection();
+ nextTick(() => {
+ if (tableRef.value) {
+ tableRef.value.clearSelection();
+ tableRef.value.toggleRowSelection(lastSelected, true);
+ }
+ });
+ } else if (props.single) {
+ selectedRowMap.value = new Map();
+ if (val[0]) {
+ selectedRowMap.value.set(getRowKey(val[0]), val[0]);
+ }
+ syncMultipleSelection();
+ } else {
+ val.forEach((row) => {
+ selectedRowMap.value.set(getRowKey(row), row);
+ });
+ syncMultipleSelection();
+ }
+}
+
+function onSearch() {
+ page.pageNum = 1;
+ loadData();
+}
+
+function onReset() {
+ query.productCategory = "";
+ query.unit = "";
+ page.pageNum = 1;
+ loadData();
+}
+
+function onPageChange() {
+ loadData();
+}
+
+function onConfirm() {
+ if (multipleSelection.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�鏉′骇鍝�");
+ return;
+ }
+ if (props.single && multipleSelection.value.length > 1) {
+ ElMessage.warning("鍙兘閫夋嫨涓�涓骇鍝�");
+ return;
+ }
+ emit("confirm", props.single ? [multipleSelection.value[0]] : multipleSelection.value);
+ close();
+}
+
+async function loadData() {
+ loading.value = true;
+ try {
+ let filtered = props.products || [];
+ if (query.productCategory) {
+ filtered = filtered.filter(item => item.productCategory && item.productCategory.includes(query.productCategory));
+ }
+ if (query.unit) {
+ filtered = filtered.filter(item => item.unit && item.unit.includes(query.unit));
+ }
+
+ total.value = filtered.length;
+ const start = (page.pageNum - 1) * page.pageSize;
+ const end = start + page.pageSize;
+ tableData.value = filtered.slice(start, end);
+
+ nextTick(() => {
+ if (tableRef.value) {
+ tableRef.value.clearSelection();
+ tableData.value.forEach(row => {
+ if (selectedRowMap.value.has(getRowKey(row))) {
+ tableRef.value.toggleRowSelection(row, true);
+ }
+ });
+ }
+ syncMultipleSelection();
+ });
+ } finally {
+ loading.value = false;
+ }
+}
+
+// 鐩戝惉寮圭獥鎵撳紑锛岄噸缃�夋嫨
+watch(() => props.modelValue, (visible) => {
+ if (visible) {
+ initSelectionFromProps();
+ page.pageNum = 1;
+ loadData();
+ }
+});
+
+watch(() => props.products, () => {
+ const latestMap = new Map();
+ const currentKeys = new Set(selectedRowMap.value.keys());
+ (props.products || []).forEach((row) => {
+ const key = getRowKey(row);
+ if (currentKeys.has(key)) {
+ latestMap.set(key, row);
+ }
+ });
+ selectedRowMap.value.forEach((row, key) => {
+ if (!latestMap.has(key)) {
+ latestMap.set(key, row);
+ }
+ });
+ selectedRowMap.value = latestMap;
+ syncMultipleSelection();
+ if (props.modelValue) {
+ loadData();
+ }
+}, { deep: true });
+
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/customerService/feedbackRegistration/components/formDia.vue b/src/views/customerService/feedbackRegistration/components/formDia.vue
index 41c8ac6..71cd167 100644
--- a/src/views/customerService/feedbackRegistration/components/formDia.vue
+++ b/src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -2,70 +2,118 @@
<div>
<el-dialog
v-model="dialogFormVisible"
- title="鍞悗鐧昏"
- width="70%"
+ title="鏂板鍞悗鍗�"
+ width="90%"
@close="closeDia"
>
- <el-form
- :model="form"
- label-width="140px"
- label-position="top"
- :rules="rules"
- ref="formRef"
- >
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="鍙嶉鏃堕棿锛�" prop="feedbackDate">
- <el-date-picker
- style="width: 100%"
- v-model="form.feedbackDate"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- type="date"
- placeholder="璇烽�夋嫨"
- clearable
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鐧昏浜猴細" prop="checkUserId">
- <el-select
- v-model="form.checkUserId"
- placeholder="璇烽�夋嫨"
- clearable
- >
- <el-option
- v-for="item in userList"
- :key="item.userId"
- :label="item.nickName"
- :value="item.userId"
- ></el-option>
- </el-select>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerName">
- <el-input
- v-model="form.customerName"
- placeholder="璇疯緭鍏�"
- clearable
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="闂鎻忚堪锛�" prop="proDesc">
- <el-input
- v-model="form.proDesc"
- placeholder="璇疯緭鍏�"
- clearable
- type="textarea"
- />
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
+ <div>
+ <span class="descriptions">鍩虹璧勬枡</span>
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="4">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerName">
+ <el-select
+ v-model="form.customerName"
+ filterable
+ @change="customerNameChange"
+ >
+ <el-option
+ v-for="item in customerNameOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍞悗绫诲瀷锛�" prop="serviceType">
+ <el-select
+ v-model="form.serviceType"
+ filterable
+ >
+ <el-option
+ v-for="dict in serviceTypeOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍏宠仈閿�鍞崟鍙凤細" prop="salesContractNo">
+ <el-select
+ v-model="form.salesContractNo"
+ @change="associatedSalesOrderNumberChange"
+ filterable
+ >
+ <el-option
+ v-for="item in associatedSalesOrderNumberOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="绱ф�ョ▼搴︼細" prop="urgency">
+ <el-select
+ v-model="form.urgency"
+ filterable
+ >
+ <el-option
+ v-for="dict in urgencyOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="闂鎻忚堪锛�" prop="disRes">
+ <el-input
+ v-model="form.disRes"
+ placeholder="璇疯緭鍏ラ棶棰樻弿杩�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <hr>
+ <div style="padding-top: 20px">
+ <div style="display: flex; justify-content: space-between">
+ <span class="descriptions">鍏宠仈浜у搧</span>
+ <el-button
+ type="primary"
+ style="margin-right: 12px; margin-bottom: 10px"
+ @click="isShowProductSelectDialog = true"
+ >
+ 閫夋嫨浜у搧
+ </el-button>
+ </div>
+ <PIMTable
+ :isShowPagination="false"
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ >
+ <template #shippingStatus="{ row }">
+ <el-tag :type="getShippingStatusType(row)" size="small">
+ {{ getShippingStatusText(row) }}
+ </el-tag>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">纭</el-button>
@@ -73,63 +121,295 @@
</div>
</template>
</el-dialog>
+ <!-- 閫夋嫨浜у搧寮圭獥 -->
+ <ProductSelectDialog
+ v-model="isShowProductSelectDialog"
+ :products="currentSalesOrderProducts"
+ :selected-ids="currentSelectedProductIds"
+ @confirm="handleSelectProducts"
+ />
</div>
</template>
<script setup>
-import {ref} from "vue";
+import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue";
+import ProductSelectDialog from "./ProductSelectDialog.vue";
import useUserStore from "@/store/modules/user.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
-import {afterSalesServiceAdd, afterSalesServiceUpdate} from "@/api/customerService/index.js";
+import {afterSalesServiceAdd, afterSalesServiceUpdate, getAllCustomerList, getSalesLedger } from "@/api/customerService/index.js";
import { getCurrentDate } from "@/utils/index.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
const operationType = ref('')
+const formRef = ref(null)
+const customerNameOptions = ref([])
const userStore = useUserStore();
const data = reactive({
form: {
- feedbackDate: "",
- checkUserId: "",
- customerName: "",
- proDesc: "",
+ topic: "",
+ serviceType: "",
+ urgency: "",
+ salesLedgerId: null,
+ productModelIds: "",
+ customerId: null,
+ salesContractNo: "",
+ disRes: "",
+ customerName: ""
},
rules: {
+ customerName: [{required: true, message: "璇烽�夋嫨瀹㈡埛鍚嶇О", trigger: "change"}],
+ serviceType: [{required: true, message: "璇烽�夋嫨鍞悗绫诲瀷", trigger: "change"}],
+ urgency: [{required: true, message: "璇烽�夋嫨绱ф�ョ▼搴�", trigger: "change"}],
feedbackDate: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
- checkUserId: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
- customerName: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
- proDesc: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
}
})
+
+// 鑷畾涔夋牎楠屽嚱鏁帮細鍒ゆ柇鏄惁闇�瑕佹牎楠屽敭鍚庣紪鍙�
+
const { form, rules } = toRefs(data);
const userList = ref([])
+const formatCurrency = (val) => {
+ if (val === null || val === undefined || val === '') return '-'
+ const num = Number(val)
+ return Number.isFinite(num) ? num.toFixed(2) : '-'
+}
+
+const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
+ "post_sale_waiting_list",
+ "degree_of_urgency"
+);
+
+const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []);
+const urgencyOptions = computed(() => degree_of_urgency?.value || []);
+
+const getProductRowId = (row) => {
+ return row?.id ?? row?.productModelId ?? row?.modelId ?? `${row?.productCategory || row?.productName || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}`
+}
+
+const normalizeProductRow = (row) => {
+ return {
+ ...row,
+ id: getProductRowId(row),
+ productCategory: row?.productCategory ?? row?.productName ?? '',
+ specificationModel: row?.specificationModel ?? row?.model ?? '',
+ unit: row?.unit ?? '',
+ approveStatus: row?.approveStatus ?? null,
+ shippingStatus: row?.shippingStatus ?? '',
+ expressCompany: row?.expressCompany ?? '',
+ expressNumber: row?.expressNumber ?? '',
+ shippingCarNumber: row?.shippingCarNumber ?? '',
+ shippingDate: row?.shippingDate ?? '',
+ quantity: row?.quantity ?? 0,
+ taxRate: row?.taxRate ?? 0,
+ taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0,
+ taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0,
+ taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0,
+ }
+}
+
+const tableColumn = ref([
+ { label: "浜у搧澶х被", prop: "productCategory" },
+ { label: "瑙勬牸鍨嬪彿", prop: "specificationModel" },
+ { label: "鍗曚綅", prop: "unit" },
+ {
+ label: "浜у搧鐘舵��",
+ prop: "approveStatus",
+ width: 100,
+ align: "center",
+ dataType: "tag",
+ formatData: (v) => (v === 1 ? "鍏呰冻" : "涓嶈冻"),
+ formatType: (v) => (v === 1 ? "success" : "danger"),
+ },
+ {
+ label: "鍙戣揣鐘舵��",
+ align: "center",
+ width: 140,
+ dataType: "slot",
+ slot: "shippingStatus",
+ },
+ { label: "蹇�掑叕鍙�", prop: "expressCompany", width: 140 },
+ { label: "蹇�掑崟鍙�", prop: "expressNumber", width: 160 },
+ { label: "鍙戣揣杞︾墝", prop: "shippingCarNumber", minWidth: 100, align: "center" },
+ { label: "鍙戣揣鏃ユ湡", prop: "shippingDate", minWidth: 100, align: "center" },
+ { label: "鏁伴噺", prop: "quantity", width: 100 },
+ { label: "绋庣巼(%)", prop: "taxRate", width: 100 },
+ {
+ label: "鍚◣鍗曚环(鍏�)",
+ prop: "taxInclusiveUnitPrice",
+ width: 160,
+ formatData: formatCurrency,
+ },
+ {
+ label: "鍚◣鎬讳环(鍏�)",
+ prop: "taxInclusiveTotalPrice",
+ width: 160,
+ formatData: formatCurrency,
+ },
+ {
+ label: "涓嶅惈绋庢�讳环(鍏�)",
+ prop: "taxExclusiveTotalPrice",
+ width: 160,
+ formatData: formatCurrency,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "鍒犻櫎",
+ type: "text",
+ clickFun: (row) => {
+ tableData.value = tableData.value.filter(i => getProductRowId(i) !== getProductRowId(row))
+ },
+
+ },
+ ],
+ },
+])
+const tableData = ref([])
+// 閫夋嫨浜у搧寮圭獥
+const isShowProductSelectDialog = ref(false)
+const handleSelectProducts = (rows) => {
+ if (!Array.isArray(rows)) return
+ const existingIds = new Set(tableData.value.map(i => String(getProductRowId(i))))
+ const mapped = rows
+ .map(normalizeProductRow)
+ .filter(r => !existingIds.has(String(getProductRowId(r))))
+ tableData.value = tableData.value.concat(mapped)
+}
+const currentSelectedProductIds = computed(() => {
+ return tableData.value.map(item => getProductRowId(item)).filter(item => item !== undefined && item !== null && item !== '')
+})
+
+const associatedSalesOrderNumberChange = () => {
+ const opt = associatedSalesOrderNumberOptions.value.find(
+ (item) => item.value === form.value.salesContractNo
+ )
+ tableData.value = (opt?.productData || []).map(normalizeProductRow)
+ form.value.salesLedgerId = opt?.id || null
+}
+
+const associatedSalesOrderNumberOptions = ref([])
+
+const currentSalesOrderProducts = computed(() => {
+ const opt = associatedSalesOrderNumberOptions.value.find(
+ (item) => item.value === form.value.salesContractNo
+ )
+ return (opt?.productData || []).map(normalizeProductRow)
+})
+
+const customerNameChange = (val) => {
+ const opt = customerNameOptions.value.find(item => item.value === val);
+ if (opt) {
+ form.value.customerId = opt.id;
+ }
+ getSalesLedger({
+ customerName: form.value.customerName
+ }).then(res => {
+ if(res.code === 200){
+ associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({
+ label: item.salesContractNo,
+ value: item.salesContractNo,
+ productData:item.productData,
+ id: item.id
+ }))
+ }
+ })
+}
+
+const getShippingStatusText = (row) => {
+ if (!row) return '寰呭彂璐�'
+ if (row.shippingDate || row.shippingCarNumber) {
+ return '宸插彂璐�'
+ }
+ const status = row.shippingStatus
+ if (status === null || status === undefined || status === '') {
+ return '寰呭彂璐�'
+ }
+ const map = {
+ '寰呭彂璐�': '寰呭彂璐�',
+ '寰呭鏍�': '寰呭鏍�',
+ '瀹℃牳涓�': '瀹℃牳涓�',
+ '瀹℃牳鎷掔粷': '瀹℃牳鎷掔粷',
+ '瀹℃牳閫氳繃': '瀹℃牳閫氳繃',
+ '宸插彂璐�': '宸插彂璐�'
+ }
+ return map[String(status).trim()] || '寰呭彂璐�'
+}
+
+const getShippingStatusType = (row) => {
+ if (!row) return 'info'
+ if (row.shippingDate || row.shippingCarNumber) {
+ return 'success'
+ }
+ const status = row.shippingStatus
+ if (status === null || status === undefined || status === '') {
+ return 'info'
+ }
+ const map = {
+ '寰呭彂璐�': 'info',
+ '寰呭鏍�': 'warning',
+ '瀹℃牳涓�': 'warning',
+ '瀹℃牳鎷掔粷': 'danger',
+ '瀹℃牳閫氳繃': 'success',
+ '宸插彂璐�': 'success'
+ }
+ return map[String(status).trim()] || 'info'
+}
+
// 鎵撳紑寮规
-const openDialog = (type, row) => {
+const openDialog =async (type, row) => {
+ // 璇锋眰澶氫釜鎺ュ彛锛岃幏鍙栨暟鎹�
+ let res = await getAllCustomerList();
+ if(res.records){
+ customerNameOptions.value = res.records.map(item => ({
+ label: item.customerName,
+ value: item.customerName,
+ id: item.id
+ }));
+ }
+
+
operationType.value = type;
dialogFormVisible.value = true;
form.value = {}
proxy.resetForm("formRef");
form.value.checkUserId = userStore.id;
form.value.feedbackDate = getCurrentDate();
+ // 鏂板鏃舵竻绌哄凡閫夊叧鑱斾骇鍝�
+ if (type === "add") {
+ tableData.value = []
+ }
userListNoPageByTenantId().then((res) => {
userList.value = res.data;
});
if (type === "edit") {
form.value = {...row}
+ if (form.value.customerName) {
+ const res = await getSalesLedger({ customerName: form.value.customerName })
+ if (res?.code === 200) {
+ console.log(res)
+ associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(item => ({
+ label: item.salesContractNo,
+ value: item.salesContractNo,
+ productData: item.productData,
+ id: item.id
+ }))
+ }
+ }
+ console.log(form.value)
}
}
-// const setName = (code) => {
-// const index = userList.value.findIndex(item => item.deviceModel === code);
-// if (index > -1) {
-// console.log(userList)
-// form.value.name = userList.value[index].deviceName;
-// }
-// }
const submitForm = () => {
proxy.$refs["formRef"].validate(valid => {
if (valid) {
+ // 鍖归厤浜у搧鍨嬪彿IDs
+ form.value.productModelIds = tableData.value.map(item => item.id).join(",")
if (operationType.value === "add") {
afterSalesServiceAdd(form.value).then(response => {
proxy.$modal.msgSuccess("鏂板鎴愬姛")
@@ -155,6 +435,25 @@
});
</script>
-<style scoped>
+<style scoped lang="scss">
+.descriptions {
+ margin-bottom: 20px;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: 600;
+ padding-left: 12px;
+ position: relative;
+}
-</style>
\ No newline at end of file
+.descriptions::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 1rem;
+ background-color: #002FA7; /* Element 榛樿绾㈣壊 */
+ border-radius: 2px;
+}
+</style>
diff --git a/src/views/customerService/feedbackRegistration/index.vue b/src/views/customerService/feedbackRegistration/index.vue
index 3d97ef8..3a2d362 100644
--- a/src/views/customerService/feedbackRegistration/index.vue
+++ b/src/views/customerService/feedbackRegistration/index.vue
@@ -1,229 +1,514 @@
<template>
- <div class="app-container">
- <div class="search_form">
- <div>
- <span class="search_title">鍙嶉鏃ユ湡锛�</span>
- <el-date-picker
- v-model="searchForm.feedbackDate"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- type="date"
- placeholder="璇烽�夋嫨"
- clearable
- @change="handleQuery"
- />
- <span style="margin-left: 10px;" class="search_title">澶勭悊鐘舵�侊細</span>
- <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" @change="handleQuery" style="width: 140px" clearable>
- <el-option label="寰呭鐞�" :value="1"></el-option>
- <el-option label="宸插鐞�" :value="2"></el-option>
- </el-select>
- <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
- >鎼滅储</el-button
- >
- </div>
- <div>
- <el-button type="primary" @click="openForm('add')">鏂板</el-button>
- <el-button @click="handleOut">瀵煎嚭</el-button>
- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
- </div>
- </div>
- <div class="table_list">
- <PIMTable
- rowKey="id"
- :column="tableColumn"
- :tableData="tableData"
- :page="page"
- :isSelection="true"
- @selection-change="handleSelectionChange"
- :tableLoading="tableLoading"
- @pagination="pagination"
- ></PIMTable>
- </div>
- <form-dia ref="formDia" @close="handleQuery"></form-dia>
- </div>
+ <div class="app-container">
+ <div class="workorder-stats">
+ <div
+ v-for="(item, index) in statsList"
+ :key="index"
+ class="stat-card"
+ >
+ <div class="stat-icon" :style="{ backgroundColor: item.bgColor }">
+ <el-icon :color="item.color" :size="20">
+ <component :is="item.icon" />
+ </el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-number">{{ item.count }}</div>
+ <div class="stat-label">{{ item.label }}</div>
+ </div>
+ </div>
+ </div>
+ <div class="search-wrapper">
+ <el-form
+ :model="searchForm"
+ class="demo-form-inline"
+ >
+ <el-row :gutter="20">
+ <el-col :span="4">
+ <el-form-item>
+ <el-input
+ v-model="searchForm.afterSalesServiceNo"
+ placeholder="璇疯緭鍏ュ伐鍗曠紪鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-select
+ v-model="searchForm.status"
+ placeholder="璇烽�夋嫨宸ュ崟鐘舵��"
+ clearable
+ >
+ <el-option
+ v-for="dict in workOrderStatusOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-select
+ v-model="searchForm.urgency"
+ placeholder="璇烽�夋嫨绱ф�ョ▼搴�"
+ clearable
+ >
+ <el-option
+ v-for="dict in degreeOfUrgencyOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-select
+ v-model="searchForm.serviceType"
+ placeholder="璇烽�夋嫨鍞悗绫诲瀷"
+ clearable
+ >
+ <el-option
+ v-for="dict in classificationOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-input
+ v-model="searchForm.orderNo"
+ placeholder="璇疯緭鍏ラ攢鍞崟鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+
+
+
+ <!-- 鎸夐挳 -->
+ <el-col :span="4">
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">
+ 鎼滅储
+ </el-button>
+ <el-button @click="handleReset">
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div class="table_header" style="display: flex; justify-content: space-between; align-items: center;">
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板鍞悗鍗�</el-button>
+ </div>
+ <div>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :height="tableHeight"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ </div>
</template>
<script setup>
-import {Search} from "@element-plus/icons-vue";
-import {onMounted, ref, getCurrentInstance, nextTick} from "vue";
+import {onMounted, reactive, ref, toRefs, computed, getCurrentInstance, nextTick} from "vue";
import FormDia from "@/views/customerService/feedbackRegistration/components/formDia.vue";
import {ElMessageBox} from "element-plus";
-import {afterSalesServiceDelete, afterSalesServiceListPage} from "@/api/customerService/index.js";
+import {afterSalesServiceDelete, afterSalesServiceListPage, getSalesLedgerDetail} from "@/api/customerService/index.js";
import useUserStore from "@/store/modules/user.js";
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
+import { Document, FolderOpened, UserFilled } from "@element-plus/icons-vue"
+import { markRaw } from 'vue'
+
+const statsList = ref([
+ {
+ icon: markRaw(Document),
+ count: 0,
+ label: '鍏ㄩ儴宸ュ崟',
+ color: '#4080ff',
+ bgColor: '#eaf2ff'
+ },
+ {
+ icon: markRaw(FolderOpened),
+ count: 0,
+ label: '宸插鐞�',
+ color: '#ff9a2e',
+ bgColor: '#fff5e6'
+ },
+ {
+ icon: markRaw(UserFilled),
+ count: 0,
+ label: '宸插畬鎴�',
+ color: '#00b42a',
+ bgColor: '#e6f7ed'
+ },
+])
const data = reactive({
- searchForm: {
- feedbackDate: "",
- },
+ searchForm : {
+ customerName: "",
+ status: "",
+ urgency: "",
+ serviceType: "",
+ reviewStatus: "",
+ orderNo: "",
+ }
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
- {
- label: "澶勭悊鐘舵��",
- prop: "status",
- dataType: "tag",
- formatData: (params) => {
- if (params == 1) {
- return "寰呭鐞�";
- } else if (params == 2) {
- return "宸插鐞�";
- } else {
- return null;
- }
- },
- formatType: (params) => {
- if (params == 1) {
- return "danger";
- } else if (params == 2) {
- return "success";
- } else {
- return null;
- }
- },
- },
- {
- label: "鍙嶉鏃ユ湡",
- prop: "feedbackDate",
- width: 150,
- },
- {
- label: "鐧昏浜�",
- prop: "checkNickName",
- },
- {
- label: "瀹㈡埛鍚嶇О",
- prop: "customerName",
- width: 200,
- },
- {
- label: "闂鎻忚堪",
- prop: "proDesc",
- width:300
- },
- {
- label: "鍏宠仈閮ㄩ棬",
- prop: "deptName",
- width: 200,
- },
- {
- dataType: "action",
- label: "鎿嶄綔",
- align: "center",
- fixed: 'right',
- operation: [
- {
- name: "缂栬緫",
- type: "text",
- clickFun: (row) => {
- openForm("edit", row);
- },
- disabled: (row) => {
- return row.status !== 1
- }
- },
- ],
- },
+ {
+ label: "宸ュ崟缂栧彿",
+ prop:"afterSalesServiceNo",
+ width: 150,
+ align: "center"
+ },
+ {
+ label: "閿�鍞崟鍙�",
+ prop:"salesContractNo",
+ width: 150,
+ align: "center"
+ },
+ {
+ label: "澶勭悊鐘舵��",
+ prop: "status",
+ dataType: "tag",
+
+ formatData: (params) => {
+ if (params) {
+ let part = String(params)
+ const item = workOrderStatusOptions.value.find(item => item.value === part);
+ return item?.label || params;
+ }
+ return null;
+ },
+ formatType: (params) => {
+ if (params === 1) {
+ return "danger";
+ } else if (params === 2) {
+ return "success";
+ } else {
+ return null;
+ }
+ },
+ align: "center"
+ },
+ {
+ label: "鍙嶉鏃ユ湡",
+ prop: "feedbackDate",
+ width: 150,
+ align: "center"
+ },
+ {
+ label: "鐧昏浜�",
+ prop: "checkNickName",
+ align: "center"
+ },
+ {
+ label: "绱ф�ョ▼搴�",
+ prop: "urgency",
+ // 鏍规嵁degreeOfUrgencyOptions瀛楀吀鍘昏嚜鍔ㄥ尮閰�
+ formatData: (params) => {
+ if (params) {
+ const item = degreeOfUrgencyOptions.value.find(item => item.value === params);
+ return item?.label || params;
+ }
+ return null;
+ },
+ align: "center"
+ },
+ {
+ label: "鍞悗绫诲瀷",
+ prop: "serviceType",
+ // 鏍规嵁classificationOptions瀛楀吀鍘昏嚜鍔ㄥ尮閰�
+ formatData: (params) => {
+ if (params) {
+ const item = classificationOptions.value.find(item => item.value === params);
+ return item?.label || params;
+ }
+ return null;
+ },
+ align: "center"
+ },
+ {
+ label: "闂鎻忚堪",
+ prop: "disRes",
+ width:300,
+ },
+ {
+ label: "鍏宠仈閮ㄩ棬",
+ prop: "deptName",
+ width: 200,
+ align: "center"
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ console.log(row)
+ openForm("edit", row);
+ },
+ disabled: (row) => {
+ return row.status !== 1
+ }
+ },
+ ],
+ align: "center"
+ },
]);
const tableData = ref([]);
const tableLoading = ref(false);
const page = reactive({
- current: 1,
- size: 100,
- total: 0,
+ current: 1,
+ size: 100,
+ total: 0,
});
const selectedRows = ref([]);
+const tableHeight = computed(() => "calc(100% -80px)");
+const handleReset = () => {
+ Object.keys(searchForm.value).forEach(key => {
+ searchForm.value[key] = ""
+ })
+ page.current = 1;
+ getList();
+}
// 琛ㄦ牸閫夋嫨鏁版嵁
const handleSelectionChange = (selection) => {
- selectedRows.value = selection;
+ selectedRows.value = selection;
};
const formDia = ref()
+
+// 瀛楀吀鑾峰彇
+/*
+post_sale_waiting_list 鏂板鐨勫敭鍚庡垎绫�
+degree_of_urgency 鏂板鐨勭揣鎬ョ▼搴�
+work_order_status 涓婚〉鐨勫伐鍗曠姸鎬�
+review_status 棣栭〉鐨勫鏍哥姸鎬�
+*/
+const { post_sale_waiting_list, degree_of_urgency, work_order_status, review_status } = proxy.useDict(
+ "post_sale_waiting_list",
+ "degree_of_urgency",
+ "work_order_status",
+ "review_status"
+);
+
+const classificationOptions = computed(() => post_sale_waiting_list?.value || []);
+const degreeOfUrgencyOptions = computed(() => degree_of_urgency?.value || []);
+const workOrderStatusOptions = computed(() => work_order_status?.value || []);
// 鏌ヨ鍒楄〃
/** 鎼滅储鎸夐挳鎿嶄綔 */
const handleQuery = () => {
- page.current = 1;
- getList();
+ page.current = 1;
+ getList();
};
const pagination = (obj) => {
- page.current = obj.page;
- page.size = obj.limit;
- getList();
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
};
const getList = () => {
- tableLoading.value = true;
- afterSalesServiceListPage({ ...searchForm.value, ...page }).then((res) => {
- tableLoading.value = false;
- tableData.value = res.data.records;
- page.total = res.data.total;
- });
+ tableLoading.value = true;
+ getSalesLedgerDetails()
+ afterSalesServiceListPage({ ...searchForm.value, ...page }).then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ });
};
// 鎵撳紑寮规
const openForm = (type, row) => {
- nextTick(() => {
- formDia.value?.openDialog(type, row)
- })
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
};
-const handleDelete = () => {
- let ids = [];
- if (selectedRows.value.length > 0) {
- // 妫�鏌ユ槸鍚︽湁浠栦汉缁存姢鐨勬暟鎹�
- const unauthorizedData = selectedRows.value.filter(item => item.checkUserId !== userStore.id);
- if (unauthorizedData.length > 0) {
- proxy.$modal.msgWarning("涓嶅彲鍒犻櫎浠栦汉缁存姢鐨勬暟鎹�");
- return;
- }
- ids = selectedRows.value.map((item) => item.id);
- } else {
- proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
- return;
- }
- ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
- confirmButtonText: "纭",
- cancelButtonText: "鍙栨秷",
- type: "warning",
- })
- .then(() => {
- tableLoading.value = true;
- afterSalesServiceDelete(ids)
- .then((res) => {
- proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
- getList();
- })
- .finally(() => {
- tableLoading.value = false;
- });
- })
- .catch(() => {
- proxy.$modal.msg("宸插彇娑�");
- });
+function handleDelete() {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ // 妫�鏌ユ槸鍚︽湁浠栦汉缁存姢鐨勬暟鎹�
+ const unauthorizedData = selectedRows.value.filter(item => item.checkUserId !== userStore.id);
+ if (unauthorizedData.length > 0) {
+ proxy.$modal.msgWarning("涓嶅彲鍒犻櫎浠栦汉缁存姢鐨勬暟鎹�");
+ return;
+ }
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ afterSalesServiceDelete(ids)
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
};
// 瀵煎嚭
const handleOut = () => {
- ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
- confirmButtonText: "纭",
- cancelButtonText: "鍙栨秷",
- type: "warning",
- })
- .then(() => {
- proxy.download("/afterSalesService/export", {}, "鍙嶉鐧昏.xlsx");
- })
- .catch(() => {
- proxy.$modal.msg("宸插彇娑�");
- });
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/afterSalesService/export", {}, "鍙嶉鐧昏.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
};
+ // 鑾峰彇缁熻鏁版嵁骞跺埛鏂伴《閮ㄥ崱鐗�
+ const getSalesLedgerDetails = () => {
+ getSalesLedgerDetail({}).then((res) => {
+ if (res.code === 200) {
+ statsList.value[0].count = res.data.filter((item) => item.status === 3)[0].count;
+ statsList.value[1].count = res.data.filter((item) => item.status === 2)[0].count;
+ statsList.value[2].count = res.data.filter((item) => item.status === 1)[0].count;
+
+ // });
+ }
+ });
+ }
+
+
+
onMounted(() => {
- getList();
+ getList();
+
});
</script>
-<style scoped>
+<style scoped lang="scss">
+.search-wrapper {
+ background: white;
+ padding: 1rem 1rem 0 1rem;
+ border: 8px;
+ border-radius: 16px;
+}
-</style>
\ No newline at end of file
+.expand-btn {
+ width: 100%;
+ padding: 20px; /* 涓婁笅宸﹀彸鍚�20px锛岀偣鍑昏繖涓寖鍥撮兘鑳借Е鍙戜簨浠� */
+ cursor: pointer; /* 榧犳爣鎮诞鏄剧ず鎵嬪瀷锛屾彁鍗囦綋楠� */
+ text-align: center;
+}
+
+.workorder-stats {
+ display: flex;
+ gap: 16px;
+ padding-bottom:1rem;
+ border-radius: 8px;
+}
+
+.stat-card {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 20px;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
+}
+
+.stat-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ border-radius: 8px;
+}
+
+.stat-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.stat-number {
+ font-size: 24px;
+ font-weight: 600;
+ color: #303133;
+ line-height: 1;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #909399;
+ line-height: 1;
+}
+.table_header{
+ padding-bottom: 10px;
+}
+
+.table_list {
+ height: calc(100vh - 380px);
+ min-height: 360px;
+ background: #fff;
+ margin-top: 20px;
+ display: flex;
+ flex-direction: column;
+}
+
+:deep(.table_list .pagination-container) {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ margin-top: auto;
+ padding: 12px 0 0;
+}
+
+:deep(.table_list .el-pagination) {
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+ width: 100%;
+}
+</style>
diff --git a/src/views/equipmentManagement/spareParts/index.vue b/src/views/equipmentManagement/spareParts/index.vue
index 0b0dae2..4a48d28 100644
--- a/src/views/equipmentManagement/spareParts/index.vue
+++ b/src/views/equipmentManagement/spareParts/index.vue
@@ -19,60 +19,21 @@
<el-button type="primary" @click="addCategory" >鏂板</el-button>
</div>
</div>
+
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="renderTableData"
+ :tableLoading="loading"
+ :page="pagination"
+ :isShowPagination="true"
+ @pagination="handleSizeChange"
+ >
+ <template #status="{ row }">
+ <el-tag type="success" size="small">{{ row.status }}</el-tag>
+ </template>
+ </PIMTable>
- <div class="table_list">
- <el-table
- v-loading="loading"
- :data="renderTableData"
- style="width: 100%; margin-top: 10px;"
- border
- row-key="id"
- >
- <el-table-column prop="deviceNameStr" label="璁惧鍚嶇О" width="300"></el-table-column>
- <el-table-column prop="name" label="澶囦欢鍚嶇О" width="200"></el-table-column>
- <el-table-column prop="sparePartsNo" label="澶囦欢缂栧彿" width="200"></el-table-column>
- <el-table-column prop="status" label="鐘舵��" width="100">
- <template #default="{ row }">
- <el-tag type="success" size="small">{{ row.status }}</el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="price" label="浠锋牸" width="140"></el-table-column>
- <el-table-column prop="quantity" label="鏁伴噺" width="140"></el-table-column>
- <el-table-column prop="description" label="鎻忚堪"></el-table-column>
- <el-table-column label="鎿嶄綔" width="150" fixed="right" align="center">
- <template #default="{ row }">
- <el-button
- link
- type="primary"
- @click="() => editCategory(row)"
- :disabled="loading"
- >
- 缂栬緫
- </el-button>
- <el-button
- link
- @click="() => deleteCategory(row.id)"
- style="color: #f56c6c;"
- :disabled="loading"
- >
- 鍒犻櫎
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- <!-- 鍒嗛〉缁勪欢 -->
- <div class="pagination-container">
- <el-pagination
- v-model:current-page="pagination.current"
- v-model:page-size="pagination.size"
- :page-sizes="[10, 20, 50, 100]"
- :total="pagination.total"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- />
- </div>
- </div>
<el-dialog title="鍒嗙被绠$悊" v-model="dialogVisible" width="60%">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="璁惧" prop="deviceLedgerIds">
@@ -165,6 +126,60 @@
size: 10,
total: 0
});
+const columns = ref([
+ {
+ label: "璁惧鍚嶇О",
+ prop: "deviceNameStr",
+ },
+ {
+ label: "澶囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ label: "澶囦欢缂栧彿",
+ prop: "sparePartsNo",
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ slot: "status",
+ dataType: "slot",
+ },
+ {
+ label: "浠锋牸",
+ prop: "price",
+ },
+ {
+ label: "鏁伴噺",
+ prop: "quantity",
+ },
+ {
+ label: "鎻忚堪",
+ prop: "description",
+ },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ width: 150,
+ fixed: 'right',
+ align: "center",
+ dataType: "action",
+ operation: [
+ {
+ name: "缂栬緫",
+ clickFun: (row) => {
+ editCategory(row)
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ clickFun: (row) => {
+ deleteCategory(row.id)
+ },
+ },
+ ],
+ },
+]);
// 琛ㄥ崟鏁版嵁
const form = reactive({
id:'',
@@ -298,6 +313,7 @@
form.status = '';
form.description = '';
form.deviceLedgerIds = [];
+ form.quantity = undefined;
form.price = null;
operationType.value = 'add'
dialogVisible.value = true;
diff --git a/src/views/fileManagement/document/index.vue b/src/views/fileManagement/document/index.vue
index a0d824a..aa182b0 100644
--- a/src/views/fileManagement/document/index.vue
+++ b/src/views/fileManagement/document/index.vue
@@ -107,7 +107,6 @@
current: pagination.currentPage,
size: pagination.pageSize,
total: pagination.total,
- layout: 'total, sizes, prev, pager, next, jumper'
}"
@selection-change="handleSelectionChange"
@pagination="handlePagination"
@@ -1137,9 +1136,9 @@
// 鏋勫缓鏌ヨ鍙傛暟
const query = {
- page: pagination.currentPage,
+ current: pagination.currentPage,
size: pagination.pageSize,
- documentClassificationId:currentId.value
+ documentClassificationId: currentId.value
};
const res = await getDocumentList(query);
@@ -1166,9 +1165,10 @@
};
// 澶勭悊鍒嗛〉鍙樺寲
-const handlePagination = (current, size) => {
- pagination.currentPage = current;
- pagination.pageSize = size;
+const handlePagination = (payload) => {
+ // PIMTable emit: { page, limit }
+ pagination.currentPage = payload?.page || 1;
+ pagination.pageSize = payload?.limit || pagination.pageSize;
loadDocumentList();
};
diff --git a/src/views/financialManagement/revenueManagement/Modal.vue b/src/views/financialManagement/revenueManagement/Modal.vue
index 245cdf2..7dd6865 100644
--- a/src/views/financialManagement/revenueManagement/Modal.vue
+++ b/src/views/financialManagement/revenueManagement/Modal.vue
@@ -26,7 +26,7 @@
placeholder="璇烽�夋嫨"
clearable
>
- <el-option :label="item.label" :value="item.value" v-for="(item,index) in income_types" :key="index" />
+ <el-option :label="item.label" :value="item.value" v-for="(item,index) in income_types.filter(item => item.value != 3)" :key="index" />
</el-select>
</el-form-item>
<el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
diff --git a/src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue b/src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue
new file mode 100644
index 0000000..d8218c1
--- /dev/null
+++ b/src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue
@@ -0,0 +1,226 @@
+<template>
+ <el-dialog v-model="visible" title="鏀舵/閫�娆�" width="90%" append-to-body>
+ <div class="section">
+ <div class="section-title descriptions">鍩虹璧勬枡</div>
+ <el-form :model="form" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鍗曟嵁缂栧彿">
+ <el-input v-model="form.billNo" placeholder="浣跨敤绯荤粺缂栧彿" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹㈡埛">
+ <el-select v-model="form.customerId" placeholder="璇烽�夋嫨">
+ <el-option v-for="c in customerOptions" :key="c.value" :label="c.label" :value="c.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鍒跺崟浜�">
+ <el-select v-model="form.makerId" placeholder="璇烽�夋嫨">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鍒跺崟鏃ユ湡">
+ <el-date-picker v-model="form.makeDate" type="date" value-format="YYYY-MM-DD" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鐢宠閮ㄩ棬">
+ <el-select v-model="form.applyDeptId" placeholder="璇烽�夋嫨">
+ <el-option v-for="d in deptOptions" :key="d.value" :label="d.label" :value="d.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞">
+ <el-input v-model="form.remark" maxlength="100" show-word-limit placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="闄勪欢">
+ <el-upload :action="uploadUrl" :headers="uploadHeaders" name="files" :on-success="onUploadSuccess">
+ <el-button>涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+
+ <div class="section">
+ <div class="toolbar">
+ <div class="section-title descriptions">浠樻鍒楄〃</div>
+ <el-input v-model="form.discountAmount" placeholder="浼樻儬閲戦" style="width:240px" />
+ </div>
+ <el-table :data="form.paymentList" border>
+ <el-table-column label="浠樻璐﹀彿" minWidth="160">
+ <template #default="scope">
+ <el-input v-model="scope.row.accountNo" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浠樻璐﹀彿鍚嶇О" minWidth="180">
+ <template #default="scope">
+ <el-select v-model="scope.row.accountName" placeholder="璇烽�夋嫨">
+ <el-option v-for="a in accountOptions" :key="a.value" :label="a.label" :value="a.label" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="浠樻鏂瑰紡" minWidth="140">
+ <template #default="scope">
+ <el-select v-model="scope.row.payMethod" placeholder="璇烽�夋嫨">
+ <el-option v-for="m in payMethodOptions" :key="m.value" :label="m.label" :value="m.value" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹為檯浠樻閲戦" minWidth="160">
+ <template #default="scope">
+ <el-input v-model="scope.row.amount" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬬画璐�" minWidth="140">
+ <template #default="scope">
+ <el-input v-model="scope.row.fee" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浜ゆ槗鍙�/绁ㄦ嵁鍙�" minWidth="180">
+ <template #default="scope">
+ <el-input v-model="scope.row.txNo" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" minWidth="200">
+ <template #default="scope">
+ <el-input v-model="scope.row.remark" maxlength="30" show-word-limit placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" minWidth="120" fixed="right">
+ <template #default="scope">
+ <el-button link type="primary" @click="addPayment">鏂板涓�琛�</el-button>
+ <el-button link type="danger" @click="removePayment(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="summary">鍚堣</div>
+ </div>
+
+ <div class="section">
+ <div class="section-container">
+ <div class="section-title descriptions">婧愬崟淇℃伅</div>
+ <div class="source-toolbar">
+ <el-button @click="clearSource">娓呯┖</el-button>
+ <el-button @click="selectSource">閫夋嫨婧愬崟</el-button>
+ <el-button type="primary" @click="autoWriteOff">鑷姩鏍搁攢</el-button>
+ </div>
+ </div>
+ <el-table :data="form.sourceList" border>
+ <el-table-column label="鍗曟嵁鏃ユ湡" minWidth="160" prop="billDate" />
+ <el-table-column label="鍗曟嵁绫诲瀷" minWidth="160" prop="billType" />
+ <el-table-column label="鍗曟嵁缂栧彿" minWidth="200" prop="billNo" />
+ <el-table-column label="鍗曟嵁閲戦" minWidth="120" prop="billAmount" />
+ <el-table-column label="宸叉牳閿�閲戦" minWidth="120" prop="wroteAmount" />
+ <el-table-column label="鏈牳閿�閲戦" minWidth="120" prop="unWroteAmount" />
+ <el-table-column label="鏈鏍搁攢閲戦" minWidth="160">
+ <template #default="scope">
+ <el-input v-model="scope.row.thisWriteOffAmount" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="100" fixed="right">
+ <template #default="scope">
+ <el-button link type="danger" @click="removeSource(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="summary">鍚堣</div>
+ </div>
+
+ <template #footer>
+ <el-button type="primary" @click="submit">纭</el-button>
+ <el-button @click="visible=false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { getToken } from '@/utils/auth';
+
+const visible = ref(false);
+const form = ref({
+ billNo: '',
+ customerId: undefined,
+ makerId: undefined,
+ makeDate: '',
+ applyDeptId: undefined,
+ remark: '',
+ discountAmount: '',
+ paymentList: [{ accountNo: '', accountName: '', payMethod: '', amount: '', fee: '', txNo: '', remark: '' }],
+ sourceList: [{ billDate: '', billType: '', billNo: '', billAmount: 0, wroteAmount: 0, unWroteAmount: 0, thisWriteOffAmount: '' }]
+});
+
+const customerOptions = ref([]);
+const userOptions = ref([]);
+const deptOptions = ref([]);
+const accountOptions = ref([]);
+const payMethodOptions = ref([]);
+
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload';
+const uploadHeaders = { Authorization: 'Bearer ' + getToken() };
+
+function addPayment() {
+ form.value.paymentList.push({ accountNo: '', accountName: '', payMethod: '', amount: '', fee: '', txNo: '', remark: '' });
+}
+function removePayment(i) {
+ form.value.paymentList.splice(i, 1);
+}
+function removeSource(i) {
+ form.value.sourceList.splice(i, 1);
+}
+function clearSource() {
+ form.value.sourceList = [];
+}
+function selectSource() {}
+function autoWriteOff() {}
+function onUploadSuccess() {}
+
+function open(payload) {
+ visible.value = true;
+}
+function submit() {
+ visible.value = false;
+ emit('submitted');
+}
+
+defineExpose({ open });
+</script>
+
+<style scoped>
+.section { background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); padding: 16px; margin-bottom: 16px; }
+.section-title { font-weight: 600; margin-bottom: 12px; }
+.descriptions {
+ margin-bottom: 20px;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: 600;
+ padding-left: 12px;
+ position: relative;
+}
+.descriptions::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 1rem;
+ background-color: #002FA7;
+ border-radius: 2px;
+}
+.toolbar { margin-bottom: 10px; display: flex; justify-content: space-between;
+ align-items: center; }
+.source-toolbar { margin-bottom: 10px; display: flex; gap: 8px; }
+.summary { padding: 8px 12px; background: #fff7e6; color: #ad6800; }
+.section-container{display: flex;align-items: center;justify-content: space-between; }
+</style>
diff --git a/src/views/financialManagement/salesRefund/index.vue b/src/views/financialManagement/salesRefund/index.vue
new file mode 100644
index 0000000..b4a792f
--- /dev/null
+++ b/src/views/financialManagement/salesRefund/index.vue
@@ -0,0 +1,134 @@
+<template>
+ <div class="app-container">
+ <!-- 浣跨敤鍏叡鎼滅储缁勪欢 -->
+ <SearchPanel
+ v-model="queryParams"
+ :schema="searchSchema"
+ @search="handleQuery"
+ @reset="resetQuery"
+ />
+
+ <!-- 琛ㄦ牸鍖哄煙 -->
+ <el-card class="table-card">
+ <el-table :data="refundList" v-loading="loading" border>
+ <el-table-column label="閫�璐у崟鍙�" prop="returnManagementNo" align="center" />
+ <el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" align="center" />
+ <el-table-column label="閿�鍞崟鍙�" prop="salesContractNo" align="center" />
+ <el-table-column label="搴旈��娆鹃噾棰�" prop="refundAmount" align="center" />
+ <el-table-column label="宸查��娆鹃噾棰�" prop="refundedAmount" align="center" />
+ <el-table-column label="鏈��娆鹃噾棰�" prop="notRefundedAmount" align="center" />
+ <el-table-column label="鐘舵��" prop="status" align="center">
+ <template #default="scope">
+ <dict-tag :options="dictRef.sales_refund_status.value" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓浜�" prop="createUserName" align="center" />
+ <el-table-column label="鍒涘缓鏃堕棿" prop="createTime" align="center" />
+ <el-table-column label="鎿嶄綔" align="center" width="150">
+ <template #default="scope">
+ <el-button link type="primary" @click="openDetail(scope.row)">璇︽儏</el-button>
+ <el-button link type="primary" @click="openConfirm(scope.row)">纭</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-card>
+<ReceiptandRefundPopupWindow ref="popupRef" @submitted="getList" />
+ </div>
+</template>
+
+<script setup name="SalesRefund">
+import { ref, reactive, onMounted, computed, getCurrentInstance } from 'vue';
+const { proxy } = getCurrentInstance();
+import { listPage, add, update, del } from '@/api/financialManagement/salesRefund';
+import SearchPanel from '@/components/SearchPanel/index.vue';
+import ReceiptandRefundPopupWindow from './components/ReceiptandRefundPopupWindow.vue';
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNum: 1,
+ pageSize: 10,
+ returnManagementNo: undefined,
+ customerName: undefined,
+ salesContractNo: undefined,
+ createUserName: undefined,
+ status: undefined
+});
+
+const dictRef = proxy.useDict('sales_refund_status');
+const salesRefundStatusOptions = computed(() => dictRef.sales_refund_status.value || []);
+
+// 鎼滅储鏍忛厤缃�
+const searchSchema = [
+ { type: 'input', prop: 'returnManagementNo', placeholder: '閫�璐у崟鍙�' },
+ { type: 'input', prop: 'customerName', placeholder: '瀹㈡埛鍚嶇О' },
+ { type: 'input', prop: 'salesContractNo', placeholder: '閿�鍞崟鍙�' },
+ { type: 'input', prop: 'createUserName', placeholder: '鍒涘缓浜哄悕绉�' },
+ { type: 'select', prop: 'status', placeholder: '鐘舵��', options: salesRefundStatusOptions }
+];
+
+const loading = ref(false);
+const total = ref(0);
+const refundList = ref([]);
+const popupRef = ref(null);
+
+/** 鏌ヨ鍒楄〃 */
+function getList() {
+ loading.value = true;
+ const { pageNum, pageSize, ...filters } = queryParams;
+ listPlan({
+ current: pageNum,
+ size: pageSize,
+ ...filters
+ })
+ .then(res => {
+ refundList.value = res?.data?.records || res?.rows || [];
+ total.value = res?.data?.total || res?.total || 0;
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.pageNum = 1;
+ getList();
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ handleQuery();
+}
+
+function openDetail(row) {
+ if (popupRef.value) {
+ popupRef.value.open({ mode: 'detail', row });
+ }
+}
+function openConfirm(row) {
+ if (popupRef.value) {
+ popupRef.value.open({ mode: 'confirm', row });
+ }
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.table-card {
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+</style>
+
+<!-- keep-alive child -->
diff --git a/src/views/index.vue b/src/views/index.vue
index 5c16ed4..4ae5fe4 100644
--- a/src/views/index.vue
+++ b/src/views/index.vue
@@ -132,17 +132,17 @@
<div class="process-card">
<div class="process-card__label">绱鎬绘姇鍏�</div>
- <div class="process-card__value">{{ formatAmount(processAside.totalInput) }}<span class="unit">鍏�</span>
+ <div class="process-card__value">{{ formatAmount(processAside.totalInput) }}
</div>
</div>
<div class="process-card">
<div class="process-card__label">绱鎬绘姤搴�</div>
- <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }}<span class="unit">鍏�</span>
+ <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }}
</div>
</div>
<div class="process-card">
<div class="process-card__label">绱鎬讳骇鍑�</div>
- <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }}<span class="unit">鍏�</span>
+ <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }}
</div>
</div>
</div>
@@ -552,7 +552,7 @@
{
name: '寮�绁�',
type: 'line',
- data: receiptAmount,
+ data: invoiceAmount,
stack: 'Total',
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@@ -581,7 +581,7 @@
{
name: '鍥炴',
type: 'line',
- data: invoiceAmount,
+ data: receiptAmount,
stack: 'Total',
lineStyle: {
width: 0
diff --git a/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue b/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
new file mode 100644
index 0000000..a410be0
--- /dev/null
+++ b/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
@@ -0,0 +1,515 @@
+<template>
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="700px"
+ :close-on-click-modal="false">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120px"
+ class="mt8">
+ <!-- 閮ㄩ棬閫夋嫨 -->
+ <el-form-item label="閮ㄩ棬"
+ prop="sysDeptId">
+ <el-tree-select v-model="form.sysDeptId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ check-strictly
+ style="width: 100%"
+ :disabled="['edit', 'view'].includes(operationType)" />
+ </el-form-item>
+ <!-- 鍦扮偣淇℃伅 -->
+ <!-- <el-form-item label="鍦扮偣鍚嶇О"
+ prop="locationName">
+ <el-input v-model="form.locationName"
+ placeholder="璇疯緭鍏ュ湴鐐瑰悕绉�"
+ :disabled="operationType === 'view'" />
+ </el-form-item> -->
+ <!-- 鎵撳崱鑼冨洿 -->
+ <el-form-item label="鐝"
+ prop="shift">
+ <el-select v-model="form.shift"
+ placeholder="璇烽�夋嫨鐝"
+ :disabled="operationType === 'view'"
+ style="width: 100%">
+ <el-option v-for="item in shifts_list"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎵撳崱鑼冨洿(m)"
+ prop="radius">
+ <el-input-number v-model="form.radius"
+ :min="10"
+ :max="1000"
+ :step="10"
+ placeholder="璇疯緭鍏ユ墦鍗¤寖鍥�"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ <!-- 楂樺痉鍦板浘閫夋嫨 -->
+ <el-form-item label="鎵撳崱浣嶇疆"
+ prop="longitude">
+ <div class="map-container">
+ <div class="map-header"
+ style="margin-bottom: 10px">
+ <!-- <el-button @click="getCurrentLocation">
+ <el-icon>
+ <Position />
+ </el-icon>
+ 褰撳墠浣嶇疆
+ </el-button> -->
+ <!-- <span style="margin-left: 10px; color: #909399;font-size: 12px;">鐐瑰嚮鍦板浘閫夋嫨浣嶇疆</span> -->
+ </div>
+ <div id="map-container"
+ class="map"
+ ref="mapContainer"></div>
+ <div class="coordinates-info mt10">
+ <el-input v-model="form.longitude"
+ readonly
+ placeholder="缁忓害"
+ style="width: 140px; margin-right: 10px" />
+ <el-input v-model="form.latitude"
+ readonly
+ placeholder="绾害"
+ style="width: 140px; margin-right: 10px" />
+ <!-- <el-input v-model="form.locationName"
+ placeholder="鍦扮偣鍚嶇О"
+ style="width: calc(100% - 290px)" /> -->
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="鍦扮偣鍚嶇О"
+ prop="locationName">
+ <el-input v-model="form.locationName"
+ :disabled="operationType === 'view'"
+ placeholder="璇疯緭鍏ュ湴鐐瑰悕绉�" />
+ </el-form-item>
+ <!-- 涓婁笅鐝椂闂� -->
+ <el-form-item label="涓婄彮鏃堕棿"
+ prop="startAt">
+ <el-time-picker v-model="form.startAt"
+ format="HH:mm"
+ value-format="HH:mm"
+ placeholder="璇烽�夋嫨涓婄彮鏃堕棿"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ <el-form-item label="涓嬬彮鏃堕棿"
+ prop="endAt">
+ <el-time-picker v-model="form.endAt"
+ format="HH:mm"
+ value-format="HH:mm"
+ :picker-options="{
+ minTime: form.startAt
+ }"
+ placeholder="璇烽�夋嫨涓嬬彮鏃堕棿"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary"
+ @click="submitForm"
+ v-if="operationType !== 'view'">
+ 纭畾
+ </el-button>
+ </span>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, watch, onMounted, nextTick } from "vue";
+ import { ElMessage } from "element-plus";
+ import { Position } from "@element-plus/icons-vue";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import { addAttendanceRule } from "@/api/personnelManagement/attendanceRules.js";
+ import { useDict } from "@/utils/dict";
+
+ const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false,
+ },
+ operationType: {
+ type: String,
+ default: "add",
+ },
+ row: {
+ type: Object,
+ default: () => ({}),
+ },
+ });
+ // const pickerOptions = ref({ minTime: form.value.startAt });
+
+ const emit = defineEmits(["update:modelValue", "close"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const dialogTitle = computed(() => {
+ if (props.operationType === "add") return "鏂板鐝";
+ if (props.operationType === "edit") return "缂栬緫鐝";
+ return "鏌ョ湅鐝";
+ });
+
+ // 鑾峰彇鐝瀛楀吀鍊�
+ const { shifts_list } = useDict("shifts_list");
+
+ // 琛ㄥ崟鏁版嵁
+ const formRef = ref();
+ const form = reactive({
+ id: "",
+ sysDeptId: "",
+ locationName: "",
+ longitude: "",
+ latitude: "",
+ radius: 100,
+ startAt: "09:00",
+ endAt: "18:00",
+ shift: "",
+ });
+
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ sysDeptId: [{ required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" }],
+ locationName: [
+ { required: true, message: "璇疯緭鍏ュ湴鐐瑰悕绉�", trigger: "blur" },
+ ],
+ longitude: [{ required: true, message: "璇烽�夋嫨鎵撳崱浣嶇疆", trigger: "blur" }],
+ latitude: [{ required: true, message: "璇烽�夋嫨鎵撳崱浣嶇疆", trigger: "blur" }],
+ shift: [{ required: true, message: "璇烽�夋嫨鐝", trigger: "change" }],
+ radius: [{ required: true, message: "璇疯緭鍏ユ墦鍗¤寖鍥�", trigger: "blur" }],
+ startAt: [{ required: true, message: "璇烽�夋嫨涓婄彮鏃堕棿", trigger: "change" }],
+ endAt: [
+ { required: true, message: "璇烽�夋嫨涓嬬彮鏃堕棿", trigger: "change" },
+ {
+ validator: (rule, value, callback) => {
+ if (form.startAt && value) {
+ const startParts = form.startAt.split(":");
+ const endParts = value.split(":");
+ const startTime =
+ parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
+ const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
+ if (endTime <= startTime) {
+ callback(new Error("涓嬬彮鏃堕棿涓嶈兘鏃╀簬涓婄彮鏃堕棿"));
+ } else {
+ callback();
+ }
+ } else {
+ callback();
+ }
+ },
+ trigger: "change",
+ },
+ ],
+ };
+
+ // 閮ㄩ棬閫夐」
+ const deptOptions = ref([]);
+
+ // 鍦板浘鐩稿叧
+ const mapContainer = ref(null);
+ let map = null;
+ let marker = null;
+ let circle = null;
+
+ // 鑾峰彇閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ // 杩囨护绂佺敤鐨勯儴闂�
+ const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+
+ // 鍒濆鍖栧湴鍥�
+ const initMap = () => {
+ nextTick(() => {
+ if (window.AMap && mapContainer.value) {
+ // 鍒濆鍖栧湴鍥�
+ map = new window.AMap.Map(mapContainer.value, {
+ zoom: 16,
+ center: [116.397428, 39.90923], // 榛樿鍖椾含
+ });
+
+ // 娣诲姞鎺т欢
+ window.AMap.plugin(["AMap.ToolBar", "AMap.Scale"], function () {
+ map.addControl(new window.AMap.ToolBar());
+ map.addControl(new window.AMap.Scale());
+ });
+
+ // 娣诲姞鏍囪
+ marker = new window.AMap.Marker({
+ position: [116.397428, 39.90923],
+ draggable: true,
+ cursor: "move",
+ title: "鎷栨嫿瀹氫綅",
+ });
+ map.add(marker);
+
+ // 娣诲姞鍦嗗舰鑼冨洿
+ circle = new window.AMap.Circle({
+ center: [116.397428, 39.90923],
+ radius: form.radius,
+ strokeColor: "#3366FF",
+ strokeOpacity: 0.8,
+ strokeWeight: 2,
+ fillColor: "#3366FF",
+ fillOpacity: 0.2,
+ });
+ map.add(circle);
+
+ // 鐩戝惉鏍囪鎷栨嫿
+ marker.on("dragend", e => {
+ const position = e.lnglat;
+ const lng = position.getLng();
+ const lat = position.getLat();
+ form.longitude = lng;
+ form.latitude = lat;
+ updateCircle(position);
+ });
+
+ // 鐩戝惉鏍囪鎷栨嫿寮�濮�
+ marker.on("dragstart", () => {
+ map.setDefaultCursor("move");
+ });
+
+ // 鐩戝惉鏍囪鎷栨嫿缁撴潫
+ marker.on("dragend", () => {
+ map.setDefaultCursor("default");
+ });
+
+ // 鐩戝惉鍦板浘鐐瑰嚮
+ map.on("click", e => {
+ const position = e.lnglat;
+ const lng = position.getLng();
+ const lat = position.getLat();
+ form.longitude = lng;
+ form.latitude = lat;
+ updateMarker(position);
+ updateCircle(position);
+ });
+
+ // 灏濊瘯鑾峰彇褰撳墠浣嶇疆骞惰缃负鍦板浘涓績
+ if (navigator.geolocation && !form.longitude && !form.latitude) {
+ navigator.geolocation.getCurrentPosition(
+ position => {
+ console.log("鑾峰彇鍒板綋鍓嶄綅缃�:", position);
+ const { longitude, latitude } = position.coords;
+ const currentPosition = [longitude, latitude];
+ map.setCenter(currentPosition);
+ updateMarker(currentPosition);
+ updateCircle(currentPosition);
+ form.longitude = longitude;
+ form.latitude = latitude;
+ },
+ error => {
+ console.log("鑾峰彇浣嶇疆澶辫触锛屼娇鐢ㄩ粯璁や綅缃�");
+ }
+ );
+ } else if (form.longitude && form.latitude) {
+ // 濡傛灉鏈夋暟鎹紝璁剧疆鍒板湴鍥�
+ const position = [form.longitude, form.latitude];
+ map.setCenter(position);
+ updateMarker(position);
+ updateCircle(position);
+ }
+ }
+ });
+ };
+
+ // 鏇存柊鏍囪浣嶇疆
+ const updateMarker = position => {
+ if (marker) {
+ marker.setPosition(position);
+ }
+ };
+
+ // 鏇存柊鍦嗗舰鑼冨洿
+ const updateCircle = position => {
+ if (circle) {
+ circle.setCenter(position);
+ circle.setRadius(form.radius);
+ }
+ };
+
+ // 鑾峰彇褰撳墠浣嶇疆
+ const getCurrentLocation = () => {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ position => {
+ const { longitude, latitude } = position.coords;
+ form.longitude = longitude;
+ form.latitude = latitude;
+ if (map) {
+ map.setCenter([longitude, latitude]);
+ updateMarker([longitude, latitude]);
+ updateCircle([longitude, latitude]);
+ }
+
+ // 閫嗗湴鐞嗙紪鐮佽幏鍙栧湴鍧�
+ if (window.AMap) {
+ // 鍔犺浇Geocoder鎻掍欢
+ window.AMap.plugin("AMap.Geocoder", function () {
+ const geocoder = new window.AMap.Geocoder();
+ geocoder.getAddress([longitude, latitude], (status, result) => {
+ if (status === "complete" && result.regeocode) {
+ form.locationName = result.regeocode.formattedAddress;
+ }
+ });
+ });
+ }
+ },
+ error => {
+ ElMessage.error("鑾峰彇浣嶇疆澶辫触锛岃鎵嬪姩閫夋嫨");
+ }
+ );
+ } else {
+ ElMessage.error("娴忚鍣ㄤ笉鏀寔鍦扮悊瀹氫綅");
+ }
+ };
+
+ // 鐩戝惉鍗婂緞鍙樺寲
+ watch(
+ () => form.radius,
+ newValue => {
+ if (circle) {
+ circle.setRadius(newValue);
+ }
+ }
+ );
+
+ // 鐩戝惉涓婄彮鏃堕棿鍙樺寲锛岃Е鍙戜笅鐝椂闂存牎楠�
+ watch(
+ () => form.startAt,
+ () => {
+ if (formRef.value && form.endAt) {
+ formRef.value.validateField("endAt");
+ }
+ }
+ );
+
+ // 鐩戝惉寮圭獥鏄剧ず
+ watch(
+ () => dialogVisible.value,
+ newValue => {
+ if (newValue) {
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form, {
+ id: "",
+ sysDeptId: "",
+ locationName: "",
+ longitude: "",
+ latitude: "",
+ radius: 100,
+ startAt: "09:00",
+ endAt: "18:00",
+ shift: "",
+ });
+
+ // 濡傛灉鏄紪杈戞垨鏌ョ湅锛屽~鍏呮暟鎹�
+ if (props.operationType !== "add" && props.row.id) {
+ // 澶勭悊鏃堕棿鏍煎紡锛岀‘淇濇槸HH:mm鏍煎紡
+ const rowData = { ...props.row };
+ if (rowData.startAt && rowData.startAt.includes(":")) {
+ rowData.startAt = rowData.startAt.split(":").slice(0, 2).join(":");
+ }
+ if (rowData.endAt && rowData.endAt.includes(":")) {
+ rowData.endAt = rowData.endAt.split(":").slice(0, 2).join(":");
+ }
+ Object.assign(form, rowData);
+ }
+
+ // 鍒濆鍖栧湴鍥�
+ setTimeout(() => {
+ initMap();
+ }, 100);
+ }
+ }
+ );
+
+ // 鎻愪氦琛ㄥ崟
+ const submitForm = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ const submitData = {
+ ...form,
+ // 杞崲鏃堕棿鏍煎紡锛岀‘淇濆彧淇濈暀鏃跺垎閮ㄥ垎
+ startAt: form.startAt
+ ? `${form.startAt.split(":").slice(0, 2).join(":")}`
+ : null,
+ endAt: form.endAt
+ ? `${form.endAt.split(":").slice(0, 2).join(":")}`
+ : null,
+ };
+
+ if (props.operationType === "add") {
+ addAttendanceRule(submitData).then(() => {
+ ElMessage.success("鏂板鎴愬姛");
+ emit("close");
+ });
+ } else if (props.operationType === "edit") {
+ addAttendanceRule(submitData).then(() => {
+ ElMessage.success("鏇存柊鎴愬姛");
+ emit("close");
+ });
+ }
+ }
+ });
+ };
+
+ // 鍒濆鍖�
+ onMounted(() => {
+ fetchDeptOptions();
+ });
+</script>
+
+<style scoped lang="scss">
+ .map-container {
+ width: 100%;
+ }
+
+ .map {
+ width: 100%;
+ height: 400px;
+ border: 1px solid #e4e7ed;
+ }
+
+ .coordinates-info {
+ display: flex;
+ gap: 10px;
+ }
+
+ .coordinates-display {
+ padding: 10px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ }
+
+ .mt10 {
+ margin-top: 10px;
+ }
+
+ .mt8 {
+ margin-top: 8px;
+ }
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue b/src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue
new file mode 100644
index 0000000..58d4b4f
--- /dev/null
+++ b/src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue
@@ -0,0 +1,316 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬鍜屾搷浣滄寜閽� -->
+ <div class="page-header">
+ <div class="title">鐝閰嶇疆</div>
+ <div class="actions">
+ <el-button type="primary"
+ @click="openForm('add')">
+ <el-icon>
+ <Plus />
+ </el-icon>
+ 鏂板鐝
+ </el-button>
+ </div>
+ </div>
+ <!-- 鏌ヨ鏉′欢 -->
+ <!-- <el-form :model="searchForm"
+ :inline="true"
+ class="search-form mb16">
+ <el-form-item label="閮ㄩ棬锛�"
+ prop="countId">
+ <el-tree-select v-model="searchForm.countId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ check-strictly
+ style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="鍦扮偣锛�"
+ prop="locationName">
+ <el-input v-model="searchForm.locationName"
+ placeholder="璇疯緭鍏ュ湴鐐瑰悕绉�"
+ clearable
+ style="width: 200px" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="fetchData">
+ <el-icon>
+ <Search />
+ </el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">
+ <el-icon>
+ <Refresh />
+ </el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form> -->
+ <!-- 鐝鍒楄〃 -->
+ <el-card shadow="never"
+ class="mb16">
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ height="calc(100vh - 18.5em)"
+ style="width: 100%"
+ row-key="id">
+ <el-table-column type="index"
+ label="搴忓彿"
+ width="60"
+ align="center" />
+ <el-table-column label="閮ㄩ棬">
+ <template #default="scope">
+ {{ getDeptNameById(scope.row.sysDeptId) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鐝">
+ <template #default="scope">
+ {{ getShiftNameByValue(scope.row.shift) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="locationName"
+ label="鍦扮偣鍚嶇О" />
+ <el-table-column prop="longitude"
+ label="缁忓害" />
+ <el-table-column prop="latitude"
+ label="绾害" />
+ <el-table-column prop="radius"
+ label="鎵撳崱鑼冨洿(m)" />
+ <el-table-column prop="startAt"
+ label="涓婄彮鏃堕棿">
+ <template #default="scope">
+ {{ scope.row.startAt }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="endAt"
+ label="涓嬬彮鏃堕棿">
+ <template #default="scope">
+ {{ scope.row.endAt }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="180"
+ fixed="right"
+ align="center">
+ <template #default="scope">
+ <el-button type="primary"
+ size="small"
+ link
+ @click="openForm('edit', scope.row)">缂栬緫</el-button>
+ <el-button type="danger"
+ size="small"
+ link
+ @click="handleDelete(scope.row.id)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination :total="page.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ class="mt10" />
+ </el-card>
+ <!-- 鏂板/缂栬緫鐝寮圭獥 -->
+ <rule-form ref="ruleFormRef"
+ v-model="dialogVisible"
+ :operation-type="operationType"
+ :row="currentRow"
+ @close="dialogVisible = false; fetchData()" />
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import {
+ Plus,
+ Edit,
+ Delete,
+ Search,
+ Refresh,
+ ArrowLeft,
+ } from "@element-plus/icons-vue";
+ import Pagination from "@/components/Pagination/index.vue";
+ import RuleForm from "./components/form.vue";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import {
+ getAttendanceRules,
+ deleteAttendanceRule,
+ } from "@/api/personnelManagement/attendanceRules.js";
+ import { useDict } from "@/utils/dict";
+
+ const { proxy } = getCurrentInstance();
+
+ // 琛ㄦ牸鏁版嵁
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+
+ // 鍒嗛〉鍙傛暟
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ // 鏌ヨ琛ㄥ崟
+ const searchForm = reactive({
+ countId: "",
+ locationName: "",
+ });
+
+ // 閮ㄩ棬閫夐」
+ const deptOptions = ref([]);
+ // 鑾峰彇鐝瀛楀吀鍊�
+ const { shifts_list } = useDict("shifts_list");
+
+ // 寮圭獥鎺у埗
+ const dialogVisible = ref(false);
+ const operationType = ref("add");
+ const currentRow = ref({});
+ const ruleFormRef = ref();
+
+ // 鏍煎紡鍖栨椂闂�
+ const formatTime = timestamp => {
+ if (!timestamp) return "";
+ const date = new Date(timestamp);
+ return `${String(date.getHours()).padStart(2, "0")}:${String(
+ date.getMinutes()
+ ).padStart(2, "0")}`;
+ };
+
+ // 鏍规嵁鐝鍊艰幏鍙栫彮娆″悕绉�
+ const getShiftNameByValue = value => {
+ if (!value) return "";
+ const shift = shifts_list.value.find(item => item.value === value);
+ return shift ? shift.label : value;
+ };
+
+ // 鑾峰彇閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ // 杩囨护绂佺敤鐨勯儴闂�
+ const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+
+ // 鏍规嵁閮ㄩ棬ID鏌ユ壘閮ㄩ棬鍚嶇О
+ const getDeptNameById = (deptId, deptList = deptOptions.value) => {
+ for (const dept of deptList) {
+ if (dept.id === deptId) {
+ return dept.label;
+ }
+ if (dept.children && dept.children.length) {
+ const name = getDeptNameById(deptId, dept.children);
+ if (name) {
+ return name;
+ }
+ }
+ }
+ return "";
+ };
+
+ // 鏌ヨ鐝鍒楄〃
+ const fetchData = () => {
+ tableLoading.value = true;
+ getAttendanceRules({ ...page, ...searchForm })
+ .then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鍒嗛〉鍙樻洿
+ const paginationChange = pagination => {
+ page.current = pagination.page;
+ page.size = pagination.limit;
+ fetchData();
+ };
+
+ // 閲嶇疆鎼滅储
+ const resetSearch = () => {
+ searchForm.countId = "";
+ searchForm.locationName = "";
+ fetchData();
+ };
+
+ // 鎵撳紑琛ㄥ崟
+ const openForm = (type, row = {}) => {
+ operationType.value = type;
+ currentRow.value = row;
+ dialogVisible.value = true;
+ };
+
+ // 鍒犻櫎鐝
+ const handleDelete = id => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ繖鏉$彮娆″悧锛�", "鍒犻櫎纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ deleteAttendanceRule([id]).then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ fetchData();
+ });
+ })
+ .catch(() => {
+ // 鍙栨秷鍒犻櫎
+ });
+ };
+
+ // 鍒濆鍖�
+ onMounted(() => {
+ fetchDeptOptions();
+ fetchData();
+ });
+</script>
+
+<style scoped lang="scss">
+ .page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+
+ .title {
+ font-size: 18px;
+ font-weight: 600;
+ }
+
+ .actions {
+ display: flex;
+ gap: 10px;
+ }
+ }
+
+ .mb16 {
+ margin-bottom: 16px;
+ }
+
+ .mt10 {
+ margin-top: 10px;
+ }
+</style>
diff --git a/src/views/personnelManagement/attendanceCheckin/index.vue b/src/views/personnelManagement/attendanceCheckin/index.vue
index bcfdb00..b7b0f92 100644
--- a/src/views/personnelManagement/attendanceCheckin/index.vue
+++ b/src/views/personnelManagement/attendanceCheckin/index.vue
@@ -1,10 +1,12 @@
<template>
<div class="app-container">
<!-- 鍛樺伐鎵撳崱鍖� -->
- <el-card shadow="never" class="mb16">
+ <!-- <el-card shadow="never"
+ class="mb16">
<div class="attendance-header">
<div>
- <div class="title">鎵撳崱绛惧埌</div>
+ <div class="title">鎵撳崱绛惧埌
+ </div>
<div class="sub-title">鏀寔涓�閿墦鍗★紝鑷姩璁板綍涓婁笅鐝椂闂�</div>
</div>
<div class="attendance-actions">
@@ -12,458 +14,496 @@
<div class="label">褰撳墠鏃堕棿</div>
<div class="value">{{ nowTime }}</div>
</div>
- <el-button type="primary" size="large" @click="handleCheckInOut">
+ <el-button type="primary"
+ size="large"
+ @click="handleCheckInOut"
+ :disabled="todayRecord.workEndAt">
{{ checkInOutText }}
</el-button>
</div>
</div>
- <el-descriptions border :column="4" class="mt10">
+ <el-descriptions border
+ :column="4"
+ class="mt10">
<el-descriptions-item label="鍛樺伐濮撳悕">
- {{ currentUser.name }}
+ {{ todayRecord.staffName }}
</el-descriptions-item>
<el-descriptions-item label="宸ュ彿">
- {{ currentUser.no }}
+ {{ todayRecord.staffNo }}
</el-descriptions-item>
<el-descriptions-item label="鎵�灞為儴闂�">
- {{ currentUser.dept }}
+ {{ todayRecord.deptName }}
</el-descriptions-item>
<el-descriptions-item label="浠婃棩鐘舵��">
- <el-tag :type="todayStatusTag" size="small">
+ <el-tag :type="todayStatusTag"
+ size="small">
{{ todayStatusText }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="涓婄彮鏃堕棿">
- {{ todayRecord?.checkInTime || '-' }}
+ {{ todayRecord?.workStartAt || '-' }}
</el-descriptions-item>
<el-descriptions-item label="涓嬬彮鏃堕棿">
- {{ todayRecord?.checkOutTime || '-' }}
+ {{ todayRecord?.workEndAt || '-' }}
</el-descriptions-item>
<el-descriptions-item label="宸ユ椂(灏忔椂)">
{{ todayRecord?.workHours ?? '-' }}
</el-descriptions-item>
<el-descriptions-item label="寮傚父鏍囪">
- <span v-if="todayRecord?.status === 'normal'">-</span>
- <el-tag v-else type="danger" size="small">
- {{ todayRecord?.statusText }}
+ <span v-if="!todayRecord.id || todayRecord?.status === 0">-</span>
+ <el-tag v-else
+ type="danger"
+ size="small">
+ {{ todayRecord?.status ? getStatusText(todayRecord.status) : '-' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
- </el-card>
-
- <!-- 鏌ヨ鏉′欢锛堢鐞嗗憳鑰冨嫟鏃ユ姤锛� -->
- <div class="search_form">
- <div>
- <span class="search_title">閮ㄩ棬锛�</span>
- <el-select
- v-model="searchForm.dept"
- placeholder="璇烽�夋嫨閮ㄩ棬"
- style="width: 180px"
- clearable
- >
- <el-option
- v-for="item in deptOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
-
- <span class="search_title ml10">鏃ユ湡锛�</span>
- <el-date-picker
- v-model="searchForm.date"
- type="date"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- placeholder="璇烽�夋嫨鏃ユ湡"
- clearable
- />
-
- <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
- 鎼滅储
- </el-button>
- <el-button @click="resetSearch">閲嶇疆</el-button>
- </div>
- <div>
- <el-button icon="Download" @click="handleExport">
- 瀵煎嚭鑰冨嫟鏃ユ姤
- </el-button>
- </div>
+ </el-card> -->
+ <div class="attendance-operation">
+ <!-- 鏌ヨ鏉′欢锛堢鐞嗗憳鑰冨嫟鏃ユ姤锛� -->
+ <el-form :model="searchForm"
+ :inline="true"
+ class="search-form">
+ <el-form-item label="閮ㄩ棬锛�"
+ prop="deptId">
+ <el-tree-select v-model="searchForm.deptId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ check-strictly
+ style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="鏃ユ湡锛�"
+ prop="date">
+ <el-date-picker v-model="searchForm.date"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="fetchData">
+ <el-icon>
+ <Search />
+ </el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">
+ <el-icon>
+ <Refresh />
+ </el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ <el-button icon="Download"
+ @click="handleExport">
+ 瀵煎嚭鑰冨嫟鏃ユ姤
+ </el-button>
</div>
-
<!-- 鑰冨嫟鏃ユ姤琛ㄦ牸 -->
<div class="table_list">
- <el-table
- :data="tableData"
- border
- style="width: 100%"
- height="calc(100vh - 24em)"
- :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
- :row-class-name="rowClassName"
- >
- <el-table-column type="index" label="搴忓彿" width="60" align="center" />
- <el-table-column
- prop="date"
- label="鏃ユ湡"
- width="120"
- />
- <el-table-column
- prop="dept"
- label="閮ㄩ棬"
- width="140"
- />
- <el-table-column
- prop="name"
- label="濮撳悕"
- width="120"
- />
- <el-table-column
- prop="no"
- label="宸ュ彿"
- width="120"
- />
- <el-table-column
- prop="checkInTime"
- label="涓婄彮鏃堕棿"
- width="140"
- />
- <el-table-column
- prop="checkOutTime"
- label="涓嬬彮鏃堕棿"
- width="140"
- />
- <el-table-column
- prop="workHours"
- label="宸ユ椂(灏忔椂)"
- width="110"
- align="center"
- />
- <el-table-column
- prop="statusText"
- label="鑰冨嫟鐘舵��"
- width="120"
- align="center"
- >
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ style="width: 100%"
+ height="calc(100vh - 24em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ :row-class-name="rowClassName">
+ <el-table-column type="index"
+ label="搴忓彿"
+ width="60"
+ align="center" />
+ <el-table-column prop="date"
+ label="鏃ユ湡"
+ width="120" />
+ <el-table-column prop="deptName"
+ label="閮ㄩ棬"
+ width="140" />
+ <el-table-column prop="staffName"
+ label="濮撳悕"
+ width="120" />
+ <el-table-column prop="staffNo"
+ label="宸ュ彿"
+ width="120" />
+ <el-table-column prop="workStartAt"
+ label="涓婄彮鏃堕棿"
+ width="140" />
+ <el-table-column prop="workEndAt"
+ label="涓嬬彮鏃堕棿"
+ width="140" />
+ <el-table-column prop="workHours"
+ label="宸ユ椂(灏忔椂)"
+ align="center" />
+ <el-table-column prop="status"
+ label="鑰冨嫟鐘舵��"
+ align="center">
<template #default="scope">
- <el-tag
- v-if="scope.row.status === 'normal'"
- type="success"
- size="small"
- >
+ <el-tag v-if="scope.row.status === 0"
+ type="success"
+ size="small">
姝e父
</el-tag>
- <el-tag
- v-else
- type="danger"
- size="small"
- >
- {{ scope.row.statusText }}
+ <el-tag v-else
+ type="danger"
+ size="small">
+ <!-- {{ scope.row.status === 1 ? '杩熷埌' : scope.row.status === 2 ? '鏃╅��' : '杩熷埌銆佹棭閫�' }} -->
+ {{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
- <el-table-column
- prop="remark"
- label="澶囨敞"
- show-overflow-tooltip
- />
+ <el-table-column prop="remark"
+ label="澶囨敞"
+ show-overflow-tooltip />
</el-table>
+ <pagination :total="page.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
</div>
</div>
</template>
<script setup>
-import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
-import { ElMessage } from "element-plus";
+ import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
+ import { useRouter } from "vue-router";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import {
+ createPersonalAttendanceRecord,
+ findPersonalAttendanceRecords,
+ findTodayPersonalAttendanceRecord,
+ } from "@/api/personnelManagement/personalAttendanceRecords.js";
+ import Pagination from "@/components/Pagination/index.vue";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import { Refresh, Search, ArrowLeft } from "@element-plus/icons-vue";
-// 妯℃嫙褰撳墠鐧诲綍鍛樺伐
-const currentUser = reactive({
- id: 1,
- name: "寮犱笁",
- no: "E10001",
- dept: "鐢熶骇涓�閮�",
-});
-
-// 閮ㄩ棬閫夐」
-const deptOptions = [
- { label: "鐢熶骇涓�閮�", value: "鐢熶骇涓�閮�" },
- { label: "鐢熶骇浜岄儴", value: "鐢熶骇浜岄儴" },
- { label: "璁惧缁存姢閮�", value: "璁惧缁存姢閮�" },
- { label: "璐ㄦ閮�", value: "璐ㄦ閮�" },
-];
-
-// 妯℃嫙鑰冨嫟鍘熷鏁版嵁
-const rawAttendance = ref([
- {
- id: 1,
- date: "2024-12-01",
- userId: 1,
- name: "寮犱笁",
- no: "E10001",
- dept: "鐢熶骇涓�閮�",
- checkInTime: "08:58",
- checkOutTime: "18:10",
- workHours: 9.2,
- status: "normal",
- statusText: "姝e父",
- remark: "",
- },
- {
- id: 2,
- date: "2024-12-01",
- userId: 2,
- name: "鏉庡洓",
- no: "E10002",
- dept: "鐢熶骇涓�閮�",
- checkInTime: "09:15",
- checkOutTime: "18:05",
- workHours: 8.8,
- status: "late",
- statusText: "杩熷埌",
- remark: "鍥犱氦閫氭嫢鍫佃繜鍒�",
- },
- {
- id: 3,
- date: "2024-12-01",
- userId: 3,
- name: "鐜嬩簲",
- no: "E20001",
- dept: "璁惧缁存姢閮�",
- checkInTime: "08:50",
- checkOutTime: "17:20",
- workHours: 8.5,
- status: "early",
- statusText: "鏃╅��",
- remark: "澶栧嚭澶勭悊绱ф�ユ晠闅�",
- },
- {
- id: 4,
- date: "2024-12-02",
- userId: 1,
- name: "寮犱笁",
- no: "E10001",
- dept: "鐢熶骇涓�閮�",
- checkInTime: "08:45",
- checkOutTime: "18:30",
- workHours: 9.7,
- status: "normal",
- statusText: "姝e父",
- remark: "鍔犵彮0.5灏忔椂",
- },
-]);
-
-// 鏌ヨ琛ㄥ崟
-const searchForm = reactive({
- dept: "",
- date: "",
-});
-
-// 琛ㄦ牸鏁版嵁
-const tableData = ref([]);
-
-// 褰撳墠鏃堕棿灞曠ず
-const nowTime = ref("");
-let timer = null;
-
-const updateNowTime = () => {
- const now = new Date();
- const Y = now.getFullYear();
- const M = String(now.getMonth() + 1).padStart(2, "0");
- const D = String(now.getDate()).padStart(2, "0");
- const h = String(now.getHours()).padStart(2, "0");
- const m = String(now.getMinutes()).padStart(2, "0");
- const s = String(now.getSeconds()).padStart(2, "0");
- nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
-};
-
-// 浠婃棩鏃ユ湡
-const todayStr = computed(() => nowTime.value.slice(0, 10));
-
-// 褰撴棩褰撳墠鍛樺伐鑰冨嫟璁板綍
-const todayRecord = computed(() =>
- rawAttendance.value.find(
- (item) =>
- item.userId === currentUser.id && item.date === todayStr.value
- )
-);
-
-// 鎵撳崱鎸夐挳鏂囨湰
-const checkInOutText = computed(() => {
- if (!todayRecord.value || !todayRecord.value.checkInTime) {
- return "涓婄彮鎵撳崱";
- }
- if (!todayRecord.value.checkOutTime) {
- return "涓嬬彮鎵撳崱";
- }
- return "浠婃棩宸叉墦鍗″畬鎴�";
-});
-
-// 浠婃棩鐘舵�佸睍绀�
-const todayStatusTag = computed(() => {
- if (!todayRecord.value) return "info";
- if (todayRecord.value.status === "normal") return "success";
- return "danger";
-});
-
-const todayStatusText = computed(() => {
- if (!todayRecord.value) return "鏈墦鍗�";
- return todayRecord.value.statusText || "姝e父";
-});
-
-// 琛屾牱寮忥細寮傚父楂樹寒
-const rowClassName = ({ row }) => {
- if (row.status === "late" || row.status === "early") {
- return "row-abnormal";
- }
- return "";
-};
-
-// 鏌ヨ
-const recomputeTable = () => {
- const list = rawAttendance.value.filter((item) => {
- if (searchForm.dept && item.dept !== searchForm.dept) {
- return false;
- }
- if (searchForm.date && item.date !== searchForm.date) {
- return false;
- }
- return true;
+ const { proxy } = getCurrentInstance();
+ const router = useRouter();
+ const tableLoading = ref(false);
+ // 鍒嗛〉鍙傛暟
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
});
- tableData.value = list;
-};
+ // 浠婃棩鏁版嵁
+ const todayRecord = ref({});
-const handleQuery = () => {
- recomputeTable();
-};
+ // 閮ㄩ棬閫夐」
+ const deptOptions = ref([]);
-const resetSearch = () => {
- searchForm.dept = "";
- searchForm.date = "";
- recomputeTable();
-};
+ // 鏌ヨ琛ㄥ崟
+ const searchForm = reactive({
+ deptId: "",
+ date: "",
+ });
-// 瀵煎嚭锛堟紨绀猴級
-const handleExport = () => {
- ElMessage.success("褰撳墠涓烘紨绀洪〉闈紝瀵煎嚭鍔熻兘鏈鎺ュ疄闄呮帴鍙�");
-};
+ // 琛ㄦ牸鏁版嵁
+ const tableData = ref([]);
-// 鎵撳崱閫昏緫锛堜粎鍓嶇妯℃嫙锛�
-const handleCheckInOut = () => {
- const [dateStr, timeStr] = nowTime.value.split(" ");
- if (!dateStr || !timeStr) return;
+ // 褰撳墠鏃堕棿灞曠ず
+ const nowTime = ref("");
+ let timer = null;
- // 涓婄彮鎵撳崱
- if (!todayRecord.value) {
- const newId = rawAttendance.value.length
- ? Math.max(...rawAttendance.value.map((i) => i.id)) + 1
- : 1;
- const status =
- timeStr > "09:00:00" ? "late" : "normal";
- const statusText = status === "late" ? "杩熷埌" : "姝e父";
- rawAttendance.value.push({
- id: newId,
- date: dateStr,
- userId: currentUser.id,
- name: currentUser.name,
- no: currentUser.no,
- dept: currentUser.dept,
- checkInTime: timeStr.slice(0, 5),
- checkOutTime: "",
- workHours: null,
- status,
- statusText,
- remark: "",
- });
- ElMessage.success("涓婄彮鎵撳崱鎴愬姛");
- } else if (!todayRecord.value.checkOutTime) {
- // 涓嬬彮鎵撳崱
- todayRecord.value.checkOutTime = timeStr.slice(0, 5);
- // 绠�鍗曟寜 9:00-18:00 璁$畻宸ユ椂
- const start = todayRecord.value.checkInTime || "09:00";
- const [sh, sm] = start.split(":").map((v) => parseInt(v, 10));
- const [eh, em] = todayRecord.value.checkOutTime
- .split(":")
- .map((v) => parseInt(v, 10));
- const diff = (eh * 60 + em - (sh * 60 + sm)) / 60;
- todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1));
+ const updateNowTime = () => {
+ const now = new Date();
+ const Y = now.getFullYear();
+ const M = String(now.getMonth() + 1).padStart(2, "0");
+ const D = String(now.getDate()).padStart(2, "0");
+ const h = String(now.getHours()).padStart(2, "0");
+ const m = String(now.getMinutes()).padStart(2, "0");
+ const s = String(now.getSeconds()).padStart(2, "0");
+ nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
+ };
- // 鏃╅��鍒ゆ柇锛�18:00 鍓嶇寮�瑙嗕负鏃╅��锛堝彧绀烘剰锛�
- if (timeStr < "18:00:00") {
- todayRecord.value.status = "early";
- todayRecord.value.statusText = "鏃╅��";
- } else if (todayRecord.value.status === "normal") {
- todayRecord.value.statusText = "姝e父";
+ // 鎵撳崱鎸夐挳鏂囨湰
+ const checkInOutText = computed(() => {
+ if (!todayRecord.value || !todayRecord.value.workStartAt) {
+ return "涓婄彮鎵撳崱";
}
- ElMessage.success("涓嬬彮鎵撳崱鎴愬姛");
- } else {
- ElMessage.info("浠婃棩宸插畬鎴愪笂涓嬬彮鎵撳崱");
+ if (!todayRecord.value.workEndAt) {
+ return "涓嬬彮鎵撳崱";
+ }
+ return "浠婃棩宸叉墦鍗″畬鎴�";
+ });
+
+ // 浠婃棩鐘舵�佸睍绀�
+ const todayStatusTag = computed(() => {
+ if (!todayRecord.value.id) return "info";
+ if (todayRecord.value.status === 0) return "success";
+ return "danger";
+ });
+ const getStatusText = status => {
+ switch (status) {
+ case 0:
+ return "姝e父";
+ case 1:
+ return "杩熷埌";
+ case 2:
+ return "鏃╅��";
+ case 3:
+ return "杩熷埌銆佹棭閫�";
+ case 4:
+ return "缂哄嫟";
+ }
+ };
+
+ const todayStatusText = computed(() => {
+ if (!todayRecord.value.id) return "鏈墦鍗�";
+ switch (todayRecord.value.status) {
+ case 0:
+ return "姝e父";
+ case 1:
+ return "杩熷埌";
+ case 2:
+ return "鏃╅��";
+ case 3:
+ return "杩熷埌銆佹棭閫�";
+ case 4:
+ return "缂哄嫟";
+ }
+ });
+
+ // 琛屾牱寮忥細寮傚父楂樹寒
+ const rowClassName = ({ row }) => {
+ if (row.status === 1 || row.status === 2) {
+ return "row-abnormal";
+ }
+ return "";
+ };
+
+ // 鏌ヨ閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ /** 杩囨护绂佺敤鐨勯儴闂� */
+ function filterDisabledDept(deptList) {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
}
- recomputeTable();
-};
+ // 鏌ヨ
+ const fetchData = () => {
+ tableLoading.value = true;
+ findPersonalAttendanceRecords({ ...page, ...searchForm })
+ .then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
-onMounted(() => {
- updateNowTime();
- timer = setInterval(updateNowTime, 1000);
- // 榛樿灞曠ず褰撳ぉ鏁版嵁
- const today = new Date();
- const Y = today.getFullYear();
- const M = String(today.getMonth() + 1).padStart(2, "0");
- const D = String(today.getDate()).padStart(2, "0");
- searchForm.date = `${Y}-${M}-${D}`;
- recomputeTable();
-});
+ // 鏌ヨ浠婃棩鎵撳崱淇℃伅
+ const fetchTodayData = () => {
+ // findTodayPersonalAttendanceRecord({}).then(res => {
+ // todayRecord.value = res.data;
+ // });
+ };
-onBeforeUnmount(() => {
- if (timer) {
- clearInterval(timer);
- }
-});
+ const paginationChange = pagination => {
+ page.current = pagination.page;
+ page.size = pagination.limit;
+ fetchData();
+ };
+
+ const resetSearch = () => {
+ searchForm.deptId = "";
+ searchForm.date = "";
+ fetchData();
+ };
+
+ const handleExport = () => {
+ ElMessageBox.confirm("鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/personalAttendanceRecords/export", {}, "鑰冨嫟璁板綍.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鑾峰彇褰撳墠浣嶇疆
+ const getCurrentLocation = () => {
+ return new Promise((resolve, reject) => {
+ if (!navigator.geolocation) {
+ reject(new Error("娴忚鍣ㄤ笉鏀寔鍦扮悊瀹氫綅"));
+ return;
+ }
+
+ // 妫�鏌ユ槸鍚︿娇鐢℉TTPS
+ const isSecureContext =
+ window.isSecureContext || window.location.protocol === "https:";
+ console.log(
+ "褰撳墠鍗忚:",
+ window.location.protocol,
+ "鏄惁瀹夊叏涓婁笅鏂�:",
+ isSecureContext
+ );
+
+ if (!isSecureContext) {
+ console.warn("褰撳墠涓嶆槸HTTPS鍗忚锛屽湴鐞嗕綅缃瓵PI鍙兘鍙楅檺");
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ position => {
+ const { longitude, latitude } = position.coords;
+ console.log("鑾峰彇浣嶇疆鎴愬姛:", longitude, latitude);
+ resolve({ longitude, latitude });
+ },
+ error => {
+ console.log("鑾峰彇浣嶇疆澶辫触:", error);
+ let errorMessage = "鑾峰彇浣嶇疆澶辫触";
+
+ // 鏍规嵁閿欒绫诲瀷鎻愪緵鏇村叿浣撶殑鎻愮ず
+ switch (error.code) {
+ case error.PERMISSION_DENIED:
+ errorMessage =
+ "鐢ㄦ埛鎷掔粷浜嗕綅缃潈闄愯姹傦紝璇峰湪娴忚鍣ㄨ缃腑鍏佽浣嶇疆璁块棶";
+ break;
+ case error.POSITION_UNAVAILABLE:
+ errorMessage = "浣嶇疆淇℃伅涓嶅彲鐢紝璇锋鏌ヨ澶囧畾浣嶅姛鑳�";
+ break;
+ case error.TIMEOUT:
+ errorMessage = "鑾峰彇浣嶇疆瓒呮椂锛岃閲嶈瘯";
+ break;
+ case error.UNKNOWN_ERROR:
+ errorMessage = "鑾峰彇浣嶇疆鏃跺彂鐢熸湭鐭ラ敊璇�";
+ break;
+ default:
+ errorMessage = `鑾峰彇浣嶇疆澶辫触: ${error.message}`;
+ }
+
+ // 妫�鏌ユ槸鍚︽槸HTTPS闂
+ if (error.code === error.PERMISSION_DENIED && !isSecureContext) {
+ errorMessage += "锛堟敞鎰忥細鐢熶骇鐜闇�瑕佷娇鐢℉TTPS鍗忚鎵嶈兘鑾峰彇浣嶇疆锛�";
+ }
+
+ reject(new Error(errorMessage));
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ maximumAge: 0,
+ }
+ );
+ });
+ };
+
+ // 鎵撳崱
+ const handleCheckInOut = () => {
+ getCurrentLocation()
+ .then(location => {
+ console.log("浣嶇疆鎴愬姛");
+ createPersonalAttendanceRecord(location).then(res => {
+ fetchData();
+ fetchTodayData();
+ ElMessage.success("鎵撳崱鎴愬姛锛�");
+ });
+ })
+ .catch(error => {
+ // 鑾峰彇浣嶇疆澶辫触鏃讹紝浠嶅厑璁告墦鍗�
+ ElMessage.warning("鑾峰彇浣嶇疆澶辫触锛屽皢浣跨敤榛樿浣嶇疆鎵撳崱");
+ createPersonalAttendanceRecord({}).then(res => {
+ fetchData();
+ fetchTodayData();
+ ElMessage.success("鎵撳崱鎴愬姛锛�");
+ });
+ });
+ };
+
+ onMounted(() => {
+ updateNowTime();
+ timer = setInterval(updateNowTime, 1000);
+ // 榛樿灞曠ず褰撳ぉ鏁版嵁
+ const today = new Date();
+ const Y = today.getFullYear();
+ const M = String(today.getMonth() + 1).padStart(2, "0");
+ const D = String(today.getDate()).padStart(2, "0");
+ searchForm.date = `${Y}-${M}-${D}`;
+ fetchData();
+ fetchTodayData();
+ fetchDeptOptions();
+ });
+
+ onBeforeUnmount(() => {
+ if (timer) {
+ clearInterval(timer);
+ }
+ });
</script>
<style scoped lang="scss">
-.mb16 {
- margin-bottom: 16px;
-}
+ .mb16 {
+ margin-bottom: 16px;
+ }
-.attendance-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
+ .attendance-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
-.attendance-header .title {
- font-size: 18px;
- font-weight: 600;
- margin-bottom: 4px;
-}
+ .attendance-header .title {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
-.attendance-header .sub-title {
- font-size: 13px;
- color: #909399;
-}
+ .attendance-header .sub-title {
+ font-size: 13px;
+ color: #909399;
+ }
-.attendance-actions {
- display: flex;
- align-items: center;
- gap: 16px;
-}
+ .attendance-actions {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
-.time-block {
- text-align: right;
-}
+ .time-block {
+ text-align: right;
+ }
-.time-block .label {
- font-size: 12px;
- color: #909399;
-}
+ .time-block .label {
+ font-size: 12px;
+ color: #909399;
+ }
-.time-block .value {
- font-size: 18px;
- font-weight: 600;
- color: #333;
-}
+ .time-block .value {
+ font-size: 18px;
+ font-weight: 600;
+ color: #333;
+ }
-::v-deep(.row-abnormal) {
- background-color: #fff5f5;
-}
+ ::v-deep(.row-abnormal) {
+ background-color: #fff5f5;
+ }
+
+ .attendance-operation {
+ display: flex;
+ justify-content: space-between;
+ }
</style>
diff --git a/src/views/personnelManagement/classsSheduling/index.vue b/src/views/personnelManagement/classsSheduling/index.vue
new file mode 100644
index 0000000..99c19bb
--- /dev/null
+++ b/src/views/personnelManagement/classsSheduling/index.vue
@@ -0,0 +1,1283 @@
+<template>
+ <div class="class-page">
+ <div class="search-container">
+ <div class="search-form">
+ <div class="search-row">
+ <div class="search-item">
+ <label class="search-label">閫夋嫨鏃堕棿锛�</label>
+ <div class="search-input-group">
+ <el-date-picker v-model="query.year"
+ type="year"
+ size="small"
+ format="YYYY"
+ placeholder="閫夋嫨骞�"
+ @change="refreshTable()"
+ style="width: 90px"
+ :clearable="false" />
+ <el-select v-model="query.month"
+ clearable
+ placeholder="閫夋嫨鏈�"
+ style="width: 70px; margin-left: 8px"
+ size="small"
+ @change="refreshTable()">
+ <el-option v-for="item in monthOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </div>
+ </div>
+ <div class="search-item">
+ <el-input v-model="query.userName"
+ placeholder="璇疯緭鍏ヤ汉鍛樺悕绉�"
+ size="small"
+ style="width: 120px"
+ clearable
+ @keyup.enter="refreshTable()" />
+ </div>
+ <div class="search-item">
+ <el-tree-select v-model="query.sysDeptId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ size="small"
+ clearable
+ @change="refreshTable()"
+ style="width: 140px" />
+ </div>
+ <div class="search-actions">
+ <el-button size="small"
+ type="primary"
+ @click="refreshTable()"
+ :icon="Search">
+ 鏌ヨ
+ </el-button>
+ <el-button size="small"
+ @click="refresh()"
+ :icon="Refresh"
+ style="margin-left: 8px">
+ 閲嶇疆
+ </el-button>
+ </div>
+ <div class="search-buttons">
+ <el-button size="small"
+ type="primary"
+ @click="configTime"
+ :icon="Setting">
+ 鐝閰嶇疆
+ </el-button>
+ <el-button size="small"
+ type="success"
+ @click="handleDown"
+ :loading="downLoading"
+ :icon="Download"
+ style="margin-left: 8px">
+ 瀵煎嚭
+ </el-button>
+ <el-button size="small"
+ type="warning"
+ @click="schedulingVisible = true"
+ :icon="Calendar"
+ style="margin-left: 8px">
+ 鎺掔彮
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="scheduling-container"
+ v-loading="pageLoading">
+ <!-- 鏈堝害鎺掔彮 -->
+ <div class="scheduling-table"
+ v-show="query.month">
+ <div class="scheduling-left">
+ <div class="scheduling-header">
+ 浜哄憳鍚嶇О
+ </div>
+ <div class="scheduling-user"
+ :class="{ 'scheduling-user-hover': currentUserIndex == index }"
+ v-for="(item, index) in listForm"
+ :key="'e' + index"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="user-avatar">
+ {{ item.name ? item.name.charAt(0) : "" }}
+ </div>
+ <div class="user-details">
+ <h4 class="user-name">{{ item.name }}</h4>
+ <!-- <div class="user-stats">
+ <span class="stat-item">鏃�:{{ item.day0 }}</span>
+ <span class="stat-item">涓�:{{ item.day1 }}</span>
+ <span class="stat-item">澶�:{{ item.day2 }}</span>
+ <span class="stat-item">浼�:{{ item.day3 }}</span>
+ <span class="stat-item">鍋�:{{ item.day4 }}</span>
+ <span class="stat-item">宸�:{{ item.day6 }}</span>
+ </div> -->
+ <div class="user-total">
+ <span class="total-label">鍚堣鍑哄嫟:</span>
+ <span class="total-value">{{ item.monthlyAttendance.totalAttendance }}澶�</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="scheduling-right">
+ <div class="scheduling-calendar">
+ <div class="calendar-header">
+ <div class="calendar-header-item"
+ v-for="(item, index) in weeks"
+ :key="'b' + index">
+ <span class="week-number"
+ v-if="item.week == '鍛ㄦ棩'">{{ item.weekNum }}鍛�</span>
+ <div class="day-info">
+ <span class="day-number">{{ item.day }}</span>
+ <span class="day-week">{{ item.week.charAt(1) }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="calendar-body">
+ <div class="calendar-row"
+ v-for="(item, index) in listForm"
+ :key="'c' + index"
+ :class="{ 'calendar-row-hover': currentUserIndex == index }"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="calendar-cell"
+ v-for="(m, i) in item.list"
+ :key="'d' + i">
+ <el-dropdown trigger="click"
+ placement="bottom"
+ @command="(e) => handleCommand(e, m)"
+ class="shift-dropdown">
+ <div class="shift-box"
+ :class="{
+ 'shift-box-early': m.shift === '鏃╃彮',
+ 'shift-box-mid': m.shift === '涓彮',
+ 'shift-box-night': m.shift === '澶滅彮',
+ 'shift-box-rest': m.shift === '浼戞伅',
+ 'shift-box-leave': m.shift === '璇峰亣',
+ 'shift-box-other': m.shift === '澶�11',
+ 'shift-box-business': m.shift === '澶�12',
+ }">
+ <span class="shift-text">{{ getShiftNameByValue(m.shift) || '鈥�' }}</span>
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item v-for="(n, j) in classType"
+ :key="'h' + j"
+ :command="n.id">{{ n.shift || '鈥�'
+ }}</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- 骞村害鎺掔彮 -->
+ <div class="yearly-table"
+ v-show="!query.month">
+ <div class="scheduling-left">
+ <div class="scheduling-header">
+ 浜哄憳鍚嶇О
+ </div>
+ <div class="scheduling-user"
+ :class="{ 'scheduling-user-hover': currentUserIndex == index }"
+ v-for="(item, index) in yearList"
+ :key="'e' + index"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="user-avatar">
+ {{ item.name ? item.name.charAt(0) : "" }}
+ </div>
+ <div class="user-details">
+ <h4 class="user-name">{{ item.name }}</h4>
+ <!-- <div class="user-stats">
+ <span class="stat-item">鏃�:{{ item.day0 }}</span>
+ <span class="stat-item">涓�:{{ item.day1 }}</span>
+ <span class="stat-item">澶�:{{ item.day2 }}</span>
+ <span class="stat-item">浼�:{{ item.day3 }}</span>
+ <span class="stat-item">鍋�:{{ item.day4 }}</span>
+ <span class="stat-item">宸�:{{ item.day6 }}</span>
+ </div> -->
+ <div class="user-total">
+ <span class="total-label">鍚堣鍑哄嫟:</span>
+ <span class="total-value">{{ item.work_time }}澶�</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="scheduling-right">
+ <div class="yearly-calendar">
+ <div class="yearly-header">
+ <div class="yearly-header-item"
+ v-for="(item, index) in monthList"
+ :key="'b' + index">
+ <span class="month-name">{{ item }}鏈�</span>
+ </div>
+ </div>
+ <div class="yearly-body">
+ <div class="yearly-row"
+ v-for="(item, index) in yearList"
+ :key="'c' + index"
+ :class="{ 'calendar-row-hover': currentUserIndex == index }"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="yearly-cell"
+ v-for="(m, i) in item.monthList"
+ :key="'d' + i">
+ <div class="monthly-attendance">
+ <span class="attendance-label">鍚堣鍑哄嫟锛�</span>
+ <span class="attendance-value">{{ m.totalMonthAttendance }}</span>
+ </div>
+ <!-- <div class="monthly-stats">
+ <span class="stat-item">鏃�:{{ m.day0 }}</span>
+ <span class="stat-item">涓�:{{ m.day1 }}</span>
+ <span class="stat-item">澶�:{{ m.day2 }}</span>
+ <span class="stat-item">浼�:{{ m.day3 }}</span>
+ <span class="stat-item">鍋�:{{ m.day4 }}</span>
+ <span class="stat-item">宸�:{{ m.day6 }}</span>
+ </div> -->
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div style="display: flex; justify-content: flex-end; margin-top: 10px; margin-right: 30px">
+ <el-pagination background
+ @current-change="currentChange"
+ :page-size="pageSize"
+ :current-page="currentPage"
+ layout="total, prev, pager, next, jumper"
+ :total="total">
+ </el-pagination>
+ </div>
+ <el-dialog title="鎺掔彮"
+ v-model="schedulingVisible"
+ width="400px">
+ <div class="search_thing">
+ <div class="search_label"
+ style="width: 90px">
+ <span style="color: red; margin-right: 4px">*</span>鍛ㄦ锛�
+ </div>
+ <div class="search_input">
+ <el-date-picker v-model="schedulingQuery.week"
+ type="week"
+ format="YYYY 绗� ww 鍛�"
+ placeholder="閫夋嫨鍛ㄦ"
+ style="width: 100%">
+ </el-date-picker>
+ </div>
+ </div>
+ <div class="search_thing">
+ <div class="search_label"
+ style="width: 90px">
+ <span style="color: red; margin-right: 4px">*</span>浜哄憳鍚嶇О锛�
+ </div>
+ <div class="search_input">
+ <el-select v-model="schedulingQuery.userId"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ multiple
+ clearable
+ collapse-tags>
+ <el-option v-for="item in personList"
+ :key="item.id"
+ :label="item.staffName"
+ :value="item.id">
+ </el-option>
+ </el-select>
+ </div>
+ </div>
+ <div class="search_thing">
+ <div class="search_label"
+ style="width: 90px">
+ <span style="color: red; margin-right: 4px">*</span>鐝锛�
+ </div>
+ <div class="search_input">
+ <el-select v-model="schedulingQuery.shift"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%">
+ <el-option v-for="item in classType"
+ :key="item.id"
+ :label="getShiftNameByValue(item.shift)"
+ :value="item.id">
+ </el-option>
+ </el-select>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="schedulingVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary"
+ @click="confirmScheduling"
+ :loading="loading">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { useRouter } from "vue-router";
+ import {
+ page,
+ pageYear,
+ add,
+ exportFile,
+ update,
+ staffOnJobListPage,
+ } from "@/api/personnelManagement/class";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js";
+ import { useDict } from "@/utils/dict";
+ const { proxy } = getCurrentInstance();
+ const router = useRouter();
+
+ // 鏌ヨ鏉′欢
+ const query = reactive({
+ userName: "",
+ sysDeptId: "",
+ year: new Date(),
+ month: new Date().getMonth() + 1,
+ });
+ // 鑾峰彇鐝瀛楀吀鍊�
+ const { shifts_list } = useDict("shifts_list");
+ // 鏈堜唤閫夐」
+ const monthOptions = [
+ { value: 1, label: "1鏈�" },
+ { value: 2, label: "2鏈�" },
+ { value: 3, label: "3鏈�" },
+ { value: 4, label: "4鏈�" },
+ { value: 5, label: "5鏈�" },
+ { value: 6, label: "6鏈�" },
+ { value: 7, label: "7鏈�" },
+ { value: 8, label: "8鏈�" },
+ { value: 9, label: "9鏈�" },
+ { value: 10, label: "10鏈�" },
+ { value: 11, label: "11鏈�" },
+ { value: 12, label: "12鏈�" },
+ ];
+
+ // 閮ㄩ棬鍒楄〃
+ const deptOptions = ref([]);
+
+ // 鍛ㄥ垪琛�
+ const weeks = ref([]);
+
+ // 鐝绫诲瀷
+ const classType = ref([]);
+
+ // 褰撳墠鐢ㄦ埛绱㈠紩
+ const currentUserIndex = ref(null);
+
+ // 鎺掔彮寮圭獥鏄剧ず鐘舵��
+ const schedulingVisible = ref(false);
+
+ // 浜哄憳鍒楄〃
+ const personList = ref([]);
+
+ // 鍔犺浇鐘舵��
+ const loading = ref(false);
+
+ // 鎺掔彮鏌ヨ鏉′欢
+ const schedulingQuery = reactive({
+ week: "",
+ userId: null,
+ shift: "",
+ });
+
+ // 鍒楄〃鏁版嵁
+ const listForm = ref([]);
+
+ // 褰撳墠椤�
+ const currentPage = ref(1);
+
+ // 姣忛〉鏉℃暟
+ const pageSize = ref(6);
+
+ // 鎬绘潯鏁�
+ const total = ref(3);
+
+ // 椤甸潰鍔犺浇鐘舵��
+ const pageLoading = ref(false);
+
+ // 鏈堜唤鍒楄〃
+ const monthList = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
+
+ // 骞村害鍒楄〃
+ const yearList = ref([]);
+
+ // 瀵煎嚭鍔犺浇鐘舵��
+ const downLoading = ref(false);
+
+ // 鑾峰彇閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ // 鏍规嵁鐝鍊艰幏鍙栫彮娆″悕绉�
+ const getShiftNameByValue = value => {
+ if (!value) return "";
+ const shift = shifts_list.value.find(item => item.value === value);
+ return shift ? shift.label : value;
+ };
+
+ // 杩囨护绂佺敤鐨勯儴闂�
+ const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+
+ // 鍒锋柊
+ const refresh = () => {
+ listForm.value = [];
+ yearList.value = [];
+ currentPage.value = 1;
+ query.userName = "";
+ query.sysDeptId = "";
+ query.year = new Date();
+ query.month = new Date().getMonth() + 1;
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ };
+
+ // 鍒锋柊琛ㄦ牸
+ const refreshTable = () => {
+ currentPage.value = 1;
+ if (query.month) {
+ listForm.value = [];
+ init();
+ } else {
+ yearList.value = [];
+ initYear();
+ }
+ };
+
+ // 椤电爜鏀瑰彉
+ const currentChange = num => {
+ currentPage.value = num;
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ };
+
+ // 鏁板瓧杞腑鏂�
+ const transFromNumber = num => {
+ let changeNum = ["闆�", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�"];
+ let unit = ["", "鍗�", "鐧�", "鍗�", "涓�"];
+ num = parseInt(num);
+ let getWan = temp => {
+ let strArr = temp.toString().split("").reverse();
+ let newNum = "";
+ for (var i = 0; i < strArr.length; i++) {
+ newNum =
+ (i == 0 && strArr[i] == 0
+ ? ""
+ : i > 0 && strArr[i] == 0 && strArr[i - 1] == 0
+ ? ""
+ : changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i])) +
+ newNum;
+ }
+ return newNum;
+ };
+ let overWan = Math.floor(num / 10000);
+ let noWan = num % 10000;
+ if (noWan.toString().length < 4) noWan = "0" + noWan;
+ return overWan ? getWan(overWan) + "涓�" + getWan(noWan) : getWan(num);
+ };
+
+ // 鍒濆鍖栨湀搴︽暟鎹�
+ const init = () => {
+ pageLoading.value = true;
+ console.log(query.year, "query.year");
+ let year = query.year.getFullYear();
+ let month0 = query.month ? query.month : new Date().getMonth() + 1;
+ let month = month0 > 9 ? month0 : "0" + month0;
+ page({
+ size: pageSize.value,
+ current: currentPage.value,
+ time: year + "-" + month + "-01 00:00:00",
+ userName: query.userName,
+ sysDeptId: query.sysDeptId,
+ })
+ .then(res => {
+ pageLoading.value = false;
+ total.value = res.data.page.total;
+ listForm.value = res.data.page.records.map(item => {
+ for (let key in item.monthlyAttendance) {
+ let type = getDayByDic(key);
+ if (type != undefined || type != null) {
+ item[`day${type}`] = item.monthlyAttendance[key];
+ }
+ }
+ return item;
+ });
+ let headerList = res.data.headerList;
+ weeks.value = [];
+ headerList.forEach(item => {
+ let obj = {
+ weekNum: item.weekly,
+ week: item.headerTime.split(" ")[1],
+ day: item.headerTime.split(" ")[0],
+ };
+ weeks.value.push(obj);
+ });
+ })
+ .catch(() => {
+ pageLoading.value = false;
+ });
+ };
+
+ // 鍒濆鍖栧勾搴︽暟鎹�
+ const initYear = () => {
+ pageLoading.value = true;
+ let year = query.year.getFullYear();
+ pageYear({
+ size: pageSize.value,
+ current: currentPage.value,
+ time: year + "-01-01 00:00:00",
+ userName: query.userName,
+ sysDeptId: query.sysDeptId,
+ }).then(res => {
+ pageLoading.value = false;
+ total.value = res.data.total;
+ yearList.value = res.data.records.map(item => {
+ for (let key in item.year) {
+ let type = getDayByDic(key);
+ if (type != undefined || type != null) {
+ item[`day${type}`] = item.year[key];
+ }
+ }
+ item.monthList = [];
+ for (let m in item.month) {
+ let obj = {};
+ for (let key in item.month[m]) {
+ let type = getDayByDic(key);
+ if (type != undefined || type != null) {
+ obj[`day${type}`] = item.month[m][key];
+ }
+ }
+ obj.totalMonthAttendance = item.month[m].totalMonthAttendance;
+ item.monthList.push(obj);
+ }
+ return item;
+ });
+ });
+ };
+
+ // 榧犳爣杩涘叆
+ const onMouseEnter = index => {
+ currentUserIndex.value = index;
+ };
+
+ // 纭鎺掔彮
+ const confirmScheduling = () => {
+ if (!schedulingQuery.week) {
+ proxy.$modal.msgError("璇烽�夋嫨鍛ㄦ");
+ return;
+ }
+ let time = schedulingQuery.week.getTime();
+
+ // 鏍煎紡鍖栨棩鏈熶负 YYYY-MM-DD 鏍煎紡
+ const formatDate = date => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ };
+
+ let startWeek =
+ formatDate(new Date(time - 24 * 60 * 60 * 1000)) + " 00:00:00";
+ let endWeek =
+ formatDate(new Date(time + 24 * 60 * 60 * 1000 * 5)) + " 00:00:00";
+
+ if (!schedulingQuery.userId || schedulingQuery.userId.length == 0) {
+ proxy.$modal.msgError("璇烽�夋嫨浜哄憳");
+ return;
+ }
+ if (!schedulingQuery.shift) {
+ proxy.$modal.msgError("璇烽�夋嫨鐝");
+ return;
+ }
+ loading.value = true;
+ add({
+ startWeek,
+ endWeek,
+ staffOnJobId: schedulingQuery.userId.join(","),
+ personalAttendanceLocationConfigId: schedulingQuery.shift,
+ })
+ .then(res => {
+ loading.value = false;
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ schedulingVisible.value = false;
+ schedulingQuery.week = "";
+ schedulingQuery.userId = null;
+ schedulingQuery.shift = "";
+ refresh();
+ })
+ .catch(err => {
+ loading.value = false;
+ });
+ };
+
+ // 鏃堕棿閰嶇疆
+ const configTime = () => {
+ // 璺宠浆鍒拌�冨嫟鎵撳崱椤甸潰
+ router.push({
+ path: "/personnelManagement/checkinRules",
+ });
+ };
+
+ // 鍒ゆ柇鏄惁涓虹┖瀵硅薄
+ const isObjectEmpty = obj => {
+ return Object.keys(obj).some(key => !obj[key]);
+ };
+
+ // 瀵煎嚭
+ const handleDown = () => {
+ let year = query.year.getFullYear();
+ let time = "";
+ if (query.month) {
+ let month = query.month > 9 ? query.month : "0" + query.month;
+ time = year + "-" + month + "-01 00:00:00";
+ } else {
+ time = year + "-01-01 00:00:00";
+ }
+ downLoading.value = true;
+ exportFile({
+ time,
+ userName: query.userName,
+ sysDeptId: query.sysDeptId,
+ isMonth: query.month ? true : false,
+ })
+ .then(res => {
+ proxy.$modal.msgSuccess("涓嬭浇鎴愬姛");
+ downLoading.value = false;
+ const blob = new Blob([res], {
+ type: "application/force-download",
+ });
+ let fileName = "";
+ if (query.month) {
+ fileName = year + "-" + query.month + " 鐝淇℃伅";
+ } else {
+ fileName = year + " 鐝姹囨��";
+ }
+ proxy.$download.saveAs(blob, fileName + ".xlsx");
+ })
+ .catch(err => {
+ downLoading.value = false;
+ });
+ };
+ // 澶勭悊鍛戒护
+ const handleCommand = (e, m) => {
+ // if (e != m.shift) {
+ update({
+ id: m.id,
+ personalAttendanceLocationConfigId: e,
+ }).then(res => {
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ // m.shift = e;
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ });
+ // }
+ };
+ // 鏌ヨ瑙勫垯鍒楄〃
+ const fetchData = () => {
+ getAttendanceRules({ current: -1, size: -1 }).then(res => {
+ classType.value = res.data.records;
+ });
+ };
+ // 鑾峰彇鐢ㄦ埛
+ const getUsers = () => {
+ // selectUserCondition({ type: 1 }).then(res => {
+ // let arr = res.data;
+ // personList.value = arr;
+ // });
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1,
+ }).then(res => {
+ let arr = res.data.records;
+ personList.value = arr;
+ });
+ };
+
+ // 鏍规嵁瀛楀吀鑾峰彇鏃ユ湡
+ const getDayByDic = e => {
+ let obj = classType.value.find(m => m.shift == e);
+ if (obj) {
+ return obj.id;
+ }
+ };
+
+ // 鏍规嵁瀛楀吀鑾峰彇鐝
+ const getShiftByDic = e => {
+ let obj = classType.value.find(m => m.shift == e);
+ if (obj) {
+ return obj.shift;
+ }
+ return "鏃�";
+ };
+
+ // 鍒濆鍖�
+ onMounted(() => {
+ fetchData();
+ getUsers();
+ fetchDeptOptions();
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ monthList.value = [];
+ for (let i = 12; i > 0; i--) {
+ monthList.value.push(i);
+ }
+ monthList.value.reverse();
+ });
+</script>
+
+<style scoped>
+ .class-page {
+ padding: 16px;
+ }
+
+ .form_title {
+ height: 36px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ font-weight: 800;
+ }
+
+ /* 鎼滅储鍖哄煙鏍峰紡 */
+ .search-container {
+ background: #f9fafb;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ }
+
+ .search-form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .search-row {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ }
+
+ .search-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .search-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ min-width: 65px;
+ text-align: right;
+ }
+
+ .search-input-group {
+ display: flex;
+ align-items: center;
+ }
+
+ .search-actions {
+ display: flex;
+ align-items: center;
+ margin-left: 8px;
+ }
+
+ .search-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex: 1;
+ }
+
+ /* 鍝嶅簲寮忚皟鏁� */
+ @media (max-width: 1200px) {
+ .search-row {
+ gap: 12px;
+ }
+
+ .search-item {
+ gap: 4px;
+ }
+
+ .search-label {
+ min-width: 60px;
+ font-size: 13px;
+ }
+
+ .search-actions {
+ margin-left: 4px;
+ }
+
+ .search-buttons {
+ margin-left: 12px;
+ }
+ }
+
+ @media (max-width: 992px) {
+ .search-row {
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ }
+
+ .search-buttons {
+ margin-left: 0;
+ margin-top: 12px;
+ width: 100%;
+ justify-content: flex-start;
+ }
+ }
+
+ /* 鎺掔彮瀹瑰櫒 */
+ .scheduling-container {
+ width: 100%;
+ min-height: calc(100vh - 280px);
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ margin-bottom: 20px;
+ }
+
+ /* 鎺掔彮琛ㄦ牸 */
+ .scheduling-table {
+ display: flex;
+ width: 100%;
+ height: calc(100vh - 280px);
+ }
+
+ /* 宸︿晶浜哄憳淇℃伅 */
+ .scheduling-left {
+ width: 240px;
+ min-width: 240px;
+ background-color: #f9fafb;
+ border-right: 1px solid #e5e7eb;
+ }
+
+ /* 鍙充晶鎺掔彮鍐呭 */
+ .scheduling-right {
+ flex: 1;
+ overflow-x: auto;
+ }
+
+ /* 琛ㄥご */
+ .scheduling-header {
+ height: 48px;
+ line-height: 48px;
+ padding: 0 20px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ background-color: #f3f4f6;
+ border-bottom: 1px solid #e5e7eb;
+ }
+
+ /* 浜哄憳淇℃伅琛� */
+ .scheduling-user {
+ display: flex;
+ align-items: center;
+ padding: 10px 10px;
+ border-bottom: 1px solid #e5e7eb;
+ transition: all 0.3s ease;
+ height: 65px;
+ box-sizing: border-box;
+ }
+
+ .scheduling-user:hover,
+ .scheduling-user-hover {
+ background-color: rgba(59, 130, 246, 0.05);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ /* 鐢ㄦ埛澶村儚 */
+ .user-avatar {
+ width: 42px;
+ height: 42px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #3b82f6, #60a5fa);
+ color: #fff;
+ font-size: 18px;
+ font-weight: 600;
+ text-align: center;
+ line-height: 42px;
+ margin-right: 16px;
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
+ transition: all 0.3s ease;
+ }
+
+ .scheduling-user:hover .user-avatar {
+ transform: scale(1.05);
+ box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
+ }
+
+ /* 鐢ㄦ埛璇︽儏 */
+ .user-details {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 100%;
+ }
+
+ /* 鐢ㄦ埛鍚� */
+ .user-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ margin: 0 0 6px 0;
+ line-height: 1.2;
+ }
+
+ /* 鐢ㄦ埛缁熻 */
+ .user-stats {
+ /* display: flex; */
+ /* flex-wrap: wrap;
+ gap: 10px; */
+ margin-bottom: 4px;
+ }
+
+ .stat-item {
+ font-size: 12px;
+ color: #666;
+ /* background-color: #f9fafb; */
+ /* padding: 2px 8px; */
+ padding-right: 4px;
+ /* border-radius: 10px; */
+ /* border: 1px solid #e5e7eb; */
+ /* transition: all 0.3s ease; */
+ }
+
+ .scheduling-user:hover .stat-item {
+ background-color: rgba(59, 130, 246, 0.1);
+ border-color: rgba(59, 130, 246, 0.3);
+ }
+
+ /* 鍚堣鍑哄嫟 */
+ .user-total {
+ display: flex;
+ align-items: center;
+ }
+
+ .total-label {
+ font-size: 12px;
+ color: #666;
+ margin-right: 6px;
+ font-weight: 500;
+ }
+
+ .total-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: #3b82f6;
+ background-color: rgba(59, 130, 246, 0.1);
+ padding: 2px 10px;
+ border-radius: 10px;
+ border: 1px solid rgba(59, 130, 246, 0.2);
+ }
+
+ /* 鏃ュ巻澶撮儴 */
+ .calendar-header {
+ display: flex;
+
+ border-bottom: 1px solid #e5e7eb;
+ }
+
+ .calendar-header-item {
+ width: 50px;
+ min-width: 50px;
+ height: 48px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border-right: 1px solid #e5e7eb;
+ background-color: #f3f4f6;
+ position: relative;
+ }
+
+ .week-number {
+ position: absolute;
+ top: 6px;
+ font-size: 10px;
+ font-weight: 600;
+ color: #3b82f6;
+ background-color: #dbeafe;
+ padding: 3px 6px;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2);
+ transition: all 0.3s ease;
+ }
+
+ .week-number:hover {
+ background-color: #3b82f6;
+ color: #fff;
+ transform: translateY(-1px);
+ }
+
+ .day-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .day-number {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ }
+
+ .day-week {
+ font-size: 12px;
+ color: #666;
+ }
+
+ /* 鏃ュ巻涓讳綋 */
+ .calendar-body {
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* 鏃ュ巻琛� */
+ .calendar-row {
+ display: flex;
+ border-bottom: 1px solid #e5e7eb;
+ transition: all 0.3s ease;
+ }
+
+ .calendar-row:hover,
+ .calendar-row-hover {
+ background-color: rgba(59, 130, 246, 0.03);
+ }
+
+ /* 鏃ュ巻鍗曞厓鏍� */
+ .calendar-cell {
+ width: 50px;
+ min-width: 50px;
+ height: 65px;
+ border-right: 1px solid #e5e7eb;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* 鐝涓嬫媺妗� */
+ .shift-dropdown {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* 鐝妗� */
+ .shift-box {
+ width: 90%;
+ height: 80%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ }
+
+ .shift-box:hover {
+ transform: scale(1.05);
+ }
+
+ /* 鐝绫诲瀷鏍峰紡 */
+ .shift-box-early {
+ background: rgba(59, 130, 246, 0.15);
+ color: #3b82f6;
+ }
+
+ .shift-box-mid {
+ background: rgba(139, 92, 246, 0.15);
+ color: #8b5cf6;
+ }
+
+ .shift-box-night {
+ background: rgba(245, 158, 11, 0.15);
+ color: #f59e0b;
+ }
+
+ .shift-box-rest {
+ background: rgba(16, 185, 129, 0.15);
+ color: #10b981;
+ }
+
+ .shift-box-leave {
+ background: rgba(239, 68, 68, 0.15);
+ color: #ef4444;
+ }
+
+ .shift-box-other {
+ background: rgba(236, 72, 153, 0.15);
+ color: #ec4899;
+ }
+
+ .shift-box-business {
+ background: rgba(17, 24, 39, 0.15);
+ color: #111827;
+ }
+
+ /* 鐝鏂囨湰 */
+ .shift-text {
+ text-align: center;
+ }
+
+ /* 骞村害琛ㄦ牸 */
+ .yearly-table {
+ display: flex;
+ width: 100%;
+ height: calc(100vh - 280px);
+ }
+
+ /* 骞村害鏃ュ巻 */
+ .yearly-calendar {
+ width: 100%;
+ }
+
+ /* 骞村害琛ㄥご */
+ .yearly-header {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ background-color: #f3f4f6;
+ border-bottom: 1px solid #e5e7eb;
+ }
+
+ .yearly-header-item {
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-right: 1px solid #e5e7eb;
+ }
+
+ .month-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ }
+
+ /* 骞村害涓讳綋 */
+ .yearly-body {
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* 骞村害琛� */
+ .yearly-row {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ border-bottom: 1px solid #e5e7eb;
+ transition: all 0.3s ease;
+ }
+
+ /* 骞村害鍗曞厓鏍� */
+ .yearly-cell {
+ padding: 12px;
+ border-right: 1px solid #e5e7eb;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ /* 鏈堝害鍑哄嫟 */
+ .monthly-attendance {
+ margin-bottom: 8px;
+ }
+
+ .attendance-label {
+ font-size: 12px;
+ color: #666;
+ margin-right: 4px;
+ }
+
+ .attendance-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ /* 鏈堝害缁熻 */
+ .monthly-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ justify-content: center;
+ }
+
+ .monthly-stats .stat-item {
+ font-size: 11px;
+ }
+
+ /* 婊氬姩鏉℃牱寮� */
+ .scheduling-right::-webkit-scrollbar {
+ height: 8px;
+ }
+
+ .scheduling-right::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ }
+
+ .scheduling-right::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 4px;
+ }
+
+ .scheduling-right::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+ }
+
+ .search_label {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 8px;
+ margin-top: 12px;
+ }
+</style>
diff --git a/src/views/personnelManagement/dimission/components/formDia.vue b/src/views/personnelManagement/dimission/components/formDia.vue
index 2b8a7fd..6d3cb46 100644
--- a/src/views/personnelManagement/dimission/components/formDia.vue
+++ b/src/views/personnelManagement/dimission/components/formDia.vue
@@ -97,6 +97,18 @@
</el-row>
<el-row :gutter="30">
<el-col :span="12">
+ <el-form-item label="绂昏亴鏃ユ湡锛�" prop="leaveDate">
+ <el-date-picker
+ v-model="form.leaveDate"
+ type="date"
+ placeholder="璇烽�夋嫨绂昏亴鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
<el-form-item label="绂昏亴鍘熷洜锛�" prop="reason">
<el-select v-model="form.reason" placeholder="璇烽�夋嫨绂昏亴鍘熷洜" style="width: 100%" @change="handleSelectDimissionReason">
<el-option
@@ -108,6 +120,8 @@
</el-select>
</el-form-item>
</el-col>
+ </el-row>
+ <el-row :gutter="30">
<el-col :span="12">
<el-form-item label="澶囨敞锛�" prop="remark" v-if="form.reason === 'other'">
<el-input
@@ -168,11 +182,13 @@
const data = reactive({
form: {
staffOnJobId: undefined,
+ leaveDate: "",
reason: "",
remark: "",
},
rules: {
staffName: [{ required: true, message: "璇烽�夋嫨浜哄憳" }],
+ leaveDate: [{ required: true, message: "璇烽�夋嫨绂昏亴鏃ユ湡", trigger: "change" }],
reason: [{ required: true, message: "璇烽�夋嫨绂昏亴鍘熷洜"}],
},
dimissionReasonOptions: [
@@ -193,6 +209,7 @@
if (operationType.value === 'edit') {
currentStaffRecord.value = row
form.value.staffOnJobId = row.staffOnJobId
+ form.value.leaveDate = row.leaveDate
form.value.reason = row.reason
form.value.remark = row.remark
personList.value = [
@@ -239,6 +256,7 @@
// 琛ㄥ崟宸叉敞閲婏紝鎵嬪姩閲嶇疆琛ㄥ崟鏁版嵁
form.value = {
staffOnJobId: undefined,
+ leaveDate: "",
reason: "",
remark: "",
};
diff --git a/src/views/personnelManagement/dimission/index.vue b/src/views/personnelManagement/dimission/index.vue
index c6ed705..02e3f02 100644
--- a/src/views/personnelManagement/dimission/index.vue
+++ b/src/views/personnelManagement/dimission/index.vue
@@ -76,6 +76,10 @@
},
},
{
+ label: "绂昏亴鏃ユ湡",
+ prop: "leaveDate",
+ },
+ {
label: "鍛樺伐缂栧彿",
prop: "staffNo",
},
diff --git a/src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue b/src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue
new file mode 100644
index 0000000..0aa4f06
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue
@@ -0,0 +1,181 @@
+<template>
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鍩烘湰淇℃伅
+ </span>
+ </template>
+
+ <el-row :gutter="24">
+ <el-col :span="5">
+ <el-form-item label="鍛樺伐缂栧彿" prop="staffNo">
+ <el-input
+ v-model="form.staffNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ :disabled="operationType !== 'add'"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="濮撳悕" prop="staffName">
+ <el-input
+ v-model="form.staffName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="鍒悕" prop="alias">
+ <el-input
+ v-model="form.alias"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="鎵嬫満" prop="phone">
+ <el-input
+ v-model="form.phone"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="11"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select
+ v-model="form.sex"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="鐢�" value="鐢�" />
+ <el-option label="濂�" value="濂�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="24">
+ <el-col :span="5">
+ <el-form-item label="鍑虹敓鏃ユ湡" prop="birthDate">
+ <el-date-picker
+ v-model="form.birthDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="骞撮緞" prop="age">
+ <el-input-number
+ v-model="form.age"
+ :min="0"
+ :max="150"
+ :precision="0"
+ :step="1"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="绫嶈疮" prop="nativePlace">
+ <el-input
+ v-model="form.nativePlace"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="姘戞棌" prop="nation">
+ <el-input
+ v-model="form.nation"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="濠氬Щ鐘跺喌" prop="maritalStatus">
+ <el-select
+ v-model="form.maritalStatus"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="鏈" value="鏈" />
+ <el-option label="宸插" value="宸插" />
+ <el-option label="绂诲紓" value="绂诲紓" />
+ <el-option label="涓у伓" value="涓у伓" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="24">
+ <el-col :span="10">
+ <el-form-item label="瑙掕壊" prop="roleId">
+ <el-select
+ v-model="form.roleId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in roleOptions"
+ :key="item.roleId"
+ :label="item.roleName"
+ :value="item.roleId"
+ :disabled="item.status == 1"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+ operationType: { type: String, default: "add" },
+ roleOptions: { type: Array, default: () => [] },
+});
+
+const { form, operationType, roleOptions } = toRefs(props);
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue b/src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue
new file mode 100644
index 0000000..c1470e7
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue
@@ -0,0 +1,263 @@
+<template>
+ <div>
+ <!-- 鏁欒偛缁忓巻 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鏁欒偛缁忓巻
+ </span>
+ </template>
+ <el-table :data="form.staffEducationList" border>
+ <el-table-column label="瀛﹀巻" prop="education" width="120">
+ <template #default="{ row }">
+ <el-select
+ v-model="row.education"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="涓笓鍙婁互涓�" value="secondary" />
+ <el-option label="澶т笓" value="junior_college" />
+ <el-option label="鏈" value="bachelor" />
+ <el-option label="纭曞+" value="master" />
+ <el-option label="鍗氬+鍙婁互涓�" value="doctor" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="姣曚笟闄㈡牎" prop="schoolName" min-width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.schoolName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="30"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏ュ鏃堕棿" prop="enrollTime" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.enrollTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="姣曚笟鏃堕棿" prop="graduateTime" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.graduateTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="涓撲笟" prop="major" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.major"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="瀛︿綅" prop="degree" width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.degree"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffEducationList.length > 1"
+ type="primary"
+ link
+ @click="removeEducationRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addEducationRow">鏂板缓涓�琛�</div>
+ </el-card>
+
+ <!-- 宸ヤ綔缁忓巻 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 宸ヤ綔缁忓巻
+ </span>
+ </template>
+ <el-table :data="form.staffWorkExperienceList" border>
+ <el-table-column label="鍓嶅叕鍙�" prop="formerCompany" min-width="180">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerCompany"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="30"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍓嶅叕鍙搁儴闂�" prop="formerDept" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerDept"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍓嶅叕鍙歌亴浣�" prop="formerPosition" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerPosition"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="寮�濮嬫棩鏈�" prop="startDate" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.startDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="缁撴潫鏃ユ湡" prop="endDate" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.endDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ヤ綔鎻忚堪" prop="workDesc" min-width="220">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.workDesc"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="500"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffWorkExperienceList.length > 1"
+ type="primary"
+ link
+ @click="removeWorkRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addWorkRow">鏂板缓涓�琛�</div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+});
+
+const emit = defineEmits(["update:form"]);
+
+const { form } = toRefs(props);
+
+const addEducationRow = () => {
+ form.value.staffEducationList.push({
+ education: "",
+ schoolName: "",
+ enrollTime: "",
+ graduateTime: "",
+ major: "",
+ degree: "",
+ });
+};
+
+const removeEducationRow = (index) => {
+ if (form.value.staffEducationList.length <= 1) return;
+ form.value.staffEducationList.splice(index, 1);
+};
+
+const addWorkRow = () => {
+ form.value.staffWorkExperienceList.push({
+ formerCompany: "",
+ formerDept: "",
+ formerPosition: "",
+ startDate: "",
+ endDate: "",
+ workDesc: "",
+ });
+};
+
+const removeWorkRow = (index) => {
+ if (form.value.staffWorkExperienceList.length <= 1) return;
+ form.value.staffWorkExperienceList.splice(index, 1);
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.table-add-row {
+ margin-top: 8px;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue b/src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue
new file mode 100644
index 0000000..bd63608
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue
@@ -0,0 +1,115 @@
+<template>
+ <div>
+ <!-- 绱ф�ヨ仈绯讳汉 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 绱ф�ヨ仈绯讳汉
+ </span>
+ </template>
+ <el-table :data="form.staffEmergencyContactList" border>
+ <el-table-column label="绱ф�ヨ仈绯讳汉濮撳悕" prop="contactName" min-width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉鍏崇郴" prop="contactRelation" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactRelation"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉鎵嬫満" prop="contactPhone" width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactPhone"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="11"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉浣忓潃" prop="contactAddress" min-width="220">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactAddress"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffEmergencyContactList.length > 1"
+ type="primary"
+ link
+ @click="removeEmergencyRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addEmergencyRow">鏂板缓涓�琛�</div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true }
+});
+
+const { form } = toRefs(props);
+
+const addEmergencyRow = () => {
+ form.value.staffEmergencyContactList.push({
+ contactName: "",
+ contactRelation: "",
+ contactPhone: "",
+ contactAddress: "",
+ });
+};
+
+const removeEmergencyRow = (index) => {
+ if (form.value.staffEmergencyContactList.length <= 1) return;
+ form.value.staffEmergencyContactList.splice(index, 1);
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.table-add-row {
+ margin-top: 8px;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue b/src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue
new file mode 100644
index 0000000..d3b8c4c
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue
@@ -0,0 +1,146 @@
+<template>
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鍦ㄨ亴淇℃伅
+ </span>
+ </template>
+
+ <!-- 绗竴琛岋細鍚堝悓寮�濮� / 鍚堝悓缁撴潫 / 璇曠敤鏈� / 杞 -->
+ <el-row :gutter="24">
+ <el-col :span="6">
+ <el-form-item label="鍏ヨ亴鏃ユ湡" prop="contractStartTime">
+ <el-date-picker
+ v-model="form.contractStartTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item
+ label="鍚堝悓缁撴潫鏃ユ湡"
+ prop="contractEndTime"
+ required
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鍚堝悓缁撴潫鏃ユ湡',
+ trigger: 'change',
+ },
+ ]"
+ >
+ <el-date-picker
+ v-model="form.contractEndTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璇曠敤鏈燂紙鏈堬級" prop="probationPeriod">
+ <el-input-number
+ v-model="form.proTerm"
+ :min="0"
+ :max="24"
+ :precision="0"
+ :step="1"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="杞鏃ユ湡" prop="positiveDate">
+ <el-date-picker
+ v-model="form.positiveDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 绗簩琛岋細閮ㄩ棬 / 宀椾綅 / 鍩烘湰宸ヨ祫 -->
+ <el-row :gutter="24">
+ <el-col :span="8">
+ <el-form-item label="閮ㄩ棬" prop="sysDeptId">
+ <el-tree-select
+ v-model="form.sysDeptId"
+ :data="deptOptions"
+ check-strictly
+ :render-after-expand="false"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="宀椾綅" prop="sysPostId">
+ <el-select
+ v-model="form.sysPostId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in postOptions"
+ :key="item.postId"
+ :label="item.postName"
+ :value="item.postId"
+ :disabled="item.status === '1'"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍩烘湰宸ヨ祫" prop="basicSalary">
+ <el-input-number
+ v-model="form.basicSalary"
+ :min="0"
+ :max="999999"
+ :precision="2"
+ :step="100"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+ postOptions: { type: Array, default: () => [] },
+ deptOptions: { type: Array, default: () => [] },
+});
+
+const { form, postOptions, deptOptions } = toRefs(props);
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue b/src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue
index f65509a..2ad06fb 100644
--- a/src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue
+++ b/src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue
@@ -1,325 +1,304 @@
<template>
- <div>
- <el-dialog
- v-model="dialogFormVisible"
- :title="operationType === 'add' ? '鏂板鍏ヨ亴' : '缂栬緫浜哄憳'"
- width="70%"
- @close="closeDia"
- >
- <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="鍛樺伐缂栧彿锛�" prop="staffNo">
- <el-input v-model="form.staffNo" placeholder="璇疯緭鍏�" clearable :disabled="operationType !== 'add'"/>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="濮撳悕锛�" prop="staffName">
- <el-input v-model="form.staffName" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="鎬у埆锛�" prop="sex">
- <el-select v-model="form.sex">
- <el-option label="鐢�" value="鐢�" />
- <el-option label="濂�" value="濂�" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鎴风睄浣忓潃锛�" prop="nativePlace">
- <el-input v-model="form.nativePlace" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="宀椾綅锛�" prop="sysPostId">
- <el-select v-model="form.sysPostId" placeholder="璇烽�夋嫨宀椾綅" clearable>
- <el-option
- v-for="item in postOptions"
- :key="item.postId"
- :label="item.postName"
- :value="item.postId"
- :disabled="item.status === '1'"
- />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鐜颁綇鍧�锛�" prop="adress">
- <el-input v-model="form.adress" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="閮ㄩ棬锛�" prop="sysDeptId">
- <el-tree-select
- v-model="form.sysDeptId"
- :data="deptOptions"
- :props="{ value: 'id', label: 'label', children: 'children' }"
- value-key="id"
- placeholder="璇烽�夋嫨閮ㄩ棬"
- check-strictly
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="骞撮緞锛�" prop="age">
- <el-input-number v-model="form.age" :precision="0" :step="1" style="width: 100%"/>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="绗竴瀛﹀巻锛�" prop="firstStudy">
- <el-input v-model="form.firstStudy" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="涓撲笟锛�" prop="profession">
- <el-input v-model="form.profession" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="鑱旂郴鐢佃瘽锛�" prop="phone">
- <el-input v-model="form.phone" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="绱ф�ヨ仈绯讳汉锛�" prop="emergencyContact">
- <el-input v-model="form.emergencyContact" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="绱ф�ヨ仈绯讳汉鑱旂郴鐢佃瘽锛�" prop="emergencyContactPhone">
- <el-input v-model="form.emergencyContactPhone" placeholder="璇疯緭鍏�" clearable/>
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鍚堝悓骞撮檺锛�" prop="contractTerm">
- <el-input-number v-model="form.contractTerm" :precision="0" :step="1" style="width: 100%" :disabled="true"/>
- </el-form-item>
- </el-col>
- </el-row>
- <el-row :gutter="30">
- <el-col :span="12">
- <el-form-item label="鍚堝悓寮�濮嬫棩鏈燂細" prop="contractStartTime">
- <el-date-picker
- v-model="form.contractStartTime"
- type="date"
- placeholder="璇烽�夋嫨鏃ユ湡"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- clearable
- style="width: 100%"
- @change="calculateContractTerm"
- />
- </el-form-item>
- </el-col>
- <el-col :span="12">
- <el-form-item label="鍚堝悓缁撴潫鏃ユ湡锛�" prop="contractEndTime">
- <el-date-picker
- v-model="form.contractEndTime"
- type="date"
- placeholder="璇烽�夋嫨鏃ユ湡"
- value-format="YYYY-MM-DD"
- format="YYYY-MM-DD"
- clearable
- style="width: 100%"
- @change="calculateContractTerm"
- />
- </el-form-item>
- </el-col>
- </el-row>
+ <FormDialog
+ v-model="dialogFormVisible"
+ :operation-type="operationType"
+ :title="dialogTitle"
+ width="90%"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia"
+ >
+ <div class="form-dia-body">
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ >
+ <BasicInfoSection
+ :form="form"
+ :operation-type="operationType"
+ :role-options="roleOptions"
+ />
+ <JobInfoSection
+ :form="form"
+ :post-options="postOptions"
+ :dept-options="deptOptions"
+ />
+ <EducationWorkSection :form="form" />
+ <EmergencyAndAttachmentSection :form="form" />
</el-form>
- <template #footer>
- <div class="dialog-footer">
- <el-button type="primary" @click="submitForm">纭</el-button>
- <el-button @click="closeDia">鍙栨秷</el-button>
- </div>
- </template>
- </el-dialog>
- </div>
+ </div>
+ </FormDialog>
</template>
<script setup>
-import {ref, onMounted} from "vue";
-import {findPostOptions} from "@/api/system/post.js";
-import {listDept} from "@/api/system/dept.js";
-import {staffOnJobInfo, createStaffOnJob, updateStaffOnJob} from "@/api/personnelManagement/staffOnJob.js";
-import {deptTreeSelect} from "@/api/system/user.js";
-const { proxy } = getCurrentInstance()
-const emit = defineEmits(['close'])
+import {
+ ref,
+ reactive,
+ toRefs,
+ onMounted,
+ getCurrentInstance,
+ nextTick,
+} from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { findPostOptions } from "@/api/system/post.js";
+import { deptTreeSelect, getUser } from "@/api/system/user.js";
+import {
+ staffOnJobInfo,
+ createStaffOnJob,
+ updateStaffOnJob,
+} from "@/api/personnelManagement/staffOnJob.js";
+
+import BasicInfoSection from "./BasicInfoSection.vue";
+import JobInfoSection from "./JobInfoSection.vue";
+import EducationWorkSection from "./EducationWorkSection.vue";
+import EmergencyAndAttachmentSection from "./EmergencyAndAttachmentSection.vue";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(["close"]);
const dialogFormVisible = ref(false);
-const operationType = ref('')
-const id = ref(0)
-const data = reactive({
- form: {
- staffNo: "",
- staffName: "",
- sex: "",
- nativePlace: "",
- postJob: "",
- adress: "",
- firstStudy: "",
- profession: "",
- age: 0,
- phone: "",
- emergencyContact: "",
- emergencyContactPhone: "",
- contractTerm: 0,
- contractStartTime: "",
- contractEndTime: "",
- sysPostId: undefined,
- sysDeptId: undefined,
- },
- rules: {
- staffNo: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" },],
- staffName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- sex: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- nativePlace: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- postJob: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- adress: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- firstStudy: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- profession: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- age: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- phone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- emergencyContact: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- emergencyContactPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- contractTerm: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- contractStartTime: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- contractEndTime: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- },
- postOptions: [], // 宀椾綅閫夐」
- deptOptions: [], // 閮ㄩ棬閫夐」
-});
-const { form, rules, postOptions, deptOptions } = toRefs(data);
+const operationType = ref("add");
+const id = ref(0);
+const formRef = ref(null);
-// 鎵撳紑寮规
+const dialogTitle = () =>
+ operationType.value === "add" ? "鏂板鍏ヨ亴" : "缂栬緫浜哄憳";
+
+const createEmptyEducation = () => ({
+ education: "",
+ schoolName: "",
+ enrollTime: "",
+ graduateTime: "",
+ major: "",
+ degree: "",
+});
+
+const createEmptyWork = () => ({
+ formerCompany: "",
+ formerDept: "",
+ formerPosition: "",
+ startDate: "",
+ endDate: "",
+ workDesc: "",
+});
+
+const createEmptyEmergency = () => ({
+ contactName: "",
+ contactRelation: "",
+ contactPhone: "",
+ contactAddress: "",
+});
+
+const createDefaultForm = () => ({
+ id: undefined,
+ // 鍩烘湰淇℃伅
+ staffNo: "",
+ staffName: "",
+ alias: "",
+ phone: "",
+ sex: "",
+ birthDate: "",
+ age: undefined,
+ nativePlace: "",
+ nation: "",
+ maritalStatus: "",
+ politicalStatus: "",
+ firstWorkDate: "",
+ workingYears: undefined,
+ idCardNo: "",
+ hukouType: "",
+ email: "",
+ currentAddress: "",
+ // 鍦ㄨ亴淇℃伅
+ contractStartTime: "",
+ contractEndTime: "",
+ proTerm: undefined,
+ positiveDate: "",
+ sysDeptId: undefined,
+ sysPostId: undefined,
+ basicSalary: undefined,
+ // 閾惰鍗′俊鎭�
+ bankName: "",
+ bankCardNo: "",
+ // 鏁欒偛缁忓巻
+ staffEducationList: [createEmptyEducation()],
+ // 宸ヤ綔缁忓巻
+ staffWorkExperienceList: [createEmptyWork()],
+ // 绱ф�ヨ仈绯讳汉
+ staffEmergencyContactList: [createEmptyEmergency()],
+ // 瑙掕壊锛堝崟閫夛級
+ roleId: undefined,
+});
+
+const state = reactive({
+ form: createDefaultForm(),
+ rules: {
+ staffNo: [{ required: true, message: "璇疯緭鍏ュ憳宸ョ紪鍙�", trigger: "blur" }],
+ staffName: [{ required: true, message: "璇疯緭鍏ュ鍚�", trigger: "blur" }],
+ phone: [{ required: true, message: "璇疯緭鍏ユ墜鏈�", trigger: "blur" }],
+ sex: [{ required: true, message: "璇烽�夋嫨鎬у埆", trigger: "change" }],
+ birthDate: [
+ { required: true, message: "璇烽�夋嫨鍑虹敓鏃ユ湡", trigger: "change" },
+ ],
+ contractStartTime: [
+ { required: true, message: "璇烽�夋嫨鍏ヨ亴鏃ユ湡", trigger: "change" },
+ ],
+ contractEndTime: [
+ { required: true, message: "璇烽�夋嫨鍚堝悓缁撴潫鏃ユ湡", trigger: "change" },
+ ],
+ sysDeptId: [
+ { required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" },
+ ],
+ roleId: [{ required: true, message: "璇烽�夋嫨瑙掕壊", trigger: "change" }],
+ },
+ postOptions: [],
+ deptOptions: [],
+});
+
+const { form, rules, postOptions, deptOptions } = toRefs(state);
+const roleOptions = ref([]);
+
+const resetForm = () => {
+ Object.assign(form.value, createDefaultForm());
+ nextTick(() => {
+ formRef.value?.clearValidate();
+ });
+};
+
+const fetchPostOptions = () => {
+ findPostOptions().then((res) => {
+ postOptions.value = res.data || [];
+ });
+};
+
+const fetchDeptOptions = () => {
+ deptTreeSelect().then((response) => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data || []))
+ );
+ });
+};
+
+const fetchRoleOptions = () => {
+ getUser().then((res) => {
+ roleOptions.value = res.roles || [];
+ });
+};
+
+function filterDisabledDept(deptList) {
+ return deptList.filter((dept) => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+}
+
const openDialog = (type, row) => {
operationType.value = type;
dialogFormVisible.value = true;
- if (operationType.value === 'edit') {
- id.value = row.id
- staffOnJobInfo(id.value, {}).then(res => {
- form.value = {...res.data}
+ fetchPostOptions();
+ fetchDeptOptions();
+ fetchRoleOptions();
+ resetForm();
+ if (type === "edit" && row?.id) {
+ id.value = row.id;
+ staffOnJobInfo(id.value, {}).then((res) => {
+ const d = res.data || {};
+ Object.assign(form.value, {
+ ...form.value,
+ ...d,
+ });
+ if (
+ !Array.isArray(form.value.staffEducationList) ||
+ !form.value.staffEducationList.length
+ ) {
+ form.value.staffEducationList = [createEmptyEducation()];
+ }
+ if (
+ !Array.isArray(form.value.staffWorkExperienceList) ||
+ !form.value.staffWorkExperienceList.length
+ ) {
+ form.value.staffWorkExperienceList = [createEmptyWork()];
+ }
+ if (
+ !Array.isArray(form.value.staffEmergencyContactList) ||
+ !form.value.staffEmergencyContactList.length
+ ) {
+ form.value.staffEmergencyContactList = [createEmptyEmergency()];
+ }
if (form.value.sysPostId === 0) {
- form.value.sysPostId = undefined
+ form.value.sysPostId = undefined;
}
if (form.value.sysDeptId === 0) {
- form.value.sysDeptId = undefined
+ form.value.sysDeptId = undefined;
}
- // 缂栬緫鏃朵篃璁$畻涓�娆″悎鍚屽勾闄�
- calculateContractTerm();
- })
- } else {
- form.value.id = ''
- }
+ });
+ }
+};
-}
onMounted(() => {
- fetchPostOptions()
- fetchDeptOptions()
-})
+ fetchPostOptions();
+ fetchDeptOptions();
+});
-const fetchPostOptions = () => {
- findPostOptions().then(res => {
- postOptions.value = res.data
- })
-}
-
-// 鏌ヨ閮ㄩ棬鍒楄〃
-const fetchDeptOptions = () => {
- deptTreeSelect().then(response => {
- deptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
- })
-}
-
-/** 杩囨护绂佺敤鐨勯儴闂� */
-function filterDisabledDept(deptList) {
- return deptList.filter(dept => {
- if (dept.disabled) {
- return false
- }
- if (dept.children && dept.children.length) {
- dept.children = filterDisabledDept(dept.children)
- }
- return true
- })
-}
-
-// 鎻愪氦浜у搧琛ㄥ崟
const submitForm = () => {
if (!form.value.sysPostId) {
- form.value.sysPostId = 0;
+ form.value.sysPostId = undefined;
}
if (!form.value.sysDeptId) {
- form.value.sysDeptId = 0;
+ form.value.sysDeptId = undefined;
}
- proxy.$refs.formRef.validate(valid => {
+ // 鍏煎鍚庣鍙兘浠嶄娇鐢� roleIds 鏁扮粍
+ form.value.roleIds = form.value.roleId ? [form.value.roleId] : [];
+ formRef.value?.validate((valid) => {
if (valid) {
if (operationType.value === "add") {
- createStaffOnJob(form.value).then(res => {
+ createStaffOnJob(form.value).then(() => {
proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
closeDia();
- })
+ });
} else {
- updateStaffOnJob(id.value, form.value).then(res => {
+ updateStaffOnJob(id.value, form.value).then(() => {
proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
closeDia();
- })
+ });
}
}
- })
-}
-// 璁$畻鍚堝悓骞撮檺
-const calculateContractTerm = () => {
- if (form.value.contractStartTime && form.value.contractEndTime) {
- const startDate = new Date(form.value.contractStartTime);
- const endDate = new Date(form.value.contractEndTime);
-
- if (endDate > startDate) {
- // 璁$畻骞翠唤宸�
- const yearDiff = endDate.getFullYear() - startDate.getFullYear();
- const monthDiff = endDate.getMonth() - startDate.getMonth();
- const dayDiff = endDate.getDate() - startDate.getDate();
-
- let years = yearDiff;
-
- // 濡傛灉缁撴潫鏃ユ湡鐨勬湀鏃ュ皬浜庡紑濮嬫棩鏈熺殑鏈堟棩锛屽垯鍑忓幓1骞�
- if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
- years = yearDiff - 1;
- }
-
- form.value.contractTerm = Math.max(0, years);
- } else {
- form.value.contractTerm = 0;
- }
- } else {
- form.value.contractTerm = 0;
- }
+ });
};
-// 鍏抽棴寮规
const closeDia = () => {
- proxy.resetForm("formRef");
+ formRef.value?.resetFields();
dialogFormVisible.value = false;
- emit('close')
+ emit("close");
};
+
defineExpose({
openDialog,
});
</script>
<style scoped>
+.form-dia-body {
+ padding: 0;
+}
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.form-card {
+ margin-bottom: 16px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/employeeRecord/index.vue b/src/views/personnelManagement/employeeRecord/index.vue
index 19addfa..e5f2e8b 100644
--- a/src/views/personnelManagement/employeeRecord/index.vue
+++ b/src/views/personnelManagement/employeeRecord/index.vue
@@ -102,67 +102,40 @@
prop: "staffName",
},
{
+ label: "鍒悕",
+ prop: "alias",
+ },
+ {
+ label: "鎵嬫満",
+ prop: "phone",
+ width: 150,
+ },
+ {
label: "鎬у埆",
prop: "sex",
},
{
- label: "鎴风睄浣忓潃",
- prop: "nativePlace",
- },
- {
- label: "閮ㄩ棬",
- prop: "deptName",
- },
- {
- label: "宀椾綅",
- prop: "postJob",
- },
- {
- label: "鐜颁綇鍧�",
- prop: "adress",
- width:200
- },
- {
- label: "绗竴瀛﹀巻",
- prop: "firstStudy",
- },
- {
- label: "涓撲笟",
- prop: "profession",
- width:100
+ label: "鍑虹敓鏃ユ湡",
+ prop: "birthDate",
+ width: 120,
},
{
label: "骞撮緞",
prop: "age",
},
{
- label: "鑱旂郴鐢佃瘽",
- prop: "phone",
- width:150
+ label: "绫嶈疮",
+ prop: "nativePlace",
},
{
- label: "绱ф�ヨ仈绯讳汉",
- prop: "emergencyContact",
- width: 120
+ label: "姘戞棌",
+ prop: "nation",
+ width: 100,
},
{
- label: "绱ф�ヨ仈绯讳汉鐢佃瘽",
- prop: "emergencyContactPhone",
- width:150
- },
- // {
- // label: "鍚堝悓骞撮檺",
- // prop: "contractTerm",
- // },
- // {
- // label: "鍚堝悓寮�濮嬫棩鏈�",
- // prop: "contractStartTime",
- // width: 120
- // },
- {
- label: "鍚堝悓缁撴潫鏃ユ湡",
- prop: "contractExpireTime",
- width: 120
+ label: "濠氬Щ鐘跺喌",
+ prop: "maritalStatus",
+ width: 100,
},
{
dataType: "action",
diff --git a/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue b/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue
new file mode 100644
index 0000000..48a41f0
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue
@@ -0,0 +1,216 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ title="宸ヨ祫瀹℃牳"
+ width="900px"
+ @close="handleClose"
+ >
+ <!-- 宸ヨ祫琛ㄥ熀纭�淇℃伅 -->
+ <el-card shadow="never" style="margin-bottom: 16px;">
+ <template #header>
+ <span>宸ヨ祫琛ㄤ俊鎭�</span>
+ </template>
+ <el-descriptions :column="3" border>
+ <el-descriptions-item label="宸ヨ祫涓婚">{{ auditData?.salaryTitle || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="宸ヨ祫鏈堜唤">{{ auditData?.salaryMonth || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="宸ヨ祫鎬婚">楼 {{ formatMoney(auditData?.totalSalary) }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯閾惰">{{ auditData?.payBank || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳浜�">{{ auditData?.auditUserName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ auditData?.remark || '-' }}</el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+
+ <!-- 鍛樺伐宸ヨ祫鏄庣粏 -->
+ <el-card shadow="never" style="margin-bottom: 16px;">
+ <template #header>
+ <span>鍛樺伐宸ヨ祫鏄庣粏</span>
+ </template>
+ <div v-if="!employeeList || employeeList.length === 0" style="text-align: center; padding: 20px; color: #909399;">
+ <div>鏆傛棤鍛樺伐宸ヨ祫鏄庣粏鏁版嵁</div>
+ <div style="font-size: 12px; margin-top: 5px;">鍛樺伐鏄庣粏鏁版嵁闇�瑕佸湪宸ヨ祫琛ㄧ敓鎴愭垨缂栬緫鏃舵墠浼氫繚瀛�</div>
+ </div>
+ <div v-else>
+ <el-table :data="employeeList" border max-height="300" style="width: 100%">
+ <el-table-column prop="staffName" label="鍛樺伐濮撳悕" width="100" />
+ <el-table-column prop="deptName" label="閮ㄩ棬" width="120" />
+ <el-table-column prop="basicSalary" label="鍩烘湰宸ヨ祫" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.basicSalary) }}</template>
+ </el-table-column>
+ <el-table-column prop="pieceSalary" label="璁′欢宸ヨ祫" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.pieceSalary) }}</template>
+ </el-table-column>
+ <el-table-column prop="hourlySalary" label="璁℃椂宸ヨ祫" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.hourlySalary) }}</template>
+ </el-table-column>
+ <el-table-column prop="otherIncome" label="鍏朵粬鏀跺叆" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.otherIncome) }}</template>
+ </el-table-column>
+ <el-table-column prop="socialPersonal" label="绀句繚涓汉" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.socialPersonal) }}</template>
+ </el-table-column>
+ <el-table-column prop="fundPersonal" label="鍏Н閲戜釜浜�" width="120" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.fundPersonal) }}</template>
+ </el-table-column>
+ <el-table-column prop="salaryTax" label="宸ヨ祫涓◣" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.salaryTax) }}</template>
+ </el-table-column>
+ <el-table-column prop="netSalary" label="瀹炲彂宸ヨ祫" width="100" align="right" fixed="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.netSalary) }}</template>
+ </el-table-column>
+ </el-table>
+ <div style="margin-top: 10px; text-align: right; font-weight: bold;">
+ 宸ヨ祫鎬婚锛毬� {{ formatMoney(totalSalary) }}
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 瀹℃牳鎿嶄綔 -->
+ <el-form label-position="top">
+ <el-form-item label="瀹℃牳缁撴灉" required>
+ <el-radio-group v-model="auditResult">
+ <el-radio :value="4">閫氳繃</el-radio>
+ <el-radio :value="2">涓嶉�氳繃</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <el-button type="primary" :loading="loading" @click="handleConfirm">
+ 纭畾
+ </el-button>
+ <el-button @click="handleClose">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+</template>
+
+<script setup>
+import { ref, computed, reactive, toRefs, getCurrentInstance, watch } from "vue";
+import { ElMessage } from "element-plus";
+import Cookies from "js-cookie";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { staffSalaryMainUpdate } from "@/api/personnelManagement/staffSalaryMain.js";
+
+const emit = defineEmits(["update:modelValue", "close", "success"]);
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ row: { type: Object, default: () => ({}) },
+});
+
+const { proxy } = getCurrentInstance();
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit("update:modelValue", val),
+});
+
+const loading = ref(false);
+const auditResult = ref(4); // 榛樿閫氳繃
+const auditData = ref({});
+const employeeList = ref([]);
+
+// 鐩戝惉row鏁版嵁鍙樺寲
+watch(() => props.row, (newRow) => {
+ if (newRow && Object.keys(newRow).length > 0) {
+ loadAuditData(newRow);
+ }
+}, { immediate: true });
+
+// 鏍煎紡鍖栭噾棰�
+const formatMoney = (value) => {
+ const num = Number(value) || 0;
+ return num.toFixed(2);
+};
+
+// 璁$畻宸ヨ祫鎬婚
+const totalSalary = computed(() => {
+ return employeeList.value.reduce((sum, e) => {
+ const salary = Number(e.netSalary) || 0;
+ return sum + salary;
+ }, 0);
+});
+
+// 鍔犺浇瀹℃牳鏁版嵁
+const loadAuditData = (row) => {
+ auditData.value = row || {};
+ auditResult.value = 4; // 榛樿閫夋嫨閫氳繃
+
+ // 鍔犺浇鍛樺伐宸ヨ祫鏄庣粏鏁版嵁
+ if (row?.staffSalaryDetailList && Array.isArray(row.staffSalaryDetailList)) {
+ employeeList.value = row.staffSalaryDetailList.map((e) => ({
+ staffName: e.staffName ?? "",
+ deptName: e.deptName ?? "",
+ basicSalary: Number(e.basicSalary) || 0,
+ pieceSalary: Number(e.pieceSalary) || 0,
+ hourlySalary: Number(e.hourlySalary) || 0,
+ otherIncome: Number(e.otherIncome) || 0,
+ socialPersonal: Number(e.socialPersonal) || 0,
+ fundPersonal: Number(e.fundPersonal) || 0,
+ salaryTax: Number(e.salaryTax) || 0,
+ netSalary: Number(e.netSalary) || 0,
+ }));
+ } else {
+ console.log('娌℃湁鎵惧埌鍛樺伐鏄庣粏鏁版嵁');
+ employeeList.value = [];
+ }
+};
+
+// 鎵撳紑寮圭獥
+const openDialog = (row) => {
+ loadAuditData(row);
+ dialogVisible.value = true;
+};
+
+// 鍏抽棴寮圭獥
+const handleClose = () => {
+ dialogVisible.value = false;
+ emit("close");
+};
+
+// 纭瀹℃牳
+const handleConfirm = () => {
+ try {
+ const row = auditData.value;
+ if (!row?.id) {
+ ElMessage.warning("鏁版嵁寮傚父锛岃閲嶈瘯");
+ return;
+ }
+
+ const username = Cookies.get("username") || "";
+ const userIdRaw = Cookies.get("userId");
+ const auditUserId = userIdRaw ? Number(userIdRaw) : undefined;
+
+ // 鏋勫缓瀹℃牳鏁版嵁
+ const submitData = {
+ id: row.id,
+ status: Number(auditResult.value) === 2 ? 2 : 4, // 2=涓嶉�氳繃 4=閫氳繃(寰呭彂鏀�)
+ auditUserId,
+ auditUserName: username,
+ };
+ loading.value = true;
+ staffSalaryMainUpdate(submitData)
+ .then(() => {
+ ElMessage.success("瀹℃牳鎴愬姛");
+ dialogVisible.value = false;
+ emit("success");
+ })
+ .catch((error) => {
+ console.error('瀹℃牳澶辫触:', error)
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ } catch (error) {
+ console.error('瀹℃牳澶勭悊寮傚父:', error);
+ loading.value = false;
+ }
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped>
+:deep(.el-descriptions__label) {
+ width: 100px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue b/src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue
new file mode 100644
index 0000000..b5fad15
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue
@@ -0,0 +1,188 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ operation-type="edit"
+ title="璁剧疆鍙戞斁閾惰涓嬫媺鏁版嵁"
+ width="640px"
+ @close="handleClose"
+ @confirm="handleConfirm"
+ @cancel="handleCancel"
+ >
+ <el-form ref="formRef" :model="form" label-position="top">
+ <el-row :gutter="16">
+ <el-col :span="24" style="display: flex; justify-content: end; gap: 10px;margin-bottom: 10px">
+ <el-button type="primary" @click="addBank">鏂板閾惰</el-button>
+ <el-button @click="resetToEmpty">娓呯┖</el-button>
+ </el-col>
+ </el-row>
+
+ <el-table :data="form.banks" border style="width: 100%">
+ <el-table-column label="閾惰鍚嶇О" min-width="260">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.bankName"
+ placeholder="渚嬪锛氫腑鍥藉伐鍟嗛摱琛�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="90" align="center">
+ <template #default="{ $index }">
+ <el-button type="danger" link @click="removeBank($index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div style="margin-top: 10px; color: #909399; font-size: 12px">
+ 鎻愮ず锛氳繖閲岀淮鎶ょ殑鏄�滃彂鏀鹃摱琛屸�濅笅鎷夋閫夐」鏁版嵁锛涗繚瀛樺悗鍦ㄦ柊寤�/缂栬緫宸ヨ祫琛ㄤ腑鍙�夋嫨銆�
+ </div>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+import { computed, reactive, ref, toRefs, watch, getCurrentInstance } from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { bankAdd, bankDelete, bankList, bankUpdate } from "@/api/personnelManagement/bank.js";
+
+const emit = defineEmits(["update:modelValue", "close", "saved"]);
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+});
+
+const { proxy } = getCurrentInstance();
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit("update:modelValue", val),
+});
+
+const formRef = ref(null);
+
+const data = reactive({
+ form: {
+ banks: [],
+ },
+});
+
+const { form } = toRefs(data);
+
+function newKey() {
+ return Math.random().toString(36).slice(2);
+}
+
+const addBank = () => {
+ form.value.banks.push({
+ _key: newKey(),
+ id: undefined,
+ bankName: "",
+ _originBankName: "",
+ });
+};
+
+const removeBank = (index) => {
+ const row = form.value.banks?.[index];
+ if (!row) return;
+ // 鏈惤搴撶殑琛岋細鐩存帴绉婚櫎
+ if (!row.id) {
+ form.value.banks.splice(index, 1);
+ return;
+ }
+ // 宸茶惤搴擄細璋冪敤鍚庣鍒犻櫎
+ bankDelete([row.id]).then(() => {
+ proxy?.$modal?.msgSuccess?.("鍒犻櫎鎴愬姛");
+ form.value.banks.splice(index, 1);
+ emit("saved");
+ });
+};
+
+const resetToEmpty = () => {
+ if (!form.value.banks?.length) return;
+ const ids = form.value.banks.map((b) => b?.id).filter(Boolean);
+ // 鑻ュ叏閮ㄦ槸鏈繚瀛樿锛屽垯浠呮竻绌烘湰鍦�
+ if (!ids.length) {
+ form.value.banks = [];
+ return;
+ }
+ proxy?.$modal
+ ?.confirm?.("纭畾娓呯┖鎵�鏈夐摱琛屽悧锛�")
+ .then(() => bankDelete(ids))
+ .then(() => {
+ proxy?.$modal?.msgSuccess?.("娓呯┖鎴愬姛");
+ form.value.banks = [];
+ emit("saved");
+ })
+ .catch(() => {});
+};
+
+const loadSetting = () => {
+ return bankList().then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ form.value.banks = list.map((b) => ({
+ _key: newKey(),
+ id: b?.id,
+ bankName: b?.bankName ?? "",
+ _originBankName: b?.bankName ?? "",
+ }));
+ });
+};
+
+const openDialog = () => {
+ loadSetting();
+};
+
+watch(
+ () => dialogVisible.value,
+ (val) => {
+ if (val) openDialog();
+ }
+);
+
+const handleConfirm = () => {
+ const names = (form.value.banks || [])
+ .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
+ .filter((n) => n !== "");
+ const unique = Array.from(new Set(names));
+ if (!unique.length) {
+ proxy?.$modal?.msgWarning?.("璇疯嚦灏戞柊澧炰竴涓摱琛岄�夐」");
+ return;
+ }
+ if (unique.length !== names.length) {
+ proxy?.$modal?.msgWarning?.("閾惰鍚嶇О涓嶅彲閲嶅");
+ return;
+ }
+
+ const rows = form.value.banks.map((b) => ({
+ ...b,
+ bankName: b?.bankName == null ? "" : String(b.bankName).trim(),
+ }));
+
+ const toAdd = rows.filter((b) => !b.id && b.bankName);
+ const toUpdate = rows.filter((b) => b.id && b.bankName && b.bankName !== (b._originBankName ?? ""));
+
+ Promise.all([
+ ...toAdd.map((b) => bankAdd({ bankName: b.bankName })),
+ ...toUpdate.map((b) => bankUpdate({ id: b.id, bankName: b.bankName })),
+ ])
+ .then(() => loadSetting())
+ .then(() => {
+ proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
+ dialogVisible.value = false;
+ emit("saved", { options: unique });
+ });
+};
+
+const handleCancel = () => {
+ dialogVisible.value = false;
+};
+
+const handleClose = () => {
+ emit("close");
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped></style>
+
diff --git a/src/views/personnelManagement/monthlyStatistics/components/formDia.vue b/src/views/personnelManagement/monthlyStatistics/components/formDia.vue
new file mode 100644
index 0000000..36c2ec3
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/components/formDia.vue
@@ -0,0 +1,804 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ :title="operationType === 'add' ? '鏂板缓宸ヨ祫琛�' : '缂栬緫宸ヨ祫琛�'"
+ width="90%"
+ @close="closeDia"
+ >
+ <template #footer>
+ <el-button type="info" @click="saveDraft">淇濆瓨鑽夌</el-button>
+ <el-button type="primary" @click="submitForm">纭鎻愪氦</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </template>
+ <div class="form-dia-body">
+ <!-- 鍩虹璧勬枡 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title"><span class="card-title-line">|</span> 鍩虹璧勬枡</span>
+ <el-icon class="card-collapse"><ArrowUp /></el-icon>
+ </template>
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+ <el-row :gutter="24">
+ <el-col :span="6">
+ <el-form-item label="宸ヨ祫涓婚" prop="salaryTitle">
+ <el-input
+ v-model="form.salaryTitle"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閫夋嫨閮ㄩ棬" prop="deptIds">
+ <el-select
+ v-model="form.deptIds"
+ placeholder="璇烽�夋嫨"
+ clearable
+ multiple
+ collapse-tags-tooltip
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in deptOptions"
+ :key="item.deptId"
+ :label="item.deptName"
+ :value="item.deptId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閫夋嫨宸ヨ祫鏈堜唤" prop="salaryMonth">
+ <el-date-picker
+ v-model="form.salaryMonth"
+ type="month"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨宸ヨ祫鏈堜唤"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="form.remark"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="24">
+ <el-col :span="6">
+ <el-form-item label="鏀粯閾惰" prop="payBank">
+ <el-select
+ v-model="form.payBank"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="b in bankOptions"
+ :key="b"
+ :label="b"
+ :value="b"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹℃牳浜�" prop="auditUserId">
+ <el-select
+ v-model="form.auditUserId"
+ placeholder="璇烽�夋嫨瀹℃牳浜�"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </el-card>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="toolbar">
+ <el-button type="primary" @click="handleGenerate">鐢熸垚宸ヨ祫琛�</el-button>
+ <el-button @click="handleClear">娓呯┖</el-button>
+ <el-button @click="handleBatchDelete">鍒犻櫎</el-button>
+ <el-button @click="handleTaxForm">涓◣琛�</el-button>
+ </div>
+
+ <!-- 鍛樺伐宸ヨ祫璇︽儏琛ㄦ牸 -->
+ <div class="employee-table-wrap">
+ <el-table
+ ref="employeeTableRef"
+ :data="employeeList"
+ border
+ max-height="400"
+ @selection-change="onEmployeeSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鍛樺伐濮撳悕" prop="staffName" minWidth="100" />
+ <el-table-column label="閮ㄩ棬" prop="deptName" minWidth="100" />
+ <el-table-column label="鍩烘湰宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.basicSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.basicSalary = parseNum(row.basicSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="璁′欢宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.pieceSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.pieceSalary = parseNum(row.pieceSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="璁℃椂宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.hourlySalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.hourlySalary = parseNum(row.hourlySalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏朵粬鏀跺叆" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.otherIncome"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.otherIncome = parseNum(row.otherIncome)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绀句繚涓汉" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.socialPersonal"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.socialPersonal = parseNum(row.socialPersonal)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏Н閲戜釜浜�" minWidth="120">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.fundPersonal"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.fundPersonal = parseNum(row.fundPersonal)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏朵粬鏀嚭" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.otherDeduct"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.otherDeduct = parseNum(row.otherDeduct)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ヨ祫涓◣" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.salaryTax"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.salaryTax = parseNum(row.salaryTax)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴斿彂宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.grossSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.grossSalary = parseNum(row.grossSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴旀墸宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.deductSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.deductSalary = parseNum(row.deductSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹炲彂宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.netSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.netSalary = parseNum(row.netSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" minWidth="120">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.remark"
+ placeholder="璇疯緭鍏�"
+ size="small"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center" fixed="right">
+ <template #default="{ row }">
+ <el-button type="primary" link @click="removeEmployee(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div v-if="!employeeList.length" class="table-empty">鏆傛棤鏁版嵁</div>
+ <div v-else class="salary-total">
+ <span class="total-label">宸ヨ祫鎬婚锛�</span>
+ <span class="total-value">楼 {{ totalSalary.toFixed(2) }}</span>
+ </div>
+ </div>
+ </div>
+
+
+
+ <!-- 鏂板浜哄憳寮圭獥 -->
+ <el-dialog
+ v-model="addPersonVisible"
+ title="鏂板浜哄憳"
+ width="400px"
+ append-to-body
+ @close="addPersonClose"
+ >
+ <div class="add-person-tree">
+ <el-tree
+ ref="personTreeRef"
+ :data="deptStaffTree"
+ show-checkbox
+ node-key="id"
+ :props="{ label: 'label', children: 'children' }"
+ default-expand-all
+ />
+ </div>
+ <template #footer>
+ <el-button @click="addPersonVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="confirmAddPerson">纭畾</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 涓◣琛ㄥ脊绐� -->
+ <el-dialog
+ v-model="taxDialogVisible"
+ title="涓◣琛�"
+ width="700px"
+ append-to-body
+ >
+ <div class="tax-desc">涓汉鎵�寰楃◣鍏嶅緛棰濓細5000鍏�</div>
+ <el-table :data="taxTableData" border style="width: 100%;margin-bottom: 20px;">
+ <el-table-column prop="level" label="绾ф暟" width="80" align="center" />
+ <el-table-column
+ prop="range"
+ label="鍏ㄥ勾搴旂撼绋庢墍寰楅/鍏�"
+ min-width="220"
+ />
+ <el-table-column
+ prop="rate"
+ label="绋庣巼(%)"
+ width="100"
+ align="center"
+ />
+ <el-table-column
+ prop="quickDeduction"
+ label="閫熺畻鎵i櫎鏁�/鍏�"
+ width="160"
+ align="center"
+ />
+ </el-table>
+ </el-dialog>
+ </FormDialog>
+</template>
+
+<script setup>
+import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue";
+import { ArrowUp } from "@element-plus/icons-vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listDept } from "@/api/system/dept.js";
+import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js";
+import { bankList } from "@/api/personnelManagement/bank.js";
+import {
+ staffSalaryMainAdd,
+ staffSalaryMainUpdate,
+ staffSalaryMainCalculateSalary,
+} from "@/api/personnelManagement/staffSalaryMain.js";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+
+
+const emit = defineEmits(["update:modelValue", "close"]);
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ operationType: { type: String, default: "add" },
+ row: { type: Object, default: () => ({}) },
+});
+
+const { proxy } = getCurrentInstance();
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit("update:modelValue", val),
+});
+
+const formRef = ref(null);
+const employeeTableRef = ref(null);
+const personTreeRef = ref(null);
+const addPersonVisible = ref(false);
+const taxDialogVisible = ref(false);
+const deptOptions = ref([]);
+const deptStaffTree = ref([]);
+const employeeList = ref([]);
+const selectedEmployees = ref([]);
+const bankOptions = ref([]);
+const userList = ref([]);
+const taxTableData = ref([
+ { level: 1, range: "涓嶈秴杩�36000鍏�", rate: 3, quickDeduction: 0 },
+ { level: 2, range: "瓒呰繃36000-144000鍏�", rate: 10, quickDeduction: 2520 },
+ { level: 3, range: "瓒呰繃144000-300000鍏�", rate: 20, quickDeduction: 16920 },
+ { level: 4, range: "瓒呰繃300000-420000鍏�", rate: 25, quickDeduction: 31920 },
+ { level: 5, range: "瓒呰繃420000-660000鍏�", rate: 30, quickDeduction: 52920 },
+ { level: 6, range: "瓒呰繃660000-960000鍏�", rate: 35, quickDeduction: 85920 },
+ { level: 7, range: "瓒呰繃960000鍏�", rate: 45, quickDeduction: 181920 },
+]);
+
+function parseNum(v) {
+ if (v === "" || v == null) return 0;
+ const n = Number(v);
+ return isNaN(n) ? 0 : n;
+}
+
+// 鍩虹璧勬枡琛ㄥ崟
+const data = reactive({
+ form: {
+ id: undefined,
+ salaryTitle: "",
+ deptIds: [],
+ salaryMonth: "",
+ remark: "",
+ payBank: "",
+ auditUserId: undefined,
+ },
+ rules: {
+ salaryTitle: [{ required: true, message: "璇疯緭鍏ュ伐璧勪富棰�", trigger: "blur" }],
+ deptIds: [{ required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" }],
+ salaryMonth: [{ required: true, message: "璇烽�夋嫨宸ヨ祫鏈堜唤", trigger: "change" }],
+ auditUserId: [{ required: true, message: "璇烽�夋嫨瀹℃牳浜�", trigger: "change" }],
+ },
+});
+const { form, rules } = toRefs(data);
+
+// 璁$畻宸ヨ祫鎬婚锛堟墍鏈夊憳宸ュ疄鍙戝伐璧勪箣鍜岋級
+const totalSalary = computed(() => {
+ return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0);
+});
+
+// 鏍规嵁瀹℃牳浜篒D鑾峰彇瀹℃牳浜哄悕绉�
+const auditUserName = computed(() => {
+ if (!form.value.auditUserId) return "";
+ const user = userList.value.find(u => u.userId === form.value.auditUserId);
+ return user ? user.nickName : "";
+});
+
+const loadBankOptions = () => {
+ return bankList().then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ bankOptions.value = list
+ .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
+ .filter((v) => v !== "");
+ });
+};
+
+const loadUserList = () => {
+ return userListNoPageByTenantId().then((res) => {
+ userList.value = res.data || [];
+ });
+};
+
+// 鎵佸钩鍖栭儴闂ㄦ爲渚涗笅鎷変娇鐢�
+function flattenDept(tree, list = []) {
+ if (!tree?.length) return list;
+ tree.forEach((node) => {
+ list.push({ deptId: node.deptId, deptName: node.deptName });
+ if (node.children?.length) flattenDept(node.children, list);
+ });
+ return list;
+}
+
+const loadDeptOptions = () => {
+ listDept().then((res) => {
+ const tree = res.data ?? [];
+ deptOptions.value = flattenDept(tree);
+ });
+};
+
+// 鏋勫缓 閮ㄩ棬-浜哄憳 鏍戯紙鐢ㄤ簬鏂板浜哄憳寮圭獥锛�
+const loadDeptStaffTree = () => {
+ Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => {
+ const tree = deptRes.data ?? [];
+ const staffList = staffRes.data ?? [];
+ const deptMap = new Map();
+ function walk(nodes) {
+ nodes.forEach((node) => {
+ deptMap.set(node.deptId, {
+ id: "dept_" + node.deptId,
+ deptId: node.deptId,
+ label: node.deptName,
+ type: "dept",
+ children: [],
+ });
+ if (node.children?.length) walk(node.children);
+ });
+ }
+ walk(tree);
+ staffList.forEach((s) => {
+ const deptId = s.deptId ?? s.dept_id;
+ const node = deptMap.get(deptId);
+ if (node) {
+ node.children.push({
+ id: s.id ?? s.staffId,
+ staffId: s.id ?? s.staffId,
+ label: s.staffName ?? s.name,
+ type: "staff",
+ ...s,
+ });
+ }
+ });
+ deptStaffTree.value = Array.from(deptMap.values()).filter(
+ (n) => n.children && n.children.length > 0
+ );
+ });
+};
+
+const openDialog = (type, row) => {
+ nextTick(() => {
+ loadDeptOptions();
+ loadBankOptions();
+ loadUserList();
+ employeeList.value = [];
+ Object.assign(form.value, {
+ id: undefined,
+ salaryTitle: "",
+ deptIds: [],
+ salaryMonth: "",
+ remark: "",
+ payBank: "",
+ auditUserId: undefined,
+ });
+ // 缂栬緫锛氬垪琛ㄩ〉宸茶繑鍥炰富琛ㄥ瓧娈碉紱杩欓噷鍙仛鍥炴樉锛堟槑缁嗙敱鈥滅敓鎴愬伐璧勮〃/璁$畻宸ヨ祫鈥濆緱鍒帮級
+ if (type === "edit" && row?.id) {
+ form.value.id = row.id;
+ form.value.salaryTitle = row.salaryTitle ?? "";
+ // deptIds 鍚庣鏄瓧绗︿覆锛堝涓敤閫楀彿鍒嗛殧锛夛紱褰撳墠琛ㄥ崟浠嶆槸鍗曢�� deptId
+ form.value.deptIds = row.deptIds
+ ? String(row.deptIds).split(",").map((id) => Number(id.trim())).filter(Boolean)
+ : [];
+ form.value.salaryMonth = row.salaryMonth ?? "";
+ form.value.remark = row.remark ?? "";
+ form.value.payBank = row.payBank ?? "";
+ form.value.auditUserId = row.auditUserId ?? undefined;
+
+ // 濡傛灉鏈夊憳宸ユ槑缁嗘暟鎹紝鐩存帴鍙嶆樉
+ if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) {
+ employeeList.value = row.staffSalaryDetailList.map((e) => ({
+ staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
+ id: e.staffOnJobId ?? e.staffId ?? e.id,
+ staffName: e.staffName ?? "",
+ postName: e.postName ?? "",
+ deptName: e.deptName ?? "",
+ basicSalary: parseNum(e.basicSalary),
+ pieceSalary: parseNum(e.pieceSalary),
+ hourlySalary: parseNum(e.hourlySalary),
+ otherIncome: parseNum(e.otherIncome),
+ socialPersonal: parseNum(e.socialPersonal),
+ fundPersonal: parseNum(e.fundPersonal),
+ otherDeduct: parseNum(e.otherDeduct),
+ salaryTax: parseNum(e.salaryTax),
+ grossSalary: parseNum(e.grossSalary),
+ deductSalary: parseNum(e.deductSalary),
+ netSalary: parseNum(e.netSalary),
+ remark: e.remark ?? "",
+ }));
+ }
+ }
+ });
+};
+
+const openAddPerson = () => {
+ loadDeptStaffTree();
+ addPersonVisible.value = true;
+ nextTick(() => {
+ personTreeRef.value?.setCheckedKeys([]);
+ });
+};
+
+const addPersonClose = () => {};
+
+const confirmAddPerson = () => {
+ const tree = personTreeRef.value;
+ if (!tree) {
+ addPersonVisible.value = false;
+ return;
+ }
+ const checked = tree.getCheckedNodes();
+ const staffNodes = checked.filter((n) => n.type === "staff");
+ const existIds = new Set(employeeList.value.map((e) => e.staffId || e.id));
+ staffNodes.forEach((node) => {
+ const id = node.staffId ?? node.id;
+ if (existIds.has(id)) return;
+ existIds.add(id);
+ employeeList.value.push({
+ staffOnJobId: id,
+ id,
+ staffName: node.label,
+ postName: node.postName ?? node.post ?? "",
+ deptName: node.deptName ?? "",
+ basicSalary: 0,
+ pieceSalary: 0,
+ hourlySalary: 0,
+ otherIncome: 0,
+ socialPersonal: 0,
+ fundPersonal: 0,
+ otherDeduct: 0,
+ salaryTax: 0,
+ grossSalary: 0,
+ deductSalary: 0,
+ netSalary: 0,
+ remark: "",
+ });
+ });
+ addPersonVisible.value = false;
+};
+
+const removeEmployee = (row) => {
+ employeeList.value = employeeList.value.filter(
+ (e) => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id)
+ );
+};
+
+const onEmployeeSelectionChange = (selection) => {
+ selectedEmployees.value = selection;
+};
+
+const handleBatchDelete = () => {
+ if (!selectedEmployees.value?.length) {
+ proxy.$modal.msgWarning("璇峰厛鍕鹃�夎鍒犻櫎鐨勫憳宸�");
+ return;
+ }
+ const ids = new Set(selectedEmployees.value.map((e) => e.staffOnJobId || e.id));
+ employeeList.value = employeeList.value.filter(
+ (e) => !ids.has(e.staffOnJobId || e.id)
+ );
+};
+
+const handleGenerate = () => {
+ if (!form.value.deptIds?.length) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨閮ㄩ棬");
+ return;
+ }
+ if (!form.value.salaryMonth) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨宸ヨ祫鏈堜唤");
+ return;
+ }
+ const payload = {
+ ids: form.value.deptIds,
+ date: form.value.salaryMonth,
+ };
+ staffSalaryMainCalculateSalary(payload).then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ if (!list.length) {
+ proxy.$modal.msgWarning("鏈绠楀埌宸ヨ祫鏁版嵁");
+ return;
+ }
+ employeeList.value = list.map((e) => ({
+ ...e,
+ staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
+ staffName: e.staffName,
+ postName: e.postName,
+ deptName: e.deptName,
+ basicSalary: parseNum(e.basicSalary),
+ pieceSalary: parseNum(e.pieceSalary),
+ hourlySalary: parseNum(e.hourlySalary),
+ otherIncome: parseNum(e.otherIncome),
+ socialPersonal: parseNum(e.socialPersonal),
+ fundPersonal: parseNum(e.fundPersonal),
+ otherDeduct: parseNum(e.otherDeduct),
+ salaryTax: parseNum(e.salaryTax),
+ grossSalary: parseNum(e.grossSalary),
+ deductSalary: parseNum(e.deductSalary),
+ netSalary: parseNum(e.netSalary),
+ remark: e.remark ?? "",
+ }));
+ proxy.$modal.msgSuccess("鐢熸垚鎴愬姛");
+ });
+};
+
+const handleClear = () => {
+ proxy.$modal.confirm("纭畾娓呯┖褰撳墠鍛樺伐鍒楄〃鍚楋紵").then(() => {
+ employeeList.value = [];
+ }).catch(() => {});
+};
+
+const handleTaxForm = () => {
+ taxDialogVisible.value = true;
+};
+
+const submitForm = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ saveData(3); // 纭鎻愪氦锛岀姸鎬佷负3锛堝緟瀹℃牳锛�
+ });
+};
+
+const saveDraft = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ saveData(1); // 淇濆瓨鑽夌锛岀姸鎬佷负1锛堣崏绋匡級
+ });
+};
+
+const saveData = (status) => {
+ const payload = {
+ id: form.value.id,
+ salaryTitle: form.value.salaryTitle,
+ deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "",
+ salaryMonth: form.value.salaryMonth,
+ remark: form.value.remark,
+ payBank: form.value.payBank,
+ auditUserId: form.value.auditUserId,
+ auditUserName: auditUserName.value,
+ totalSalary: totalSalary.value,
+ staffSalaryDetailList: employeeList.value.map((e) => ({
+ staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
+ staffName: e.staffName,
+ postName: e.postName ?? "",
+ deptName: e.deptName ?? "",
+ basicSalary: parseNum(e.basicSalary),
+ pieceSalary: parseNum(e.pieceSalary),
+ hourlySalary: parseNum(e.hourlySalary),
+ otherIncome: parseNum(e.otherIncome),
+ socialPersonal: parseNum(e.socialPersonal),
+ fundPersonal: parseNum(e.fundPersonal),
+ otherDeduct: parseNum(e.otherDeduct),
+ salaryTax: parseNum(e.salaryTax),
+ grossSalary: parseNum(e.grossSalary),
+ deductSalary: parseNum(e.deductSalary),
+ netSalary: parseNum(e.netSalary),
+ remark: e.remark ?? "",
+ })),
+ };
+ if (props.operationType === "add") {
+ staffSalaryMainAdd({ ...payload, status }).then(() => {
+ proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ } else {
+ staffSalaryMainUpdate({ ...payload, status }).then(() => {
+ proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ }
+};
+
+const closeDia = () => {
+ dialogVisible.value = false;
+ emit("close");
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped>
+.form-dia-body {
+ padding: 0;
+}
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+.form-card {
+ margin-bottom: 16px;
+}
+.form-card :deep(.el-card__header) {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+}
+.card-title {
+ font-weight: 500;
+}
+.card-collapse {
+ color: #999;
+ cursor: pointer;
+}
+.toolbar {
+ margin-bottom: 16px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+.employee-table-wrap {
+ position: relative;
+ min-height: 120px;
+}
+.table-empty {
+ text-align: center;
+ padding: 24px;
+ color: #999;
+ font-size: 14px;
+}
+.add-person-tree {
+ max-height: 360px;
+ overflow-y: auto;
+ padding: 8px 0;
+}
+.tax-desc {
+ margin-bottom: 12px;
+ font-size: 14px;
+ color: #606266;
+}
+.dialog-footer {
+ text-align: right;
+}
+.salary-total {
+ margin-top: 16px;
+ padding: 12px 16px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ text-align: right;
+ font-size: 16px;
+}
+.salary-total .total-label {
+ color: #606266;
+ margin-right: 8px;
+}
+.salary-total .total-value {
+ color: #f56c6c;
+ font-weight: bold;
+ font-size: 18px;
+}
+</style>
diff --git a/src/views/personnelManagement/monthlyStatistics/index.vue b/src/views/personnelManagement/monthlyStatistics/index.vue
new file mode 100644
index 0000000..f389f57
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/index.vue
@@ -0,0 +1,407 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">涓婚锛�</span>
+ <el-input
+ v-model="searchForm.salaryTitle"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヤ富棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ <span class="search_title ml10">鐘舵�侊細</span>
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 180px">
+ <el-option label="鑽夌" :value="1" />
+ <el-option label="瀹℃牳鏈�氳繃" :value="2" />
+ <el-option label="寰呭鏍�" :value="3" />
+ <el-option label="寰呭彂鏀�" :value="4" />
+ <el-option label="宸插彂鏀�" :value="5" />
+ </el-select>
+ <span class="search_title ml10">宸ヨ祫鏈堜唤锛�</span>
+ <el-date-picker
+ v-model="searchForm.salaryMonth"
+ type="month"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨宸ヨ祫鏈堜唤"
+ style="width: 180px"
+ clearable
+ @change="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <div style="margin-bottom: 10px">
+ <el-button type="primary" @click="openForm('add')">鏂板缓宸ヨ祫琛�</el-button>
+ <el-button @click="handleDelete">鍒犻櫎</el-button>
+ <el-button @click="openBankSetting">璁剧疆閾惰</el-button>
+ <el-button @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ :tableLoading="tableLoading"
+ @selection-change="handleSelectionChange"
+ @pagination="pagination"
+ :total="page.total"
+ />
+ </div>
+ <form-dia
+ v-model="dialogVisible"
+ :operation-type="operationType"
+ :row="currentRow"
+ ref="formDiaRef"
+ @close="handleQuery"
+ />
+ <bank-setting-dia
+ v-model="bankDialogVisible"
+ ref="bankDiaRef"
+ @saved="handleBankSaved"
+ />
+ <el-dialog v-model="issueDialogVisible" title="宸ヨ祫鍙戞斁" width="720px">
+ <el-form label-position="top">
+ <el-form-item label="鍙戞斁閾惰" required>
+ <el-select
+ v-model="issueForm.bank"
+ placeholder="璇烽�夋嫨鍙戞斁閾惰"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option v-for="b in issueBankOptions" :key="b" :label="b" :value="b" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" :loading="issueLoading" @click="confirmIssue">
+ 纭畾
+ </el-button>
+ <el-button @click="issueDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ <audit-dia
+ v-model="auditDialogVisible"
+ :row="auditRow"
+ @close="auditDialogVisible = false"
+ @success="handleAuditSuccess"
+ />
+ </div>
+</template>
+
+<script setup>
+import {
+ onMounted,
+ computed,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+} from "vue";
+import { ElMessageBox } from "element-plus";
+import Cookies from "js-cookie";
+import FormDia from "./components/formDia.vue";
+import BankSettingDia from "./components/bankSettingDia.vue";
+import AuditDia from "./components/auditDia.vue";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import { bankList } from "@/api/personnelManagement/bank.js";
+import {
+ staffSalaryMainListPage,
+ staffSalaryMainDelete,
+ staffSalaryMainUpdate,
+} from "@/api/personnelManagement/staffSalaryMain.js";
+
+const data = reactive({
+ searchForm: {
+ salaryTitle: "",
+ status: "",
+ salaryMonth: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const tableColumn = ref([
+ { label: "宸ヨ祫涓婚", prop: "salaryTitle", minWidth: 140 },
+ { label: "宸ヨ祫鏈堜唤", prop: "salaryMonth", width: 120 },
+ {
+ label: "鐘舵��",
+ prop: "statusName",
+ width: 110,
+ dataType: "tag",
+ formatType: (status) => {
+ const statusMap = {
+ "鑽夌": "info",
+ "瀹℃牳鏈�氳繃": "danger",
+ "寰呭鏍�": "warning",
+ "寰呭彂鏀�": "primary",
+ "宸插彂鏀�": "success"
+ };
+ return statusMap[status] || "info";
+ }
+ },
+ { label: "宸ヨ祫鎬婚", prop: "totalSalary", width: 120 },
+ { label: "鏀粯閾惰", prop: "payBank", width: 120 },
+ { label: "瀹℃牳浜�", prop: "auditUserName", width: 110 },
+ { label: "澶囨敞", prop: "remark", minWidth: 120 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 180,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: (row) => Number(row?.status) !== 1 && Number(row?.status) !== 2,
+ clickFun: (row) => openForm("edit", row),
+ },
+ {
+ name: "瀹℃牳",
+ type: "text",
+ disabled: (row) => Number(row?.status) !== 3,
+ clickFun: (row) => openAudit(row),
+ },
+ {
+ name: "鍙戞斁",
+ type: "text",
+ disabled: (row) => Number(row?.status) !== 4,
+ clickFun: (row) => openIssue(row),
+ },
+ ],
+ },
+]);
+
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+const formDiaRef = ref(null);
+const dialogVisible = ref(false);
+const operationType = ref("add");
+const currentRow = ref({});
+const { proxy } = getCurrentInstance();
+const bankSetting = ref({});
+const bankDialogVisible = ref(false);
+const bankDiaRef = ref(null);
+const issueDialogVisible = ref(false);
+const issueLoading = ref(false);
+const issueRow = ref(null);
+const issueForm = reactive({ bank: "" });
+const auditDialogVisible = ref(false);
+const auditRow = ref(null);
+const auditDiaRef = ref(null);
+
+const issueBankOptions = computed(() => {
+ const options = Array.isArray(bankSetting.value?.options) ? bankSetting.value.options : [];
+ return options
+ .map((v) => (v == null ? "" : String(v).trim()))
+ .filter((v) => v !== "");
+});
+
+const statusName = (s) => {
+ const n = Number(s);
+ return (
+ {
+ 1: "鑽夌",
+ 2: "瀹℃牳鏈�氳繃",
+ 3: "寰呭鏍�",
+ 4: "寰呭彂鏀�",
+ 5: "宸插彂鏀�",
+ }[n] || "-"
+ );
+};
+
+const loadBankSetting = () => {
+ return bankList().then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ const options = list
+ .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
+ .filter((v) => v !== "");
+ bankSetting.value = { options, defaultBank: "" };
+ });
+};
+
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+const handleReset = () => {
+ searchForm.value.salaryTitle = "";
+ searchForm.value.status = "";
+ searchForm.value.salaryMonth = "";
+ page.current = 1;
+ getList();
+};
+
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ staffSalaryMainListPage({
+ ...searchForm.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then((res) => {
+ tableLoading.value = false;
+ const records = res.data?.records ?? res.data?.list ?? [];
+ console.log('鍒楄〃鎺ュ彛杩斿洖鏁版嵁:', records);
+ // 鍏煎鍚庣瀛楁锛氳嫢鎺ュ彛浠嶈繑鍥炲彴璐︾粨鏋勶紝鍙湪姝ゅ仛鏄犲皠
+ tableData.value = records.map((item) => ({
+ ...item,
+ salaryTitle: item.salaryTitle ?? "-",
+ salaryMonth: item.salaryMonth ?? "-",
+ statusName: statusName(item.status),
+ totalSalary: item.totalSalary ?? "-",
+ payBank: (item.payBank == null ? "" : String(item.payBank).trim()) || "-",
+ auditUserName: item.auditUserName ?? "-",
+ }));
+ page.total = res.data?.total ?? res.data?.count ?? 0;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+const openForm = (type, row) => {
+ operationType.value = type;
+ currentRow.value = row || {};
+ dialogVisible.value = true;
+ nextTick(() => {
+ formDiaRef.value?.openDialog(type, row);
+ });
+};
+
+const openBankSetting = () => {
+ bankDialogVisible.value = true;
+};
+
+const openAudit = (row) => {
+ console.log('鎵撳紑瀹℃牳锛屼紶鍏ョ殑鏁版嵁:', row);
+ auditRow.value = row || null;
+ auditDialogVisible.value = true;
+ nextTick(() => {
+ auditDiaRef.value?.openDialog(row);
+ });
+};
+
+const handleAuditSuccess = () => {
+ getList();
+};
+
+const openIssue = (row) => {
+ if (!issueBankOptions.value?.length) {
+ proxy?.$modal?.msgWarning?.("璇峰厛鍦ㄢ�滆缃摱琛屸�濅腑缁存姢鍙戞斁閾惰閫夐」");
+ return;
+ }
+ issueRow.value = row || null;
+ const current = row?.payBank && row.payBank !== "-" ? String(row.payBank).trim() : "";
+ issueForm.bank = current;
+ issueDialogVisible.value = true;
+};
+
+
+
+const confirmIssue = () => {
+ const bank = issueForm.bank ? String(issueForm.bank).trim() : "";
+ if (!bank) {
+ proxy?.$modal?.msgWarning?.("璇烽�夋嫨鍙戞斁閾惰");
+ return;
+ }
+ const row = issueRow.value;
+ if (!row?.id) {
+ issueDialogVisible.value = false;
+ return;
+ }
+ issueLoading.value = true;
+ staffSalaryMainUpdate({
+ id: row.id,
+ payBank: bank,
+ status: 5,
+ })
+ .then(() => {
+ proxy?.$modal?.msgSuccess?.("鍙戞斁鎴愬姛");
+ issueDialogVisible.value = false;
+ getList();
+ })
+ .finally(() => {
+ issueLoading.value = false;
+ });
+};
+
+const handleBankSaved = () => {
+ loadBankSetting();
+ getList();
+};
+
+const handleDelete = () => {
+ if (!selectedRows.value?.length) {
+ proxy.$modal.msgWarning("璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map((item) => item.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ staffSalaryMainDelete(ids).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {});
+};
+
+const handleExport = () => {
+ proxy.download(
+ "/compensationPerformance/export",
+ { ...searchForm.value, current: page.current, size: page.size },
+ "宸ヨ祫琛�.xlsx"
+ );
+};
+
+onMounted(() => {
+ loadBankSetting();
+ getList();
+});
+</script>
+
+<style scoped>
+.search_form {
+ margin-bottom: 20px;
+}
+.search_title {
+ font-weight: 500;
+ margin-right: 5px;
+}
+.ml10 {
+ margin-left: 10px;
+}
+.table_list {
+ margin-top: 20px;
+}
+</style>
diff --git a/src/views/personnelManagement/socialSecuritySet/components/formDia.vue b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
new file mode 100644
index 0000000..71f5e61
--- /dev/null
+++ b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
@@ -0,0 +1,470 @@
+<template>
+ <div>
+ <FormDialog
+ v-model="dialogFormVisible"
+ :operation-type="operationType"
+ :title="dialogTitle"
+ width="80%"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia"
+ >
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+ <el-row :gutter="24">
+ <!-- 宸︿晶锛氶�傜敤浜哄憳 -->
+ <el-col :span="8">
+ <el-form-item label="閫傜敤浜哄憳锛�" prop="deptIds">
+ <div class="dept-checkbox-wrap">
+ <el-checkbox-group
+ v-model="form.deptIds"
+ :disabled="isDetail"
+ >
+ <div
+ v-for="dept in deptList"
+ :key="dept.deptId"
+ class="dept-checkbox-item"
+ >
+ <el-checkbox :value="dept.deptId">
+ {{ dept.deptName }}
+ <span v-if="dept.personCount != null" class="dept-count"
+ >{{ dept.personCount }}浜�</span
+ >
+ </el-checkbox>
+ </div>
+ </el-checkbox-group>
+ </div>
+ </el-form-item>
+ </el-col>
+ <!-- 鍙充晶锛氬熀纭�淇℃伅 + 淇濋櫓绫诲瀷 -->
+ <el-col :span="16">
+ <!-- 鍩虹淇℃伅 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title"><span class="card-title-line">|</span> 鍩虹淇℃伅</span>
+ <el-icon class="card-collapse"><ArrowUp /></el-icon>
+ </template>
+ <el-form-item label="鏂规鏍囬锛�" prop="title">
+ <el-input
+ v-model="form.title"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="isDetail"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞锛�" prop="remark">
+ <el-input
+ v-model="form.remark"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="isDetail"
+ />
+ </el-form-item>
+ </el-card>
+
+ <!-- 淇濋櫓绫诲瀷 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title"><span class="card-title-line">|</span> 淇濋櫓绫诲瀷</span>
+ <el-button
+ v-if="!isDetail"
+ type="primary"
+ size="small"
+ @click="addInsuranceBenefit"
+ >
+ 娣诲姞淇濋櫓绂忓埄
+ </el-button>
+ </template>
+ <el-row :gutter="16">
+ <el-col
+ v-for="(item, index) in form.insuranceBenefits"
+ :key="item._key"
+ :span="12"
+ >
+ <div class="insurance-benefit-card">
+ <div class="insurance-benefit-title">
+ 淇濋櫓绂忓埄{{ index + 1 }}
+ <el-button
+ v-if="!isDetail && form.insuranceBenefits.length > 1"
+ type="danger"
+ link
+ size="small"
+ class="card-delete-btn"
+ @click="removeInsuranceBenefit(index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <el-form-item
+ :prop="'insuranceBenefits.' + index + '.insuranceType'"
+ label="淇濋櫓绫诲瀷锛�"
+ label-width="100px"
+ >
+ <el-select
+ v-model="item.insuranceType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ :disabled="isDetail"
+ >
+ <el-option
+ v-for="opt in insuranceTypeOptions"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缂磋垂鍩烘暟锛�" label-width="100px">
+ <div class="base-salary-wrap">
+ <el-input
+ v-model="item.paymentBase"
+ placeholder="鏍规嵁鍩烘湰宸ヨ祫缂寸撼"
+ clearable
+ style="width: 120px"
+ type="number"
+ :disabled="isDetail || item.useBasicSalary"
+ @input="handlePaymentBaseInput(item)"
+ />
+ <el-checkbox
+ v-model="item.useBasicSalary"
+ @change="handleUseBasicSalaryChange(item)"
+ :disabled="isDetail"
+ >
+ 璋冪敤鍩烘湰宸ヨ祫
+ </el-checkbox>
+ </div>
+ </el-form-item>
+ <el-form-item label="涓汉缂磋垂姣斾緥锛�" label-width="100px">
+ <div class="personal-ratio-wrap">
+ <el-input
+ v-model="item.personalRatio"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 100px"
+ type="number"
+ :disabled="isDetail"
+ />
+ <span class="ratio-unit">(%)</span>
+ <span class="ratio-plus">+</span>
+ <el-input
+ v-model="item.personalFixed"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 100px"
+ type="number"
+ :disabled="isDetail"
+ />
+ </div>
+ </el-form-item>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { ArrowUp } from "@element-plus/icons-vue";
+import { listDept } from "@/api/system/dept.js";
+import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js";
+
+const emit = defineEmits(["close"]);
+const { proxy } = getCurrentInstance();
+
+const dialogFormVisible = ref(false);
+const operationType = ref("add");
+const formRef = ref(null);
+const deptList = ref([]);
+
+const isDetail = computed(() => operationType.value === "detail");
+
+const dialogTitle = () =>
+ operationType.value === "add"
+ ? "鏂板鏂规"
+ : operationType.value === "edit"
+ ? "缂栬緫鏂规"
+ : "鏂规璇︽儏";
+
+// 淇濋櫓绫诲瀷閫夐」锛堝彲鎸夊瓧鍏告浛鎹級
+const insuranceTypeOptions = [
+ { label: "鍏昏�佷繚闄�", value: "鍏昏�佷繚闄�" },
+ { label: "鍖荤枟淇濋櫓", value: "鍖荤枟淇濋櫓" },
+ { label: "澶变笟淇濋櫓", value: "澶变笟淇濋櫓" },
+ { label: "宸ヤ激淇濋櫓", value: "宸ヤ激淇濋櫓" },
+ { label: "鐢熻偛淇濋櫓", value: "鐢熻偛淇濋櫓" },
+ { label: "鍏Н閲�", value: "鍏Н閲�" },
+];
+
+const defaultBenefit = () => ({
+ _key: Math.random().toString(36).slice(2),
+ insuranceType: "",
+ paymentBase: "",
+ useBasicSalary: false,
+ personalRatio: "",
+ personalFixed: "",
+});
+
+const data = reactive({
+ form: {
+ id: undefined,
+ title: "",
+ remark: "",
+ deptIds: [],
+ insuranceBenefits: [defaultBenefit()],
+ },
+ rules: {
+ title: [{ required: true, message: "璇疯緭鍏ユ柟妗堟爣棰�", trigger: "blur" }],
+ deptIds: [
+ {
+ required: true,
+ type: "array",
+ min: 1,
+ message: "璇疯嚦灏戦�夋嫨涓�涓�傜敤閮ㄩ棬",
+ trigger: "change",
+ },
+ ],
+ },
+});
+const { form, rules } = toRefs(data);
+
+function flattenDept(tree, list = []) {
+ if (!tree || !tree.length) return list;
+ tree.forEach((node) => {
+ list.push({
+ deptId: node.deptId,
+ deptName: node.deptName,
+ personCount: node.personCount ?? null,
+ });
+ if (node.children && node.children.length) {
+ flattenDept(node.children, list);
+ }
+ });
+ return list;
+}
+
+const loadDeptList = () => {
+ listDept().then((res) => {
+ const tree = res.data ?? [];
+ deptList.value = flattenDept(tree);
+ });
+};
+
+const addInsuranceBenefit = () => {
+ form.value.insuranceBenefits.push(defaultBenefit());
+};
+
+const removeInsuranceBenefit = (index) => {
+ form.value.insuranceBenefits.splice(index, 1);
+};
+
+const handleUseBasicSalaryChange = (item) => {
+ if (item.useBasicSalary) {
+ item.paymentBase = "";
+ }
+};
+
+const handlePaymentBaseInput = (item) => {
+ if (item.paymentBase !== "" && item.paymentBase != null) {
+ item.useBasicSalary = false;
+ }
+};
+
+const resetForm = () => {
+ form.value = {
+ id: undefined,
+ title: "",
+ remark: "",
+ deptIds: [],
+ insuranceBenefits: [defaultBenefit()],
+ };
+};
+
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ loadDeptList();
+ resetForm();
+ if ((type === "edit" || type === "detail") && row) {
+ const d = row || {};
+ form.value.id = d.id;
+ form.value.title = d.title;
+ form.value.remark = d.remark ?? "";
+ // deptIds 鍚庣鍙兘鏄�楀彿鍒嗛殧瀛楃涓叉垨鏁扮粍锛岃繖閲岀粺涓�杞负鏁扮粍骞跺敖閲忚繕鍘熸暟鍊肩被鍨�
+ if (d.deptIds) {
+ form.value.deptIds = String(d.deptIds)
+ .split(",")
+ .filter((v) => v !== "")
+ .map((v) => {
+ const num = Number(v);
+ return Number.isNaN(num) ? v : num;
+ });
+ } else {
+ form.value.deptIds = [];
+ }
+ const detailList = d.schemeInsuranceDetailList || [];
+ form.value.insuranceBenefits =
+ detailList.length > 0
+ ? detailList.map((b) => ({
+ _key: Math.random().toString(36).slice(2),
+ insuranceType: b.insuranceType || "",
+ paymentBase: b.paymentBase ?? "",
+ useBasicSalary: b.useBasicSalary === 2,
+ personalRatio: b.personalRatio ?? "",
+ personalFixed: b.personalFixed ?? "",
+ }))
+ : [defaultBenefit()];
+ }
+};
+
+const submitForm = () => {
+ // 璇︽儏妯″紡涓嬩笉鎻愪氦锛屽彧鍏抽棴寮圭獥
+ if (operationType.value === "detail") {
+ closeDia();
+ return;
+ }
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ const deptIds =
+ Array.isArray(form.value.deptIds) && form.value.deptIds.length
+ ? form.value.deptIds.join(",")
+ : "";
+ const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map(
+ ({ _key, ...rest }) => ({
+ ...rest,
+ useBasicSalary: rest.useBasicSalary ? 2 : 1,
+ })
+ );
+ const insuranceTypes = schemeInsuranceDetailList
+ .map((item) => item.insuranceType)
+ .filter((v) => v)
+ .join(",");
+ // 閮ㄩ棬鍚嶇О锛屽涓娇鐢ㄩ�楀彿闅斿紑锛堟牴鎹�変腑鐨� deptIds 涓� deptList 璁$畻锛�
+ const deptNames = (deptList.value || [])
+ .filter((d) =>
+ (form.value.deptIds || []).some(
+ (id) => String(id) === String(d.deptId)
+ )
+ )
+ .map((d) => d.deptName)
+ .join(",");
+ const submitData = {
+ id: form.value.id,
+ title: form.value.title,
+ remark: form.value.remark ?? "",
+ deptIds,
+ insuranceTypes,
+ deptNames,
+ schemeInsuranceDetailList,
+ };
+ if (operationType.value === "add") {
+ socialSecurityAdd(submitData).then(() => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ closeDia();
+ });
+ } else {
+ socialSecurityUpdate(submitData).then(() => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeDia();
+ });
+ }
+ });
+};
+
+const closeDia = () => {
+ proxy.resetForm?.("formRef");
+ dialogFormVisible.value = false;
+ emit("close");
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped>
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+.form-card {
+ margin-bottom: 16px;
+}
+.form-card :deep(.el-card__header) {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+}
+.card-title {
+ font-weight: 500;
+}
+.card-collapse {
+ color: #999;
+ cursor: pointer;
+}
+.dept-checkbox-wrap {
+ max-height: 320px;
+ overflow-y: auto;
+ padding: 8px 0;
+ border: 1px solid var(--el-border-color);
+ border-radius: 4px;
+ background: #fff;
+}
+.dept-checkbox-item {
+ padding: 6px 12px;
+}
+.dept-count {
+ color: #909399;
+ font-size: 12px;
+ margin-left: 4px;
+}
+.insurance-benefit-card {
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: 4px;
+ padding: 12px 16px;
+ margin-bottom: 12px;
+ background: #fafafa;
+}
+.insurance-benefit-title {
+ font-size: 14px;
+ margin-bottom: 12px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.card-delete-btn {
+ margin-left: auto;
+}
+.checkbox-group-inline {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+}
+.base-salary-wrap {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+.base-salary-text {
+ color: #606266;
+ font-size: 14px;
+}
+.personal-ratio-wrap {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.ratio-unit,
+.ratio-plus {
+ color: #606266;
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/personnelManagement/socialSecuritySet/index.vue b/src/views/personnelManagement/socialSecuritySet/index.vue
new file mode 100644
index 0000000..2a1ff65
--- /dev/null
+++ b/src/views/personnelManagement/socialSecuritySet/index.vue
@@ -0,0 +1,212 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">涓婚锛�</span>
+ <el-input
+ v-model="searchForm.title"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヤ富棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <div style="margin-bottom: 10px; display: flex; gap: 10px">
+ <el-button type="primary" @click="openForm('add')">鏂板鏂规</el-button>
+ <el-button
+ type="danger"
+ @click="handleBatchDelete"
+ :disabled="selectedRows.length === 0"
+ >
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ :total="page.total"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="pagination"
+ />
+ </div>
+ <form-dia ref="formDiaRef" @close="handleQuery" />
+ </div>
+</template>
+
+<script setup>
+import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import FormDia from "./components/formDia.vue";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import {
+ socialSecurityListPage,
+ socialSecurityDelete,
+} from "@/api/personnelManagement/socialSecuritySet.js";
+
+const data = reactive({
+ searchForm: {
+ title: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const tableColumn = ref([
+ { label: "涓婚", prop: "title", minWidth: 120 },
+ { label: "淇濋櫓绫诲瀷", prop: "insuranceTypes", width: 120 },
+ { label: "浣跨敤鑼冨洿", prop: "deptNames", width: 120 },
+ { label: "澶囨敞", prop: "remark", minWidth: 120 },
+ { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 160 },
+ { label: "鍒涘缓浜�", prop: "createUserName", width: 100 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 180,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => openForm("edit", row),
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => openForm("detail", row),
+ },
+ {
+ name: "鍒犻櫎",
+ type: "text",
+ style: {
+ color: "#F56C6C",
+ },
+ clickFun: (row) => handleDelete(row),
+ },
+ ],
+ },
+]);
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+const selectedRows = ref([]);
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+const formDiaRef = ref(null);
+
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+const handleReset = () => {
+ searchForm.value.title = "";
+ page.current = 1;
+ getList();
+};
+
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ socialSecurityListPage({
+ ...searchForm.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data?.records ?? [];
+ page.total = res.data?.total ?? 0;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDiaRef.value?.openDialog(type, row);
+ });
+};
+
+// 鍒犻櫎鏂规锛岄�昏緫涓庡叾瀹冮〉闈繚鎸佷竴鑷达紙纭寮圭獥 + 璋冪敤鍒犻櫎鎺ュ彛 + 鍒锋柊鍒楄〃锛�
+const handleDelete = (row) => {
+ ElMessageBox.confirm(
+ `纭鍒犻櫎鏂规"${row.title}"鍚楋紵`,
+ "鍒犻櫎纭",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ socialSecurityDelete([row.id])
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+// 鎵归噺鍒犻櫎
+const handleBatchDelete = () => {
+ if (!selectedRows.value.length) return;
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉℃柟妗堝悧锛焋,
+ "鎵归噺鍒犻櫎纭",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ const ids = selectedRows.value.map((item) => item.id);
+ socialSecurityDelete(ids)
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/procurementManagement/invoiceEntry/components/Modal.vue b/src/views/procurementManagement/invoiceEntry/components/Modal.vue
index b723175..33d5144 100644
--- a/src/views/procurementManagement/invoiceEntry/components/Modal.vue
+++ b/src/views/procurementManagement/invoiceEntry/components/Modal.vue
@@ -133,10 +133,15 @@
/>
<el-table-column label="鏈寮�绁ㄦ暟" prop="ticketsNum" width="180">
<template #default="scope">
- <el-input-number :step="0.1" :min="0" :max="scope.row.tempFutureTickets || 0" style="width: 100%"
- :precision="2"
- v-model="scope.row.ticketsNum"
- @change="invoiceNumBlur(scope.row)"
+ <el-input-number
+ :step="0.1"
+ :min="0"
+ :max="scope.row.tempFutureTickets || 0"
+ style="width: 100%"
+ :precision="2"
+ v-model="scope.row.ticketsNum"
+ :disabled="isProductDisabled(scope.row)"
+ @change="invoiceNumBlur(scope.row)"
/>
</template>
</el-table-column>
@@ -146,10 +151,14 @@
width="180"
>
<template #default="scope">
- <el-input-number :step="0.01" :min="0" style="width: 100%"
- :precision="2"
- v-model="scope.row.ticketsAmount"
- @change="invoiceAmountBlur(scope.row)"
+ <el-input-number
+ :step="0.01"
+ :min="0"
+ style="width: 100%"
+ :precision="2"
+ v-model="scope.row.ticketsAmount"
+ :disabled="isProductDisabled(scope.row)"
+ @change="invoiceAmountBlur(scope.row)"
/>
</template>
</el-table-column>
@@ -402,7 +411,7 @@
// 璁剧疆浜у搧鏁版嵁锛屽苟鍒濆鍖栧紑绁ㄦ暟閲忓拰閲戦
allProductData.forEach(item => {
- // 淇濆瓨鈥滃師濮嬫湭鏉ョエ鏁�/閲戦鈥濓紙鐢ㄤ簬鏍¢獙涓庤绠楋級
+ // 淇濆瓨"鍘熷鏈潵绁ㄦ暟/閲戦"锛堢敤浜庢牎楠屼笌璁$畻锛�
// 浼樺厛浣跨敤鍚庣杩斿洖鐨� futureTickets/futureTicketsAmount锛涙病鏈夊垯鍥為��鍒� quantity/taxInclusiveTotalPrice
item.tempFutureTickets = Number(
item.futureTickets !== undefined ? item.futureTickets : (item.quantity || 0)
@@ -411,15 +420,23 @@
item.futureTicketsAmount !== undefined ? item.futureTicketsAmount : (item.taxInclusiveTotalPrice || 0)
);
- // 鏂板鏃讹細鏈寮�绁ㄦ暟榛樿 = 鏈潵绁ㄦ暟锛堜笖涓嶈兘澶т簬鏈潵绁ㄦ暟锛�
- item.ticketsNum = Number(item.tempFutureTickets || 0);
- // 鑱斿姩璁$畻鏈寮�绁ㄩ噾棰濄�佹湭鏉ョエ鏁般�佹湭鏉ョエ閲戦
- const unitPrice = Number(item.taxInclusiveUnitPrice || 0);
- item.ticketsAmount = Number((item.ticketsNum * unitPrice).toFixed(2));
- item.futureTickets = Number((item.tempFutureTickets - item.ticketsNum).toFixed(2));
- item.futureTicketsAmount = Number(
- (item.tempFutureTicketsAmount - item.ticketsAmount).toFixed(2)
- );
+ // 濡傛灉鏈潵绁ㄩ噾棰濅负0锛屽垯鏈寮�绁ㄦ暟鍜岄噾棰濋兘璁剧疆涓�0
+ if (item.tempFutureTicketsAmount <= 0) {
+ item.ticketsNum = 0;
+ item.ticketsAmount = 0;
+ item.futureTickets = Number(item.tempFutureTickets || 0);
+ item.futureTicketsAmount = 0;
+ } else {
+ // 鏂板鏃讹細鏈寮�绁ㄦ暟榛樿 = 鏈潵绁ㄦ暟锛堜笖涓嶈兘澶т簬鏈潵绁ㄦ暟锛�
+ item.ticketsNum = Number(item.tempFutureTickets || 0);
+ // 鑱斿姩璁$畻鏈寮�绁ㄩ噾棰濄�佹湭鏉ョエ鏁般�佹湭鏉ョエ閲戦
+ const unitPrice = Number(item.taxInclusiveUnitPrice || 0);
+ item.ticketsAmount = Number((item.ticketsNum * unitPrice).toFixed(2));
+ item.futureTickets = Number((item.tempFutureTickets - item.ticketsNum).toFixed(2));
+ item.futureTicketsAmount = Number(
+ (item.tempFutureTicketsAmount - item.ticketsAmount).toFixed(2)
+ );
+ }
});
form.productData = allProductData;
@@ -435,15 +452,45 @@
});
} else if (type == "edit") {
const id = Array.isArray(selectedRows) ? selectedRows[0].id : selectedRows;
- const data = await getPurchaseById({ id, type: 2 });
- form.purchaseLedgerNo = data.purchaseContractNumber;
+ const response = await getPurchaseById({ id, type: 2 });
+ // 鍏煎涓嶅悓鐨勮繑鍥炴牸寮忥細鍙兘鏄� { code, data } 鎴栫洿鎺ヨ繑鍥炴暟鎹�
+ const data = response.data || response;
+
+ // 鍏煎涓嶅悓鐨勫瓧娈靛悕锛歱urchaseContractNumber 鎴� purchaseLedgerNo
+ form.purchaseLedgerNo = data.purchaseContractNumber || data.purchaseLedgerNo || "";
form.invoiceAmount = data.invoiceAmount;
form.invoiceNumber = data.invoiceNumber;
form.salesContractNo = data.salesContractNo;
form.projectName = data.projectName;
form.supplierName = data.supplierName;
form.entryDate = data.entryDate;
- form.productData = data.productData;
+ form.enterDate = data.enterDate || dayjs().format("YYYY-MM-DD");
+
+ // 缂栬緫鏃朵篃闇�瑕佸垵濮嬪寲浜у搧鏁版嵁鐨� tempFutureTickets 鍜� tempFutureTicketsAmount
+ // 鍚屾椂涓烘瘡涓骇鍝佹坊鍔犲悎鍚屽彿绛変俊鎭�
+ const contractNumber = data.purchaseContractNumber || data.purchaseLedgerNo || "";
+ if (data.productData && Array.isArray(data.productData)) {
+ data.productData.forEach(item => {
+ // 淇濆瓨"鍘熷鏈潵绁ㄦ暟/閲戦"锛堢敤浜庢牎楠屼笌璁$畻锛�
+ // 浼樺厛浣跨敤鍚庣杩斿洖鐨� futureTickets/futureTicketsAmount锛涙病鏈夊垯鍥為��鍒� quantity/taxInclusiveTotalPrice
+ item.tempFutureTickets = Number(
+ item.futureTickets !== undefined ? item.futureTickets : (item.quantity || 0)
+ );
+ item.tempFutureTicketsAmount = Number(
+ item.futureTicketsAmount !== undefined ? item.futureTicketsAmount : (item.taxInclusiveTotalPrice || 0)
+ );
+
+ // 纭繚姣忎釜浜у搧閮芥湁鍚堝悓鍙凤紝鐢ㄤ簬鏄剧ず鍦�"鎵�灞炲悎鍚�"鍒�
+ if (!item.purchaseLedgerNo) {
+ item.purchaseLedgerNo = contractNumber;
+ }
+ });
+ }
+
+ form.productData = data.productData || [];
+
+ // 缂栬緫妯″紡涓嬶紝鏍规嵁浜у搧鏁版嵁涓殑鏈寮�绁ㄩ噾棰濊嚜鍔ㄨ绠楀彂绁ㄩ噾棰�
+ calculateinvoiceAmount();
}
};
// 瀛愯〃鍚堣鏂规硶
@@ -515,22 +562,45 @@
form.invoiceAmount = Number(invoiceAmountTotal.toFixed(2));
};
-const open = async (type, selectedRows) => {
- visible.value = true;
+// 鍒ゆ柇浜у搧鏄惁鍙互缁х画鏉ョエ鎿嶄綔锛氬鏋滄湭鏉ョエ鏁板拰鏈潵绁ㄩ噾棰濋兘涓�0鎴栧皬浜庣瓑浜�0锛屽垯绂佺敤
+const isProductDisabled = (row) => {
+ // 浼樺厛浣跨敤 tempFutureTickets锛堝師濮嬫湭鏉ョエ鏁帮級锛屽鏋滄病鏈夊垯浣跨敤 futureTickets
+ const futureTickets = Number(row.tempFutureTickets !== undefined
+ ? row.tempFutureTickets
+ : (row.futureTickets !== undefined ? row.futureTickets : 0));
- // 濡傛灉鏄壒閲忔搷浣滐紝璁剧疆鏍囬
- if (Array.isArray(selectedRows) && selectedRows.length > 1) {
- modalOptions.value = {
- ...(modalOptions.value || {}),
- title: `鎵归噺鏂板 (${selectedRows.length}鏉�)`,
- };
- } else {
- modalOptions.value = {
- ...(modalOptions.value || {}),
- title: type === "add" ? "鏂板" : "缂栬緫",
- };
+ // 浼樺厛浣跨敤 tempFutureTicketsAmount锛堝師濮嬫湭鏉ョエ閲戦锛夛紝濡傛灉娌℃湁鍒欎娇鐢� futureTicketsAmount
+ const futureAmount = Number(row.tempFutureTicketsAmount !== undefined
+ ? row.tempFutureTicketsAmount
+ : (row.futureTicketsAmount !== undefined ? row.futureTicketsAmount : 0));
+
+ // 鍙湁褰撴湭鏉ョエ鏁板拰鏈潵绁ㄩ噾棰濋兘涓�0鎴栧皬浜庣瓑浜�0鏃讹紝鎵嶇鐢�
+ return futureTickets <= 0 && futureAmount <= 0;
+};
+
+const open = async (type, selectedRows) => {
+ // 纭繚 modalOptions.value 鏄璞�
+ if (!modalOptions.value || typeof modalOptions.value !== 'object') {
+ modalOptions.value = {};
}
+ // 鏍规嵁鎿嶄綔绫诲瀷鍜岄�変腑鏁版嵁璁剧疆鏍囬
+ if (Array.isArray(selectedRows) && selectedRows.length > 1) {
+ // 鎵归噺鎿嶄綔
+ modalOptions.value.title = type === "add" ? `鎵归噺鏂板 (${selectedRows.length}鏉�)` : `鎵归噺缂栬緫 (${selectedRows.length}鏉�)`;
+ } else {
+ // 鍗曚釜鎿嶄綔 - 鏄庣‘鍒ゆ柇 type 鐨勫��
+ if (type === "add" || type === "鏂板") {
+ modalOptions.value.title = "鏂板";
+ } else if (type === "edit" || type === "缂栬緫") {
+ modalOptions.value.title = "缂栬緫";
+ } else {
+ modalOptions.value.title = "鏉ョエ鐧昏"; // 榛樿鏍囬
+ }
+ }
+
+ visible.value = true;
+
// 濡傛灉鏄崟涓搷浣滐紝鑾峰彇id
if (!Array.isArray(selectedRows) || selectedRows.length === 1) {
const idValue = Array.isArray(selectedRows) ? selectedRows[0].id : selectedRows;
diff --git a/src/views/procurementManagement/invoiceEntry/index.vue b/src/views/procurementManagement/invoiceEntry/index.vue
index 00a6881..8c435e0 100644
--- a/src/views/procurementManagement/invoiceEntry/index.vue
+++ b/src/views/procurementManagement/invoiceEntry/index.vue
@@ -39,7 +39,7 @@
<div></div>
<div>
<el-button @click="handleExport" style="margin-right: 10px">瀵煎嚭</el-button>
- <el-button type="primary" @click="handleAdd('add')">
+ <el-button type="primary" @click="handleAdd('add')" :disabled="isInvoiceDisabled">
鏉ョエ鐧昏
</el-button>
<!-- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>-->
@@ -84,7 +84,7 @@
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
import {delRegistration, gePurchaseListPage} from "@/api/procurementManagement/invoiceEntry.js";
-import { nextTick, onMounted, getCurrentInstance, ref } from "vue";
+import { nextTick, onMounted, getCurrentInstance, ref, computed } from "vue";
import ExpandTable from "./components/ExpandTable.vue";
import Modal from "./components/Modal.vue";
import {ElMessageBox} from "element-plus";
@@ -186,6 +186,18 @@
);
};
+// 璁$畻鏄惁鍙互鏉ョエ鐧昏锛氬鏋滄墍鏈夐�変腑琛岀殑寰呮潵绁ㄩ噾棰濋兘涓�0锛屽垯绂佺敤鎸夐挳
+const isInvoiceDisabled = computed(() => {
+ if (selectedRows.value.length === 0) {
+ return true;
+ }
+ // 濡傛灉鎵�鏈夐�変腑琛岀殑寰呮潵绁ㄩ噾棰濋兘涓�0鎴栧皬浜庣瓑浜�0锛屽垯绂佺敤
+ return selectedRows.value.every(row => {
+ const amount = parseFloat(row.unReceiptPaymentAmount || 0);
+ return amount <= 0;
+ });
+});
+
const handleAdd = (type) => {
if (selectedRows.value.length < 1) {
proxy.$modal.msgWarning("璇疯嚦灏戦�変腑涓�鏉℃暟鎹�");
diff --git a/src/views/procurementManagement/procurementInvoiceLedger/index.vue b/src/views/procurementManagement/procurementInvoiceLedger/index.vue
index 00ae65a..d82e3e7 100644
--- a/src/views/procurementManagement/procurementInvoiceLedger/index.vue
+++ b/src/views/procurementManagement/procurementInvoiceLedger/index.vue
@@ -237,21 +237,32 @@
}
);
-// 涓昏〃鍚堣鏂规硶
const summarizeMainTable = (param) => {
- return proxy.summarizeTable(
+ const sums = proxy.summarizeTable(
param,
- [
- "taxInclusiveTotalPrice",
- "ticketsAmount",
- "unTicketsPrice",
- "invoiceAmount",
- ],
+ ["ticketsAmount", "unTicketsPrice", "invoiceAmount"],
{
- ticketsNum: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
- futureTickets: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ ticketsNum: { noDecimal: true },
+ futureTickets: { noDecimal: true },
}
);
+
+ const keySet = new Set();
+ let taxInclusiveSum = 0;
+ (param.data || []).forEach((row) => {
+ const key = `${row.purchaseContractNumber ?? ""}\n${row.salesContractNo ?? ""}\n${row.productCategory ?? ""}\n${row.specificationModel ?? ""}`;
+ if (keySet.has(key)) return;
+ keySet.add(key);
+ const val = Number(row.taxInclusiveTotalPrice);
+ if (!isNaN(val)) taxInclusiveSum += val;
+ });
+ const taxInclusiveIndex = (param.columns || []).findIndex(
+ (c) => c.property === "taxInclusiveTotalPrice"
+ );
+ if (taxInclusiveIndex !== -1) {
+ sums[taxInclusiveIndex] = taxInclusiveSum.toFixed(2);
+ }
+ return sums;
};
const handleSelectionChange = (val) => {
diff --git a/src/views/procurementManagement/procurementLedger/index.vue b/src/views/procurementManagement/procurementLedger/index.vue
index a1fcdfb..20c6b1c 100644
--- a/src/views/procurementManagement/procurementLedger/index.vue
+++ b/src/views/procurementManagement/procurementLedger/index.vue
@@ -233,7 +233,7 @@
<el-option v-for="item in supplierList"
:key="item.id"
:label="item.supplierName"
- :value="item.id" />
+ :value="item.id" >{{item.supplierName + '---' + item.supplierType}}</el-option>
</el-select>
</el-form-item>
</el-col>
diff --git a/src/views/procurementManagement/purchaseReturnOrder/New.vue b/src/views/procurementManagement/purchaseReturnOrder/New.vue
new file mode 100644
index 0000000..d2fe2a9
--- /dev/null
+++ b/src/views/procurementManagement/purchaseReturnOrder/New.vue
@@ -0,0 +1,618 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鏂板閲囪喘閫�璐�"
+ width="1600"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef" :inline="true">
+ <div class="section-title">
+ <span class="title-dot"></span>
+ <span class="title-text">鍩烘湰淇℃伅</span>
+ </div>
+ <el-form-item
+ label="閫�鏂欏崟鍙�"
+ prop="no"
+ :rules="[
+ {
+ required: !formState.isDefaultNo,
+ message: '璇疯緭鍏ラ��鏂欏崟鍙�',
+ trigger: 'blur',
+ }
+ ]"
+ >
+ <el-input
+ v-model="formState.no"
+ :placeholder="formState.isDefaultNo ? '浣跨敤绯荤粺缂栧彿' : '璇疯緭鍏ラ��鏂欏崟鍙�'"
+ :disabled="formState.isDefaultNo"
+ >
+ <template #append>
+ <el-checkbox v-model="formState.isDefaultNo" size="large" @change="handleChangeIsDefaultNo" />
+ </template>
+ </el-input>
+ </el-form-item>
+
+ <el-form-item
+ label="閫�璐ф柟寮�"
+ prop="returnType"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨閫�璐ф柟寮�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.returnType"
+ placeholder="璇烽�夋嫨閫�璐ф柟寮�"
+ style="width: 240px"
+ >
+ <el-option label="閫�璐ч��娆�" :value="0" />
+ <el-option label="鎷掓敹" :value="1" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="渚涘簲鍟嗗悕绉�"
+ prop="supplierId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨渚涘簲鍟�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 240px"
+ @focus="fetchSupplierOptions"
+ @change="handleChangeSupplierId"
+ >
+ <el-option
+ v-for="item in supplierOptions"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="椤圭洰"
+ prop="projectId"
+ >
+ <el-select
+ v-model="formState.projectId"
+ placeholder="璇烽�夋嫨椤圭洰"
+ style="width: 240px"
+ @focus="fetchProjectOptions"
+ >
+ <el-option
+ v-for="item in projectOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="椤圭洰闃舵"
+ prop="projectPhase"
+ >
+ <el-select
+ v-model="formState.projectPhase"
+ placeholder="璇烽�夋嫨椤圭洰闃舵"
+ style="width: 240px"
+ >
+ <el-option
+ v-for="item in projectStageOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="鍒朵綔鏃ユ湡"
+ prop="preparedAt"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鍒朵綔鏃ユ湡',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-date-picker
+ v-model="formState.preparedAt"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨鍒朵綔鏃ユ湡"
+ style="width: 240px"
+ clearable />
+ </el-form-item>
+
+ <el-form-item
+ label="鍒跺崟浜猴細"
+ prop="preparedUserId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鍒跺崟浜�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.preparedUserId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ style="width: 240px"
+ @focus="fetchUserOptions"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="閫�鏂欎汉锛�"
+ prop="returnUserId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨閫�鏂欎汉',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.returnUserId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ default-first-option
+ style="width: 240px"
+ :reserve-keyword="false"
+ @focus="fetchUserOptions"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="閲囪喘鍚堝悓鍙凤細"
+ prop="purchaseLedgerId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨閲囪喘鍚堝悓鍙�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.purchaseLedgerId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ default-first-option
+ style="width: 240px"
+ :reserve-keyword="false"
+ @change="handleChangePurchaseLedgerId"
+ >
+ <el-option
+ v-for="item in purchaseLedgerOptions"
+ :key="item.id"
+ :label="item.purchaseContractNumber"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="澶囨敞锛�"
+ prop="remark"
+ >
+ <el-input v-model="formState.remark" type="textarea" placeholder="璇疯緭鍏ュ娉�"/>
+ </el-form-item>
+
+ <div style="margin: 20px 0;">
+ <div class="section-title">
+ <span class="title-dot"></span>
+ <span class="title-text">浜у搧鍒楄〃</span>
+ </div>
+ <el-button type="primary" size="small" style="margin-bottom: 20px" @click="isShowProductsModal = true" :disabled="!formState.purchaseLedgerId">娣诲姞浜у搧</el-button>
+ <el-table :data="formState.purchaseReturnOrderProductsDtos"
+ border
+ max-height="400"
+ :scroll-y="true"
+ show-summary
+ :summary-method="summarizeChildrenTable">
+ <el-table-column align="center"
+ type="selection"
+ width="55" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="浜у搧澶х被"
+ prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="specificationModel" />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ width="70" />
+ <el-table-column label="鏁伴噺"
+ prop="quantity"
+ width="70" />
+ <el-table-column label="閫�璐ф暟閲�"
+ prop="returnQuantity"
+ width="180">
+ <template #default="scope">
+ <el-input-number v-model="scope.row.returnQuantity"
+ controls-position="right"
+ :step="1"
+ :min="1"
+ :max="scope.row.quantity"
+ required
+ placeholder="璇疯緭鍏ラ��璐ф暟閲�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨棰勮鏁伴噺"
+ prop="warnNum"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="绋庣巼(%)"
+ prop="taxRate"
+ width="80" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <el-table-column label="鏄惁璐ㄦ"
+ prop="isChecked"
+ width="150">
+ <template #default="scope">
+ <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
+ {{ scope.row.isChecked ? '鏄�' : '鍚�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right"
+ label="鎿嶄綔"
+ width="100"
+ align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ size="small"
+ @click="delProduct(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <div class="section-title">
+ <span class="title-dot"></span>
+ <span class="title-text">璐圭敤淇℃伅</span>
+ </div>
+
+ <el-form-item
+ label="鏁村崟鎶樻墸棰濓細"
+ prop="totalDiscountAmount"
+ >
+ <el-input-number v-model="formState.totalDiscountAmount"
+ controls-position="right"
+ :step="0.01"
+ :precision="2"
+ style="width: 100%;"
+ @change="handleChangeTotalDiscountAmount"
+ placeholder="璇疯緭鍏ユ暣鍗曟姌鎵i"/>
+ </el-form-item>
+
+ <el-form-item
+ label="鏁村崟鎶樻墸鐜囷細"
+ prop="totalDiscountAmount"
+ >
+ <el-input-number v-model="formState.totalDiscountRate"
+ controls-position="right"
+ :step="0.01"
+ :precision="2"
+ style="width: 100%;"
+ placeholder="璇疯緭鍏ユ暣鍗曟姌鎵g巼"/>
+ </el-form-item>
+
+ <el-form-item
+ label="鎴愪氦閲戦锛�"
+ prop="totalAmount"
+ >
+ <el-input-number v-model="formState.totalAmount"
+ controls-position="right"
+ :step="0.01"
+ :precision="2"
+ style="width: 100%;"
+ @change="handleChangeTotalAmount"
+ placeholder="璇疯緭鍏ユ垚浜ら噾棰�"/>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <ProductList
+ v-if="isShowProductsModal"
+ v-model:visible="isShowProductsModal"
+ :purchase-ledger-id="formState.purchaseLedgerId"
+ @completed="handleAddProduct"
+ />
+
+ </div>
+</template>
+
+<script setup>
+import {ref, computed, getCurrentInstance} from "vue";
+import {createPurchaseReturnOrder} from "@/api/procurementManagement/purchase_return_order.js";
+import {getOptions, purchaseList} from "@/api/procurementManagement/procurementLedger.js";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+const ProductList = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/ProductList.vue"));
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ }
+});
+let { proxy } = getCurrentInstance()
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ no: '',
+ isDefaultNo: true,
+ returnType: 0,
+ remark: '',
+ supplierId: undefined,
+ projectId: undefined,
+ projectPhase: undefined,
+ preparedAt: undefined,
+ preparedUserId: undefined,
+ returnUserId: undefined,
+ purchaseLedgerId: undefined,
+ purchaseReturnOrderProductsDtos: [],
+ totalDiscountAmount: 0,
+ totalDiscountRate: undefined,
+ totalAmount: 0,
+});
+// 渚涘簲鍟嗛�夐」
+const supplierOptions = ref([])
+// 椤圭洰閫夐」
+const projectOptions = ref([])
+// 椤圭洰闃舵閫夐」
+const projectStageOptions = ref([
+ {
+ label: '绔嬮」',
+ value: 0,
+ },
+ {
+ label: '璁捐',
+ value: 1,
+ },
+ {
+ label: '閲囪喘',
+ value: 2,
+ },
+ {
+ label: '鐢熶骇',
+ value: 3,
+ },
+ {
+ label: '鍑鸿揣',
+ value: 4,
+ }
+])
+// 鐢ㄦ埛閫夐」
+const userOptions = ref([])
+// 閲囪喘鍙拌处閫夐」
+const purchaseLedgerOptions = ref([])
+// 鏄惁灞曠ず浜у搧鍒楄〃鏁版嵁
+const isShowProductsModal = ref(false)
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const formattedNumber = (row, column, cellValue) => {
+ return parseFloat(cellValue).toFixed(2);
+};
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const summarizeChildrenTable = (param) => {
+ return proxy.summarizeTable(
+ param,
+ [
+ "quantity",
+ "returnQuantity",
+ "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice",
+ "taxExclusiveTotalPrice",
+ ],
+ {
+ quantity: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ returnQuantity: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ }
+ );
+};
+
+const handleChangeTotalDiscountAmount= () => {
+ formState.value.totalAmount = formState.value.totalDiscountAmount * -1
+}
+
+const handleChangeTotalAmount= () => {
+ formState.value.totalDiscountAmount = formState.value.totalAmount * -1
+}
+
+// 鑾峰彇渚涘簲鍟嗛�夐」
+const fetchSupplierOptions = () => {
+ if (supplierOptions.value.length > 0) {
+ return
+ }
+ getOptions().then((res) => {
+ supplierOptions.value = res.data;
+ });
+}
+
+// 鑾峰彇椤圭洰閫夐」
+const fetchProjectOptions = () => {
+ if (projectOptions.value.length > 0) {
+ return
+ }
+ // todo 椤圭洰閫夐」
+}
+
+// 鑾峰彇鐢ㄦ埛閫夐」
+const fetchUserOptions = () => {
+ if (userOptions.value.length > 0) {
+ return
+ }
+ userListNoPageByTenantId().then((res) => {
+ userOptions.value = res.data;
+ });
+}
+
+// 澶勭悊鏀瑰彉渚涘簲鍟嗘暟鎹�
+const handleChangeSupplierId = () => {
+ formState.value.purchaseLedgerId = undefined
+ fetchPurchaseLedgerOptions()
+}
+
+// 鑾峰彇閲囪喘鍙拌处閫夐」
+const fetchPurchaseLedgerOptions = () => {
+ purchaseLedgerOptions.value = []
+ if (formState.value.supplierId) {
+ purchaseList({supplierId: formState.value.supplierId}).then((res) => {
+ purchaseLedgerOptions.value = res.rows;
+ });
+ }
+}
+
+// 澶勭悊鏀瑰彉閲囪喘鍙拌处鏁版嵁
+const handleChangePurchaseLedgerId = () => {
+ formState.value.purchaseReturnOrderProductsDtos = []
+}
+
+// 澶勭悊鏀瑰彉鏄惁榛樿缂栧彿
+const handleChangeIsDefaultNo = (checked) => {
+ if (checked) {
+ formState.value.no = ''
+ }
+}
+
+// 澧炲姞浜у搧
+const handleAddProduct = (selectedRows) => {
+ const existingIds = new Set(formState.value.purchaseReturnOrderProductsDtos.map(item => item.id));
+ const newProducts = selectedRows.filter(item => !existingIds.has(item.id)).map(item => ({
+ ...item,
+ returnQuantity: undefined,
+ salesLedgerProductId: item.id,
+ }));
+ formState.value.purchaseReturnOrderProductsDtos.push(...newProducts);
+}
+
+// 鍒犻櫎鍗曢」浜у搧
+const delProduct = (index) => {
+ formState.value.purchaseReturnOrderProductsDtos.splice(index, 1)
+}
+
+// 鎻愪氦琛ㄥ崟
+const handleSubmit = () => {
+ // 楠岃瘉閫�璐ф暟閲�
+ const hasEmptyReturnQuantity = formState.value.purchaseReturnOrderProductsDtos.some(item => !item.returnQuantity || item.returnQuantity <= 0);
+ if (hasEmptyReturnQuantity) {
+ proxy.$modal.msgError("璇蜂负鎵�鏈変骇鍝佸~鍐欓��璐ф暟閲�");
+ return;
+ }
+
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ createPurchaseReturnOrder(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
+
+<style scoped lang="scss">
+.section-title {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ width: 100%;
+ clear: both;
+}
+
+.title-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ background-color: #409EFF;
+ border-radius: 50%;
+ margin-right: 8px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/procurementManagement/purchaseReturnOrder/ProductList.vue b/src/views/procurementManagement/purchaseReturnOrder/ProductList.vue
new file mode 100644
index 0000000..3eeb167
--- /dev/null
+++ b/src/views/procurementManagement/purchaseReturnOrder/ProductList.vue
@@ -0,0 +1,150 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鏂板浜у搧"
+ width="1200"
+ @close="closeModal"
+ >
+ <div class="table_list">
+ <el-table :data="tableData"
+ border
+ @selection-change="handleChangeSelection">
+ <el-table-column align="center"
+ type="selection"
+ width="55" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="浜у搧澶х被"
+ prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="specificationModel" />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ width="70" />
+ <el-table-column label="鏁伴噺"
+ prop="quantity"
+ width="70" />
+ <el-table-column label="搴撳瓨棰勮鏁伴噺"
+ prop="warnNum"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="绋庣巼(%)"
+ prop="taxRate"
+ width="80" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <el-table-column label="鏄惁璐ㄦ"
+ prop="isChecked"
+ width="150">
+ <template #default="scope">
+ <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
+ {{ scope.row.isChecked ? '鏄�' : '鍚�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current" :limit="page.size" @pagination="paginationChange" />
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" :disabled="selectedRows.length === 0" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {computed, reactive, ref, onMounted} from "vue";
+import {productList} from "@/api/procurementManagement/procurementLedger.js";
+import {ElMessage} from "element-plus";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ purchaseLedgerId: {
+ type: Number,
+ required: true,
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const tableData = ref([])
+const selectedRows = ref([])
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 100,
+})
+const total = ref(0)
+const formattedNumber = (row, column, cellValue) => {
+ return parseFloat(cellValue).toFixed(2);
+};
+
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList()
+}
+
+const handleChangeSelection = (val) => {
+ selectedRows.value = val;
+}
+
+const fetchData = () => {
+ tableLoading.value = true;
+ productList({salesLedgerId: props.purchaseLedgerId, type: 2}).then((res) => {
+ tableData.value = res.data;
+ }).finally(() => {
+ tableLoading.value = false;
+ })
+}
+
+const handleSubmit = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�鏉′骇鍝�");
+ return;
+ }
+
+ emit('completed', selectedRows.value);
+ closeModal()
+}
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+onMounted(() => {
+ fetchData()
+})
+
+</script>
diff --git a/src/views/procurementManagement/purchaseReturnOrder/index.vue b/src/views/procurementManagement/purchaseReturnOrder/index.vue
new file mode 100644
index 0000000..4e91e59
--- /dev/null
+++ b/src/views/procurementManagement/purchaseReturnOrder/index.vue
@@ -0,0 +1,109 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm"
+ :inline="true">
+ <el-form-item label="閫�鏂欏崟鍙凤細">
+ <el-input v-model="searchForm.no"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery" />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery"> 鎼滅储 </el-button>
+ </el-form-item>
+ </el-form>
+
+ <div>
+ <el-button type="primary" @click="isShowNewModal = true">鏂板</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" :row-key="row => row.id" style="width: 100%" height="calc(100vh - 18.5em)">
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="閫�鏂欏崟鍙�" prop="no" show-overflow-tooltip />
+ <el-table-column label="閫�璐ф柟寮�" prop="returnType" show-overflow-tooltip />
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" show-overflow-tooltip />
+ <el-table-column label="鍏宠仈鍗曞彿" prop="purchaseContractNumber" show-overflow-tooltip />
+ <el-table-column label="閫�鏂欎汉" prop="returnUserName" show-overflow-tooltip />
+ <el-table-column label="澶囨敞" prop="remark" show-overflow-tooltip />
+ <el-table-column label="鍒涘缓浜�" prop="createUserName" show-overflow-tooltip />
+ <el-table-column label="鍒涘缓鏃堕棿" prop="createTime" show-overflow-tooltip />
+ <el-table-column label="鏈�杩戞洿鏂版椂闂�" prop="updateTime" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" min-width="60" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small">璇︽儏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current" :limit="page.size" @pagination="paginationChange" />
+ </div>
+ <new v-if="isShowNewModal"
+ v-model:visible="isShowNewModal"
+ @completed="handleQuery" />
+ </div>
+</template>
+
+<script setup>
+import pagination from '@/components/PIMTable/Pagination.vue'
+import { ref, reactive, toRefs, onMounted } from 'vue'
+import {findPurchaseReturnOrderListPage} from "@/api/procurementManagement/purchase_return_order.js";
+const New = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/New.vue"));
+const tableData = ref([])
+const selectedRows = ref([])
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 100,
+})
+const total = ref(0)
+// 鏄惁鏄剧ず鏂板寮规
+const isShowNewModal = ref(false)
+const data = reactive({
+ searchForm: {
+ no: '',
+ }
+})
+const { searchForm } = toRefs(data)
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1
+ getList()
+}
+
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList()
+}
+
+const getList = () => {
+ tableLoading.value = true
+ findPurchaseReturnOrderListPage({ ...searchForm.value, ...page }).then(res => {
+ tableLoading.value = false
+ tableData.value = res.data.records
+ total.value = res.data.total
+ }).catch(() => {
+ tableLoading.value = false
+ })
+}
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter(item => item.id);
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
diff --git a/src/views/productionManagement/processRoute/processRouteItem/index.vue b/src/views/productionManagement/processRoute/processRouteItem/index.vue
index 3aecfa0..c1c490c 100644
--- a/src/views/productionManagement/processRoute/processRouteItem/index.vue
+++ b/src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -202,8 +202,8 @@
</el-form>
<template #footer>
- <el-button @click="closeDialog">鍙栨秷</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
</template>
</el-dialog>
diff --git a/src/views/productionManagement/productStructure/index.vue b/src/views/productionManagement/productStructure/index.vue
index 1ae3f77..2c109cd 100644
--- a/src/views/productionManagement/productStructure/index.vue
+++ b/src/views/productionManagement/productStructure/index.vue
@@ -34,8 +34,8 @@
</el-form-item>
</el-form>
<template #footer>
- <el-button @click="closeDialog">鍙栨秷</el-button>
<el-button type="primary" @click="handleSubmit">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
</template>
</el-dialog>
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
index 761139e..fc64063 100644
--- a/src/views/productionManagement/productionOrder/index.vue
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -83,10 +83,10 @@
</el-form>
<template #footer>
<span class="dialog-footer">
- <el-button @click="bindRouteDialogVisible = false">鍙� 娑�</el-button>
<el-button type="primary"
:loading="bindRouteSaving"
@click="handleBindRouteConfirm">纭� 璁�</el-button>
+ <el-button @click="bindRouteDialogVisible = false">鍙� 娑�</el-button>
</span>
</template>
</el-dialog>
diff --git a/src/views/productionManagement/productionProcess/Edit.vue b/src/views/productionManagement/productionProcess/Edit.vue
index 8e92403..fb0ee74 100644
--- a/src/views/productionManagement/productionProcess/Edit.vue
+++ b/src/views/productionManagement/productionProcess/Edit.vue
@@ -25,6 +25,21 @@
<el-form-item label="宸ュ簭缂栧彿" prop="no">
<el-input v-model="formState.no" />
</el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
<el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
<el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
</el-form-item>
@@ -67,6 +82,7 @@
const formState = ref({
id: props.record.id,
name: props.record.name,
+ type: props.record.type,
no: props.record.no,
remark: props.record.remark,
salaryQuota: props.record.salaryQuota,
@@ -89,6 +105,7 @@
id: newRecord.id,
name: newRecord.name || '',
no: newRecord.no || '',
+ type: newRecord.type,
remark: newRecord.remark || '',
salaryQuota: newRecord.salaryQuota || '',
isQuality: props.record.isQuality,
@@ -103,6 +120,7 @@
id: props.record.id,
name: props.record.name || '',
no: props.record.no || '',
+ type: props.record.type,
remark: props.record.remark || '',
salaryQuota: props.record.salaryQuota || '',
isQuality: props.record.isQuality,
diff --git a/src/views/productionManagement/productionProcess/New.vue b/src/views/productionManagement/productionProcess/New.vue
index 5443e8d..a5f00aa 100644
--- a/src/views/productionManagement/productionProcess/New.vue
+++ b/src/views/productionManagement/productionProcess/New.vue
@@ -25,8 +25,25 @@
<el-form-item label="宸ュ簭缂栧彿" prop="no">
<el-input v-model="formState.no" />
</el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
<el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
- <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
+ <el-input v-model="formState.salaryQuota" type="number" :step="0.001">
+ <template #append>鍏�</template>
+ </el-input>
</el-form-item>
<el-form-item label="鏄惁璐ㄦ" prop="isQuality">
<el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
@@ -61,6 +78,7 @@
// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
const formState = ref({
name: '',
+ type: undefined,
remark: '',
salaryQuota: '',
isQuality: false,
diff --git a/src/views/productionManagement/productionProcess/index.vue b/src/views/productionManagement/productionProcess/index.vue
index 4f3f3ef..ffe13fc 100644
--- a/src/views/productionManagement/productionProcess/index.vue
+++ b/src/views/productionManagement/productionProcess/index.vue
@@ -99,6 +99,10 @@
prop: "name",
},
{
+ label: "宸ュ簭绫诲瀷",
+ prop: "typeText",
+ },
+ {
label: "宸ヨ祫瀹氶",
prop: "salaryQuota",
},
@@ -175,6 +179,7 @@
tableLoading.value = false;
tableData.value = res.data.records.map(item => ({
...item,
+ typeText: item.type !== undefined && item.type !== null ? (item.type === 0 ? "璁℃椂" : "璁′欢") : "",
}));
page.total = res.data.total;
})
diff --git a/src/views/productionManagement/productionReporting/index.vue b/src/views/productionManagement/productionReporting/index.vue
index 08b515d..0b42dae 100644
--- a/src/views/productionManagement/productionReporting/index.vue
+++ b/src/views/productionManagement/productionReporting/index.vue
@@ -163,6 +163,11 @@
width: 120,
},
{
+ label: "宸ュ簭",
+ prop: "process",
+ width: 120,
+ },
+ {
label: "宸ュ崟缂栧彿",
prop: "workOrderNo",
width: 120,
diff --git a/src/views/productionManagement/workOrder/index.vue b/src/views/productionManagement/workOrder/index.vue
index fddcb30..162dd91 100644
--- a/src/views/productionManagement/workOrder/index.vue
+++ b/src/views/productionManagement/workOrder/index.vue
@@ -24,10 +24,12 @@
:page="page"
:tableLoading="tableLoading"
@pagination="pagination">
- <template #completionStatus="{ row }">
- <el-progress :percentage="toProgressPercentage(row?.completionStatus)" :color="progressColor(toProgressPercentage(row?.completionStatus))" :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
- </template>
- </PIMTable>
+ <template #completionStatus="{ row }">
+ <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
+ :color="progressColor(toProgressPercentage(row?.completionStatus))"
+ :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
+ </template>
+ </PIMTable>
</div>
<el-dialog v-model="editDialogVisible"
title="缂栬緫鏃堕棿"
@@ -104,7 +106,6 @@
transferCardRowData.status
}}</span>
</div> -->
-
<div class="info-item">
<span class="info-label">璁″垝寮�濮嬫椂闂�</span>
<span class="info-value">{{ transferCardRowData.planStartTime }}</span>
@@ -166,26 +167,34 @@
<el-dialog v-model="reportDialogVisible"
title="鎶ュ伐"
width="500px">
- <el-form :model="reportForm"
+ <el-form ref="reportFormRef"
+ :model="reportForm"
+ :rules="reportFormRules"
label-width="120px">
<el-form-item label="寰呯敓浜ф暟閲�">
<el-input v-model="reportForm.planQuantity"
readonly
style="width: 300px" />
</el-form-item>
- <el-form-item label="鏈鐢熶骇鏁伴噺">
+ <el-form-item label="鏈鐢熶骇鏁伴噺"
+ prop="quantity">
<el-input v-model.number="reportForm.quantity"
type="number"
min="1"
+ step="1"
style="width: 300px"
- placeholder="璇疯緭鍏ユ湰娆$敓浜ф暟閲�" />
+ placeholder="璇疯緭鍏ユ湰娆$敓浜ф暟閲�"
+ @input="handleQuantityInput" />
</el-form-item>
- <el-form-item label="鎶ュ簾鏁伴噺">
+ <el-form-item label="鎶ュ簾鏁伴噺"
+ prop="scrapQty">
<el-input v-model.number="reportForm.scrapQty"
type="number"
- min="1"
+ min="0"
+ step="1"
style="width: 300px"
- placeholder="璇疯緭鍏ユ姤搴熸暟閲�" />
+ placeholder="璇疯緭鍏ユ姤搴熸暟閲�"
+ @input="handleScrapQtyInput" />
</el-form-item>
<el-form-item label="鐝粍淇℃伅">
<el-select v-model="reportForm.userId"
@@ -196,7 +205,7 @@
@change="handleUserChange">
<el-option v-for="user in userOptions"
:key="user.userId"
- :label="user.userName"
+ :label="user.nickName"
:value="user.userId" />
</el-select>
</el-form-item>
@@ -214,7 +223,7 @@
</template>
<script setup>
- import { onMounted, ref } from "vue";
+ import { onMounted, ref, nextTick } from "vue";
import { ElMessageBox } from "element-plus";
import dayjs from "dayjs";
import {
@@ -345,10 +354,12 @@
const transferCardRowData = ref(null);
const reportDialogVisible = ref(false);
const workOrderFilesRef = ref(null);
+ const reportFormRef = ref(null);
const userOptions = ref([]);
const reportForm = reactive({
planQuantity: 0,
- quantity: 0,
+ quantity: null,
+ scrapQty: null,
userName: "",
workOrderId: "",
reportWork: "",
@@ -356,6 +367,96 @@
userId: "",
productMainId: null,
});
+
+ // 鏈鐢熶骇鏁伴噺楠岃瘉瑙勫垯
+ const validateQuantity = (rule, value, callback) => {
+ if (value === null || value === undefined || value === "") {
+ callback(new Error("璇疯緭鍏ユ湰娆$敓浜ф暟閲�"));
+ return;
+ }
+ const num = Number(value);
+ // 鏁存暟涓斿ぇ浜庣瓑浜�1
+ if (isNaN(num) || !Number.isInteger(num) || num < 1) {
+ callback(new Error("鏈鐢熶骇鏁伴噺蹇呴』澶т簬绛変簬1"));
+ return;
+ }
+ callback();
+ };
+
+ // 鎶ュ簾鏁伴噺楠岃瘉瑙勫垯
+ const validateScrapQty = (rule, value, callback) => {
+ if (value === null || value === undefined || value === "") {
+ callback();
+ return;
+ }
+ const num = Number(value);
+ // 鏁存暟涓斿ぇ浜庣瓑浜�0
+ if (isNaN(num) || !Number.isInteger(num) || num < 0) {
+ callback(new Error("鎶ュ簾鏁伴噺蹇呴』澶т簬绛変簬0"));
+ return;
+ }
+ callback();
+ };
+
+ // 楠岃瘉瑙勫垯
+ const reportFormRules = {
+ quantity: [{ required: true, validator: validateQuantity, trigger: "blur" }],
+ scrapQty: [{ validator: validateScrapQty, trigger: "blur" }],
+ };
+
+ // 澶勭悊鏈鐢熶骇鏁伴噺杈撳叆锛岄檺鍒跺繀椤诲ぇ浜庣瓑浜�1
+ const handleQuantityInput = value => {
+ if (value === "" || value === null || value === undefined) {
+ reportForm.quantity = null;
+ return;
+ }
+ const num = Number(value);
+ if (isNaN(num)) {
+ return;
+ }
+ // 濡傛灉灏忎簬1锛屾竻闄�
+ if (num < 1) {
+ reportForm.quantity = null;
+ return;
+ }
+ // 濡傛灉鏄皬鏁板彇鏁存暟閮ㄥ垎
+ if (!Number.isInteger(num)) {
+ const intValue = Math.floor(num);
+ // 濡傛灉鍙栨暣鍚庡皬浜�1锛屾竻闄�
+ if (intValue < 1) {
+ reportForm.quantity = null;
+ return;
+ }
+ reportForm.quantity = intValue;
+ return;
+ }
+ reportForm.quantity = num;
+ };
+
+ // 澶勭悊鎶ュ簾鏁伴噺
+ const handleScrapQtyInput = value => {
+ if (value === "" || value === null || value === undefined) {
+ reportForm.scrapQty = null;
+ return;
+ }
+ const num = Number(value);
+ // 濡傛灉鏄疦aN锛屼繚鎸佸師鍊�
+ if (isNaN(num)) {
+ return;
+ }
+ // 濡傛灉鏄礋鏁帮紝娓呴櫎杈撳叆
+ if (num < 0) {
+ reportForm.scrapQty = null;
+ return;
+ }
+ // 濡傛灉鏄皬鏁帮紝鍙栨暣鏁伴儴鍒�
+ if (!Number.isInteger(num)) {
+ reportForm.scrapQty = Math.floor(num);
+ return;
+ }
+ // 鏈夋晥鐨勯潪璐熸暣鏁帮紙鍖呮嫭0锛�
+ reportForm.scrapQty = num;
+ };
const currentReportRowData = ref(null);
const page = reactive({
current: 1,
@@ -430,7 +531,9 @@
// 鍒涘缓 Blob URL
const fileBlob =
- blob instanceof Blob ? blob : new Blob([blob], { type: blob.type || "application/octet-stream" });
+ blob instanceof Blob
+ ? blob
+ : new Blob([blob], { type: blob.type || "application/octet-stream" });
const url = window.URL.createObjectURL(fileBlob);
// 鍒涘缓闅愯棌 iframe锛岀敤浜庤Е鍙戞祻瑙堝櫒鎵撳嵃
@@ -494,18 +597,23 @@
const showReportDialog = row => {
currentReportRowData.value = row;
reportForm.planQuantity = row.planQuantity;
- reportForm.quantity = row.quantity;
+ reportForm.quantity =
+ row.quantity !== undefined && row.quantity !== null ? row.quantity : null;
reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
reportForm.workOrderId = row.id;
reportForm.reportWork = row.reportWork;
reportForm.productMainId = row.productMainId;
- reportForm.scrapQty = row.scrapQty;
+ reportForm.scrapQty =
+ row.scrapQty !== undefined && row.scrapQty !== null ? row.scrapQty : null;
+ nextTick(() => {
+ reportFormRef.value?.clearValidate();
+ });
// 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛淇℃伅锛岃缃负榛樿閫変腑
getUserProfile()
.then(res => {
if (res.code === 200) {
reportForm.userId = res.data.userId;
- reportForm.userName = res.data.userName;
+ reportForm.userName = res.data.nickName;
}
})
.catch(err => {
@@ -520,35 +628,79 @@
};
const handleReport = () => {
- if (reportForm.planQuantity <= 0) {
- ElMessageBox.alert("寰呯敓浜ф暟閲忎负0锛屾棤娉曟姤宸�", "鎻愮ず", {
- confirmButtonText: "纭畾",
- });
- return;
- }
- if (!reportForm.quantity || reportForm.quantity <= 0) {
- ElMessageBox.alert("璇疯緭鍏ユ湁鏁堢殑鏈鐢熶骇鏁伴噺", "鎻愮ず", {
- confirmButtonText: "纭畾",
- });
- return;
- }
- if (reportForm.quantity > reportForm.planQuantity) {
- ElMessageBox.alert("鏈鐢熶骇鏁伴噺涓嶈兘瓒呰繃寰呯敓浜ф暟閲�", "鎻愮ず", {
- confirmButtonText: "纭畾",
- });
- return;
- }
- // console.log(reportForm);
- addProductMain(reportForm).then(res => {
- if (res.code === 200) {
- proxy.$modal.msgSuccess("鎶ュ伐鎴愬姛");
- reportDialogVisible.value = false;
- getList();
- } else {
- ElMessageBox.alert(res.msg || "鎶ュ伐澶辫触", "鎻愮ず", {
+ reportFormRef.value?.validate(valid => {
+ if (!valid) {
+ return false;
+ }
+
+ if (reportForm.planQuantity <= 0) {
+ ElMessageBox.alert("寰呯敓浜ф暟閲忎负0锛屾棤娉曟姤宸�", "鎻愮ず", {
confirmButtonText: "纭畾",
});
+ return;
}
+
+ // 楠岃瘉鏈鐢熶骇鏁伴噺
+ if (
+ reportForm.quantity === null ||
+ reportForm.quantity === undefined ||
+ reportForm.quantity === ""
+ ) {
+ ElMessageBox.alert("璇疯緭鍏ユ湰娆$敓浜ф暟閲�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ const quantity = Number(reportForm.quantity);
+ const scrapQty =
+ reportForm.scrapQty === null ||
+ reportForm.scrapQty === undefined ||
+ reportForm.scrapQty === ""
+ ? 0
+ : Number(reportForm.scrapQty);
+
+ // 鏈鐢熶骇鏁伴噺
+ if (isNaN(quantity) || !Number.isInteger(quantity) || quantity < 1) {
+ ElMessageBox.alert("鏈鐢熶骇鏁伴噺蹇呴』澶т簬绛変簬1", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ // 鎶ュ簾鏁伴噺蹇呴』鏄暣鏁颁笖澶т簬绛変簬0
+ if (isNaN(scrapQty) || !Number.isInteger(scrapQty) || scrapQty < 0) {
+ ElMessageBox.alert("鎶ュ簾鏁伴噺蹇呴』澶т簬绛変簬0", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ if (quantity > reportForm.planQuantity) {
+ ElMessageBox.alert("鏈鐢熶骇鏁伴噺涓嶈兘瓒呰繃寰呯敓浜ф暟閲�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ const submitData = {
+ ...reportForm,
+ quantity: quantity,
+ scrapQty: scrapQty,
+ };
+
+ // console.log(submitData);
+ addProductMain(submitData).then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鎶ュ伐鎴愬姛");
+ reportDialogVisible.value = false;
+ getList();
+ } else {
+ ElMessageBox.alert(res.msg || "鎶ュ伐澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ }
+ });
});
};
@@ -566,11 +718,11 @@
};
// 鐢ㄦ埛閫夋嫨鍙樺寲鏃舵洿鏂� userName
- const handleUserChange = (userId) => {
+ const handleUserChange = userId => {
if (userId) {
const selectedUser = userOptions.value.find(user => user.userId === userId);
if (selectedUser) {
- reportForm.userName = selectedUser.userName;
+ reportForm.userName = selectedUser.nickName;
}
} else {
reportForm.userName = "";
diff --git a/src/views/projectManagement/Management/components/formDia.vue b/src/views/projectManagement/Management/components/formDia.vue
new file mode 100644
index 0000000..f29512b
--- /dev/null
+++ b/src/views/projectManagement/Management/components/formDia.vue
@@ -0,0 +1,1503 @@
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="95%"
+ top="5vh"
+ destroy-on-close
+ @close="closeDialog"
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="120px"
+ :disabled="isView"
+ >
+ <div class="section">
+ <div class="section-header" @click="toggleSection('base')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鍩虹璧勬枡</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.base" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.base" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鍗曟嵁缂栧彿" prop="billNo">
+ <el-input v-model="form.billNo" placeholder="绯荤粺鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鍚嶇О" prop="projectName">
+ <el-input v-model="form.projectName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+ <el-input v-model="form.customerName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="绔嬮」鏃ユ湡" prop="setupDate">
+ <el-date-picker
+ v-model="form.setupDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鏉ユ簮" prop="projectSource">
+ <el-input v-model="form.projectSource" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="绔嬮」浜�" prop="creatorName">
+ <el-input v-model="form.creatorName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="棰勮宸ユ湡(澶�)" prop="estimatedDays">
+ <el-input-number v-model="form.estimatedDays" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝寮�濮嬫棩鏈�" prop="planStartDate">
+ <el-date-picker
+ v-model="form.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝瀹屾垚鏃ユ湡" prop="planEndDate">
+ <el-date-picker
+ v-model="form.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="椤圭洰绫诲瀷" prop="projectManagementPlanId">
+ <el-select v-model="form.projectManagementPlanId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in projectTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰閲戦" prop="projectAmount">
+ <el-input-number v-model="form.projectAmount" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹℃牳鐘舵��" prop="auditStatus">
+ <el-select v-model="form.auditStatus" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="d in project_management" :key="d.value" :label="d.label" :value="d.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ </el-row>
+ <el-row :gutter="10" >
+ <el-col :span="24">
+ <el-upload
+ v-model:file-list="fileList"
+ :action="upload.url"
+ :headers="upload.headers"
+ multiple
+ :disabled="isView"
+ :before-upload="beforeUpload"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="files"
+ :on-remove="handleRemove"
+ >
+ <el-button type="primary" :disabled="isView">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ <div v-if="existingAttachments.length > 0" class="attachment-list">
+ <div
+ v-for="(att, idx) in existingAttachments"
+ :key="att.id || att.url || idx"
+ class="attachment-item"
+ >
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="100" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('product')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>浜у搧淇℃伅</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" @click="openProductForm('add')">娣诲姞</el-button>
+ <el-button v-if="!isView" plain type="danger" @click="deleteProduct">鍒犻櫎</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('product')">
+ <ArrowDown v-if="sectionCollapsed.product" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.product" class="section-body">
+ <el-table
+ :data="productData"
+ border
+ show-summary
+ :summary-method="summarizeProductTable"
+ @selection-change="productSelected"
+ >
+ <el-table-column v-if="!isView" align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+ <el-table-column label="鍗曚綅" prop="unit" />
+ <el-table-column label="鏁伴噺" prop="quantity" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column v-if="!isView" fixed="right" label="鎿嶄綔" min-width="60" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row, scope.$index)">缂栬緫</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('team')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰鍥㈤槦</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addTeamRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('team')">
+ <ArrowDown v-if="sectionCollapsed.team" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.team" class="section-body">
+ <PIMTable
+ :column="teamColumns"
+ :tableData="form.teamList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="220"
+ >
+ <template #memberId="{ row }">
+ <el-select v-model="row.memberId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #roleId="{ row }">
+ <el-select v-model="row.roleId" placeholder="璇烽�夋嫨" clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
+ </el-select>
+ </template>
+ <template #enterDate="{ row }">
+ <el-date-picker
+ v-model="row.enterDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #leaveDate="{ row }">
+ <el-date-picker
+ v-model="row.leaveDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #phone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamRemark="{ row }">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeTeamRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <!-- <div class="section">
+ <div class="section-header" @click="toggleSection('phase')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰闃舵</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addPhaseRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('phase')">
+ <ArrowDown v-if="sectionCollapsed.phase" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.phase" class="section-body">
+ <PIMTable
+ :column="phaseColumns"
+ :tableData="form.phaseList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="240"
+ >
+ <template #phaseName="{ row }">
+ <el-input v-model="row.phaseName" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseDesc="{ row }">
+ <el-input v-model="row.description" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #ownerId="{ row }">
+ <el-select v-model="row.ownerId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #planDays="{ row }">
+ <el-input-number v-model="row.planDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #planStart="{ row }">
+ <el-date-picker
+ v-model="row.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #planEnd="{ row }">
+ <el-date-picker
+ v-model="row.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #progress="{ row }">
+ <el-input-number v-model="row.progress" :min="0" :max="100" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #actualStart="{ row }">
+ <el-date-picker
+ v-model="row.actualStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #actualEnd="{ row }">
+ <el-date-picker
+ v-model="row.actualEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #overdueDays="{ row }">
+ <el-input-number v-model="row.overdueDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #completion="{ row }">
+ <el-input v-model="row.completionRemark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removePhaseRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div> -->
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('address')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鏀惰揣鍦板潃</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addAddressRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('address')">
+ <ArrowDown v-if="sectionCollapsed.address" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.address" class="section-body">
+ <PIMTable
+ :column="addressColumns"
+ :tableData="form.addressList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="200"
+ >
+ <template #receiver="{ row }">
+ <el-input v-model="row.receiver" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverPhone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverAddress="{ row }">
+ <el-input v-model="row.address" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #addressAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeAddressRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('contact')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鑱旂郴淇℃伅</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.contact" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.contact" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鑱旂郴浜哄鍚�" prop="contactName">
+ <el-input v-model="form.contactName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎬у埆" prop="contactGender">
+ <el-select v-model="form.contactGender" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option label="鐢�" value="1" />
+ <el-option label="濂�" value="2" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鐢熸棩" prop="contactBirthday">
+ <el-date-picker
+ v-model="form.contactBirthday"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閭" prop="contactEmail">
+ <el-input v-model="form.contactEmail" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="閮ㄩ棬" prop="contactDept">
+ <el-input v-model="form.contactDept" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鑱屽姟" prop="contactJob">
+ <el-input v-model="form.contactJob" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎵嬫満鍙风爜" prop="contactMobile">
+ <el-input v-model="form.contactMobile" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="寰俊鍙风爜" prop="contactWechat">
+ <el-input v-model="form.contactWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="QQ" prop="contactQq">
+ <el-input v-model="form.contactQq" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="浼佷笟寰俊" prop="contactWorkWechat">
+ <el-input v-model="form.contactWorkWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍦板潃" prop="contactAddress">
+ <el-input v-model="form.contactAddress" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="contactRemark">
+ <el-input v-model="form.contactRemark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="200" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDialog">{{ isView ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <FormDialog
+ v-model="productFormVisible"
+ :title="productOperationType === 'add' ? '鏂板浜у搧' : '缂栬緫浜у搧'"
+ :width="'40%'"
+ :operation-type="productOperationType"
+ @close="closeProductDia"
+ @confirm="submitProduct"
+ @cancel="closeProductDia"
+ >
+ <el-form ref="productFormRef" :model="productForm" label-width="140px" label-position="top" :rules="productRules">
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="浜у搧澶х被锛�" prop="productCategoryId">
+ <el-tree-select
+ v-model="productForm.productCategoryId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ check-strictly
+ :data="productCategoryOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ @change="getModels"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="productModelId">
+ <el-select v-model="productForm.productModelId" placeholder="璇烽�夋嫨" clearable filterable @change="getProductModel">
+ <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input v-model="productForm.unit" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)锛�" prop="taxRate">
+ <el-select v-model="productForm.taxRate" placeholder="璇烽�夋嫨" clearable @change="calculateFromTaxRate">
+ <el-option label="1" value="1" />
+ <el-option label="6" value="6" />
+ <el-option label="13" value="13" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鍗曚环(鍏�)锛�" prop="taxInclusiveUnitPrice">
+ <el-input-number
+ v-model="productForm.taxInclusiveUnitPrice"
+ :step="0.01"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromUnitPrice"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺锛�" prop="quantity">
+ <el-input-number
+ v-model="productForm.quantity"
+ :step="0.1"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromQuantity"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鎬讳环(鍏�)锛�" prop="taxInclusiveTotalPrice">
+ <el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromTotalPrice" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓嶅惈绋庢�讳环(鍏�)锛�" prop="taxExclusiveTotalPrice">
+ <el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromExclusiveTotalPrice" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷锛�" prop="invoiceType">
+ <el-select v-model="productForm.invoiceType" placeholder="璇烽�夋嫨" clearable>
+ <el-option label="澧炴櫘绁�" value="澧炴櫘绁�" />
+ <el-option label="澧炰笓绁�" value="澧炰笓绁�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup name="ProjectManagementFormDia">
+import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue'
+import { ArrowDown, ArrowUp, Delete, Plus, Document } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+import { listPlan } from '@/api/projectManagement/projectType'
+import { findRoleListPage } from '@/api/projectManagement/role'
+import { userListAll } from '@/api/publicApi'
+import { addProject, getProject, updateProject } from '@/api/projectManagement/project'
+import { modelList, productTreeList } from '@/api/basicData/product'
+import { delProduct as delSalesProduct } from '@/api/salesManagement/salesLedger'
+
+const emit = defineEmits(['completed'])
+const { proxy } = getCurrentInstance()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+
+const dialogVisible = ref(false)
+const operationType = ref('add')
+const formRef = ref()
+const fileList = ref([])
+const existingAttachments = ref([])
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
+ headers: { Authorization: 'Bearer ' + getToken() }
+})
+
+const projectTypeOptions = ref([])
+const roleOptions = ref([])
+const userOptions = ref([])
+const productData = ref([])
+const productSelectedRows = ref([])
+const productCategoryOptions = ref([])
+const modelOptions = ref([])
+const productFormVisible = ref(false)
+const productOperationType = ref('add')
+const productFormRef = ref()
+const productIndex = ref(0)
+const isCalculating = ref(false)
+
+const productFormData = reactive({
+ productForm: {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ },
+ productRules: {
+ productCategoryId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ productModelId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ unit: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ quantity: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxInclusiveUnitPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxRate: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ taxInclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxExclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ invoiceType: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }]
+ }
+})
+
+const { productForm, productRules } = toRefs(productFormData)
+
+const data = reactive({
+ form: {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ },
+ rules: {
+ projectName: [{ required: true, message: '璇疯緭鍏ラ」鐩悕绉�', trigger: 'blur' }]
+ }
+})
+
+const { form, rules } = toRefs(data)
+
+const sectionCollapsed = reactive({
+ base: false,
+ product: false,
+ team: false,
+ phase: false,
+ address: false,
+ contact: false
+})
+
+const isView = computed(() => operationType.value === 'view')
+const dialogTitle = computed(() => {
+ if (operationType.value === 'add') return '鏂板椤圭洰'
+ if (operationType.value === 'edit') return '缂栬緫椤圭洰'
+ return '椤圭洰璇︽儏'
+})
+
+const teamColumns = [
+ { label: '濮撳悕', prop: 'memberId', align: 'center', width: 180, dataType: 'slot', slot: 'memberId' },
+ { label: '椤圭洰缁勮鑹�', prop: 'roleId', align: 'center', width: 160, dataType: 'slot', slot: 'roleId' },
+ { label: '杩涘叆鏃ユ湡', prop: 'enterDate', align: 'center', width: 160, dataType: 'slot', slot: 'enterDate' },
+ { label: '绂诲紑鏃ユ湡', prop: 'leaveDate', align: 'center', width: 160, dataType: 'slot', slot: 'leaveDate' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'phone' },
+ { label: '澶囨敞', prop: 'remark', align: 'center', dataType: 'slot', slot: 'teamRemark' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'teamAction', fixed: 'right' }
+]
+
+const phaseColumns = [
+ { label: '闃舵鍚嶇О', prop: 'phaseName', align: 'center', width: 160, dataType: 'slot', slot: 'phaseName' },
+ { label: '鎻忚堪', prop: 'description', align: 'center', width: 200, dataType: 'slot', slot: 'phaseDesc' },
+ { label: '璐熻矗浜�', prop: 'ownerId', align: 'center', width: 160, dataType: 'slot', slot: 'ownerId' },
+ { label: '棰勮宸ユ湡(澶�)', prop: 'planDays', align: 'center', width: 140, dataType: 'slot', slot: 'planDays' },
+ { label: '璁″垝寮�濮嬫棩鏈�', prop: 'planStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'planStart' },
+ { label: '璁″垝缁撴潫鏃ユ湡', prop: 'planEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'planEnd' },
+ { label: '杩涘害(%)', prop: 'progress', align: 'center', width: 120, dataType: 'slot', slot: 'progress' },
+ { label: '瀹為檯寮�濮嬫棩鏈�', prop: 'actualStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualStart' },
+ { label: '瀹為檯缁撴潫鏃ユ湡', prop: 'actualEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualEnd' },
+ { label: '閫炬湡澶╂暟', prop: 'overdueDays', align: 'center', width: 120, dataType: 'slot', slot: 'overdueDays' },
+ { label: '瀹屾垚鎯呭喌', prop: 'completionRemark', align: 'center', width: 200, dataType: 'slot', slot: 'completion' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'phaseAction', fixed: 'right' }
+]
+
+const addressColumns = [
+ { label: '鏀惰揣浜�', prop: 'receiver', align: 'center', width: 180, dataType: 'slot', slot: 'receiver' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'receiverPhone' },
+ { label: '鏀惰揣鍦板潃', prop: 'address', align: 'center', dataType: 'slot', slot: 'receiverAddress' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'addressAction', fixed: 'right' }
+]
+
+function toggleSection(key) {
+ sectionCollapsed[key] = !sectionCollapsed[key]
+}
+
+function resetFormData() {
+ Object.assign(form.value, {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ })
+ fileList.value = []
+ productData.value = []
+}
+
+function formattedNumber(row, column, cellValue) {
+ const val = Number(cellValue ?? 0)
+ return Number.isFinite(val) ? val.toFixed(2) : '0.00'
+}
+
+function summarizeProductTable(param) {
+ return proxy.summarizeTable(param, ['taxInclusiveTotalPrice', 'taxExclusiveTotalPrice'])
+}
+
+function productSelected(selection) {
+ productSelectedRows.value = selection
+}
+
+function convertIdToValue(data) {
+ return (Array.isArray(data) ? data : []).map(item => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+function findNodeById(nodes, productId) {
+ for (let i = 0; i < (nodes || []).length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId)
+ if (foundNode) return foundNode
+ }
+ }
+ return null
+}
+
+function 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
+}
+
+function getProductOptions() {
+ return productTreeList().then(res => {
+ const list = res?.data || res
+ productCategoryOptions.value = convertIdToValue(list)
+ return productCategoryOptions.value
+ })
+}
+
+function getModels(value) {
+ const categoryLabel = findNodeById(productCategoryOptions.value, value)
+ productForm.value.productCategory = categoryLabel || ''
+ modelList({ id: value }).then(res => {
+ modelOptions.value = res?.data || res || []
+ })
+}
+
+function getProductModel(value) {
+ const index = (modelOptions.value || []).findIndex(item => item.id === value)
+ if (index !== -1) {
+ productForm.value.specificationModel = modelOptions.value[index].model
+ productForm.value.unit = modelOptions.value[index].unit
+ } else {
+ productForm.value.specificationModel = ''
+ productForm.value.unit = ''
+ }
+}
+
+async function openProductForm(type, row, index) {
+ productOperationType.value = type
+ productIndex.value = index || 0
+ productForm.value = {}
+ proxy.resetForm('productFormRef')
+
+ if (!productCategoryOptions.value || productCategoryOptions.value.length === 0) {
+ await getProductOptions()
+ }
+
+ if (type === 'edit' && row) {
+ productForm.value = { ...row }
+ try {
+ const categoryId = findNodeIdByLabel(productCategoryOptions.value, productForm.value.productCategory)
+ if (categoryId) {
+ productForm.value.productCategoryId = categoryId
+ const models = await modelList({ id: categoryId })
+ modelOptions.value = models?.data || models || []
+ const currentModel = (modelOptions.value || []).find(m => m.model === productForm.value.specificationModel)
+ if (currentModel) {
+ productForm.value.productModelId = currentModel.id
+ }
+ }
+ } catch {}
+ } else {
+ productForm.value = {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ }
+ }
+
+ productFormVisible.value = true
+}
+
+function closeProductDia() {
+ proxy.resetForm('productFormRef')
+ productFormVisible.value = false
+}
+
+function submitProduct() {
+ productFormRef.value?.validate?.(valid => {
+ if (!valid) return
+ const payload = { ...productForm.value }
+ if (productOperationType.value === 'add') {
+ productData.value.push(payload)
+ } else {
+ productData.value[productIndex.value] = payload
+ }
+ closeProductDia()
+ })
+}
+
+function deleteProduct() {
+ if (!productSelectedRows.value || productSelectedRows.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const selectedIds = productSelectedRows.value.map(r => r?.id).filter(Boolean)
+ if (operationType.value !== 'add' && selectedIds.length > 0) {
+ delSalesProduct(selectedIds)
+ .then(() => {
+ proxy.$modal?.msgSuccess?.('鍒犻櫎鎴愬姛')
+ productData.value = productData.value.filter(row => !selectedIds.includes(row?.id))
+ productSelectedRows.value = []
+ })
+ .catch(() => {})
+ return
+ }
+
+ productData.value = productData.value.filter(row => !productSelectedRows.value.includes(row))
+ productSelectedRows.value = []
+}
+
+function calculateFromTotalPrice() {
+ if (isCalculating.value) return
+ const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ if (!totalPrice || !quantity || quantity <= 0) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(totalPrice, productForm.value.taxRate)
+ }
+ isCalculating.value = false
+}
+
+function calculateFromExclusiveTotalPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const exclusiveTotalPrice = parseFloat(productForm.value.taxExclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) return
+ isCalculating.value = true
+ const taxRateDecimal = taxRate / 100
+ const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal)
+ productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2)
+ productForm.value.taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2)
+ isCalculating.value = false
+}
+
+function calculateFromQuantity() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromUnitPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromTaxRate() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const inclusiveTotalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!inclusiveTotalPrice || !taxRate) return
+ isCalculating.value = true
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate)
+ isCalculating.value = false
+}
+
+async function loadProjectTypeOptions() {
+ try {
+ const res = await listPlan({ current: 1, size: 999 })
+ const records = res?.data?.records || res?.records || res?.rows || []
+ projectTypeOptions.value = records.map(item => ({ label: item.name, value: item.id }))
+ } catch {
+ projectTypeOptions.value = []
+ }
+}
+
+async function loadRoleOptions() {
+ try {
+ const res = await findRoleListPage({ pageNum: 1, pageSize: 999 })
+ const records = res?.data?.records || res?.rows || res?.records || []
+ roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id }))
+ } catch {
+ roleOptions.value = []
+ }
+}
+
+async function loadUserOptions() {
+ try {
+ const res = await userListAll()
+ const list = res?.data || res?.rows || res || []
+ userOptions.value = (Array.isArray(list) ? list : []).map(u => ({
+ label: u.nickName || u.userName || u.username || u.name,
+ value: u.userId || u.id
+ }))
+ } catch {
+ userOptions.value = []
+ }
+}
+
+function addTeamRow() {
+ form.value.teamList.push({
+ memberId: undefined,
+ roleId: undefined,
+ enterDate: '',
+ leaveDate: '',
+ phone: '',
+ remark: ''
+ })
+}
+
+function removeTeamRow(index) {
+ if (index > -1) form.value.teamList.splice(index, 1)
+}
+
+function addPhaseRow() {
+ form.value.phaseList.push({
+ phaseName: '',
+ description: '',
+ ownerId: undefined,
+ planDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ progress: 0,
+ actualStartDate: '',
+ actualEndDate: '',
+ overdueDays: 0,
+ completionRemark: ''
+ })
+}
+
+function removePhaseRow(index) {
+ if (index > -1) form.value.phaseList.splice(index, 1)
+}
+
+function addAddressRow() {
+ form.value.addressList.push({
+ receiver: '',
+ phone: '',
+ address: ''
+ })
+}
+
+function removeAddressRow(index) {
+ if (index > -1) form.value.addressList.splice(index, 1)
+}
+
+function beforeUpload() {
+ if (isView.value) return false
+ proxy.$modal?.loading?.('姝e湪涓婁紶鏂囦欢锛岃绋嶅��...')
+ return true
+}
+
+function handleUploadError() {
+ proxy.$modal?.closeLoading?.()
+ ElMessage.error('涓婁紶鏂囦欢澶辫触')
+}
+
+function handleUploadSuccess(res, file) {
+ console.log(res, file)
+ proxy.$modal?.closeLoading?.()
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || '涓婁紶澶辫触')
+ return
+ }
+ const attachmentId = res?.data?.[0]?.id ?? ""
+ if (!attachmentId) return
+ form.value.attachmentIds.push(attachmentId)
+ console.log(form.value.attachmentIds)
+ ElMessage.success('涓婁紶鎴愬姛')
+}
+
+function handleRemove(file) {
+ const attachmentId = file?.attachmentId
+ if (!attachmentId) return
+ form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
+}
+
+async function openDialog(payload = {}) {
+ operationType.value = payload.operationType || 'add'
+ resetFormData()
+ await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), getProductOptions()])
+ if (payload.row?.id) {
+ try {
+ const res = await getProject(payload.row.id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ const info = detail?.info || {}
+ const shippingAddress = detail?.shippingAddress || {}
+ const contractInfo = detail?.contractInfo || {}
+
+ const normalizeId = v => {
+ if (v === undefined || v === null || v === '') return undefined
+ const n = Number(v)
+ return Number.isNaN(n) ? v : n
+ }
+
+ const normalizeDictValue = v => {
+ if (v === undefined || v === null || v === '') return ''
+ return String(v)
+ }
+
+ const computeEstimatedDays = (start, end) => {
+ if (!start || !end) return 0
+ const startTime = new Date(`${start}T00:00:00`).getTime()
+ const endTime = new Date(`${end}T00:00:00`).getTime()
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) return 0
+ if (endTime < startTime) return 0
+ return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
+ }
+
+ Object.assign(form.value, {
+ id: info.id,
+ billNo: info.no ?? '',
+ projectManagementPlanId: info.projectManagementPlanId ?? '',
+ estimatedDays: Number(info.estimatedDays) || computeEstimatedDays(info.planStartTime, info.planEndTime) || 0,
+ projectName: info.title ?? '',
+ customerName: info.clientName ?? '',
+ parentProjectName: info.projectManagementInfoParentName ?? '',
+ setupDate: info.establishTime ?? '',
+ projectSource: info.source ?? '',
+ creatorName: info.managerName ?? '',
+ billStatus: normalizeDictValue(info.status),
+ projectStage: normalizeDictValue(info.stage ?? info.projectStage),
+ planStartDate: info.planStartTime ?? '',
+ planEndDate: info.planEndTime ?? '',
+ projectAmount: info.orderAmount ?? 0,
+ auditStatus: normalizeDictValue(info.reviewStatus),
+ remark: info.remark ?? '',
+ attachmentIds: Array.isArray(info.attachmentIds) ? info.attachmentIds : [],
+ teamList: Array.isArray(info.teamList) ? info.teamList.map(t => ({
+ memberId: normalizeId(t.userId),
+ roleId: normalizeId(t.userRoleId),
+ enterDate: t.joinTime,
+ leaveDate: t.departTime,
+ phone: t.contact,
+ remark: t.remark
+ })) : [],
+ addressList: shippingAddress?.address
+ ? [{
+ receiver: shippingAddress.consignee,
+ phone: shippingAddress.contract,
+ address: shippingAddress.address
+ }]
+ : [],
+ contactName: contractInfo.name ?? '',
+ contactGender: contractInfo.sex === '鐢�' ? '1' : contractInfo.sex === '濂�' ? '2' : '',
+ 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 ?? ''
+ })
+
+ existingAttachments.value = Array.isArray(info.attachmentList)
+ ? info.attachmentList.map(a => ({
+ id: a.id ?? a.fileId,
+ name: a.fileName ?? a.name,
+ url: a.url ?? a.fileUrl ?? a.path
+ }))
+ : []
+
+ const rawPhaseList =
+ detail?.phaseList ||
+ detail?.projectPhaseList ||
+ detail?.projectStageList ||
+ info?.phaseList ||
+ info?.projectPhaseList ||
+ []
+ form.value.phaseList = Array.isArray(rawPhaseList)
+ ? rawPhaseList.map(p => ({
+ phaseName: p.phaseName ?? p.name ?? p.title ?? '',
+ description: p.description ?? p.workContent ?? p.desc ?? '',
+ ownerId: normalizeId(p.ownerId ?? p.leaderId ?? p.userId),
+ planDays: Number(p.planDays ?? p.estimatedDuration ?? p.estimatedDays) || 0,
+ planStartDate: p.planStartDate ?? p.planStartTime ?? p.startDate ?? '',
+ planEndDate: p.planEndDate ?? p.planEndTime ?? p.endDate ?? '',
+ progress: Number(p.progress ?? p.schedule) || 0,
+ actualStartDate: p.actualStartDate ?? p.actualStartTime ?? '',
+ actualEndDate: p.actualEndDate ?? p.actualEndTime ?? '',
+ overdueDays: Number(p.overdueDays ?? p.overDays) || 0,
+ completionRemark: p.completionRemark ?? p.remark ?? ''
+ }))
+ : []
+
+ productData.value = detail?.salesLedgerProductList || detail?.productData || []
+ } catch {}
+ }
+ if (form.value.teamList.length === 0 && !isView.value) addTeamRow()
+ if (form.value.phaseList.length === 0 && !isView.value) addPhaseRow()
+ dialogVisible.value = true
+}
+
+function downloadAttachment(att) {
+ if (att?.name) {
+ try {
+ proxy.$download.name(att.url);
+ return
+ } catch (e) {}
+ }
+ ElMessage.warning('闄勪欢鏆傛棤涓嬭浇鍦板潃')
+}
+function closeDialog() {
+ dialogVisible.value = false
+}
+
+async function submitForm() {
+ if (isView.value) {
+ closeDialog()
+ return
+ }
+ await formRef.value?.validate?.()
+ if (!productData.value || productData.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇锋坊鍔犱骇鍝佷俊鎭�')
+ return
+ }
+ const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label
+ const teamList = (form.value.teamList || []).map(t => ({
+ userId: t.memberId,
+ userName: findLabel(userOptions.value, t.memberId),
+ userRoleId: t.roleId,
+ userRoleName: findLabel(roleOptions.value, t.roleId),
+ joinTime: t.enterDate,
+ departTime: t.leaveDate,
+ contact: t.phone,
+ remark: t.remark
+ }))
+
+ const shippingRow = (form.value.addressList || [])[0] || {}
+ const shippingAddress = {
+ id: undefined,
+ consignee: shippingRow.receiver,
+ contract: shippingRow.phone,
+ address: shippingRow.address
+ }
+
+ const contractInfo = {
+ id: undefined,
+ name: form.value.contactName,
+ sex: form.value.contactGender === '1' ? '鐢�' : form.value.contactGender === '2' ? '濂�' : '',
+ birthday: form.value.contactBirthday,
+ department: form.value.contactDept,
+ job: form.value.contactJob,
+ phoneNumber: form.value.contactMobile,
+ email: form.value.contactEmail,
+ qq: form.value.contactQq,
+ lineaFissa: form.value.contactWorkWechat,
+ wx: form.value.contactWechat,
+ origineEtnica: form.value.contactAddress,
+ rappresentanteLegale: form.value.contactRemark
+ }
+ const info = {
+ id: form.value.id ?? null,
+ no: form.value.billNo,
+ title: form.value.projectName,
+ clientId: form.value.clientId ?? null,
+ clientName: form.value.customerName,
+ projectManagementInfoParentId: form.value.parentProjectId ?? null,
+ projectManagementPlanId: form.value.projectManagementPlanId ?? null,
+ establishTime: form.value.setupDate,
+ source: form.value.projectSource,
+ managerId: form.value.managerId ?? null,
+ managerName: form.value.creatorName,
+ salesmanId: form.value.salesmanId ?? null,
+ salesmanName: form.value.salesmanName ?? '',
+ planStartTime: form.value.planStartDate,
+ planEndTime: form.value.planEndDate,
+ actualStartTime: form.value.actualStartDate,
+ actualEndTime: form.value.actualEndDate,
+ status: form.value.billStatus === '' || form.value.billStatus === undefined || form.value.billStatus === null ? null : Number(form.value.billStatus),
+ departmentId: form.value.departmentId ?? null,
+ departmentName: form.value.departmentName ?? '',
+ orderDate: form.value.orderDate,
+ orderAmount: form.value.projectAmount,
+ reviewStatus: form.value.auditStatus === '' || form.value.auditStatus === undefined || form.value.auditStatus === null ? null : Number(form.value.auditStatus),
+ stage: form.value.projectStage === '' || form.value.projectStage === undefined || form.value.projectStage === null ? null : Number(form.value.projectStage),
+ remark: form.value.remark,
+ attachmentIds: Array.isArray(form.value.attachmentIds) ? form.value.attachmentIds : [],
+ teamList
+ }
+
+ const payload = {
+ info,
+ shippingAddress,
+ contractInfo,
+ salesLedgerProductList: productData.value
+ }
+
+ const req = operationType.value === 'edit' ? updateProject : addProject
+ const res = await req(payload)
+ if (res?.code === 200) {
+ ElMessage.success('淇濆瓨鎴愬姛')
+ closeDialog()
+ emit('completed')
+ return
+ }
+ ElMessage.error(res?.msg || '淇濆瓨澶辫触')
+}
+
+defineExpose({ openDialog })
+</script>
+
+<style scoped lang="scss">
+.section {
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ margin-bottom: 14px;
+ background: #fff;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ cursor: pointer;
+}
+
+.section-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.section-bar {
+ width: 3px;
+ height: 14px;
+ background: #002FA7;
+ border-radius: 2px;
+}
+
+.section-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.toggle-icon {
+ color: #909399;
+}
+
+.section-body {
+ padding: 0 14px 14px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+}
+.attachment-upload{
+
+}
+</style>
diff --git a/src/views/projectManagement/Management/index.vue b/src/views/projectManagement/Management/index.vue
new file mode 100644
index 0000000..7547209
--- /dev/null
+++ b/src/views/projectManagement/Management/index.vue
@@ -0,0 +1,430 @@
+<template>
+ <div class="app-container">
+ <SearchPanel
+ v-model="queryParams"
+ :schema="searchSchema"
+ @search="handleQuery"
+ @reset="resetQuery"
+ >
+ <template #billStatus="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨鍗曟嵁鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in bill_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ <template #auditStatus="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨璁″垝鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in project_management" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ <template #projectStage="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨瀹℃牳鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in plan_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ </SearchPanel>
+
+ <div class="table-container">
+ <div class="table-actions">
+ <el-button style="background-color: #002FA7; color: #fff" @click="handleAdd">鏂板</el-button>
+ <!-- <el-dropdown split-button type="default" @command="handleGenerateBill" style="margin-left: 10px;">
+ 鐢熸垚鍗曟嵁
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="1">鐢熸垚鍗曟嵁1</el-dropdown-item>
+ <el-dropdown-item command="2">鐢熸垚鍗曟嵁2</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown> -->
+ <el-button :loading="submitLoading" @click="handleSubmit">鎻愪氦</el-button>
+ <el-button :loading="auditLoading" @click="handleAudit">瀹℃牳</el-button>
+ <el-button :loading="reverseAuditLoading" @click="handleReverseAudit">鍙嶅鏍�</el-button>
+ <el-button :loading="deleteLoading" @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+
+ <PIMTable
+ :column="columns"
+ :tableData="tableData"
+ :page="pagination"
+ :tableLoading="loading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="handlePagination"
+ >
+ <template #auditStatus="{ row }">
+ <dict-tag :options="project_management" :value="row.auditStatus" />
+ </template>
+ <template #projectStage="{ row }">
+ <dict-tag :options="plan_status" :value="row.projectStage" />
+ </template>
+ <template #action="{ row }">
+ <el-button link type="primary" @click="handleEdit(row)">缂栬緫</el-button>
+ <el-button link type="primary" :loading="progressBtnLoadingId===row.id" @click="handleProgressReport(row)">杩涘害姹囨姤</el-button>
+ <el-button link type="primary" @click="handleDetail(row)">璇︽儏</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDia ref="formDiaRef" @completed="getList" />
+ <ProgressReportDialog
+ v-model="progressReportVisible"
+ :project-id="progressProjectId"
+ :project-info="progressProjectInfo"
+ :plan-nodes="progressPlanNodes"
+ :default-plan-node-id="progressDefaultPlanNodeId"
+ @submitted="handleProgressSubmitted"
+ />
+ </div>
+</template>
+
+<script setup name="ProjectManagement">
+import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
+import { useRouter } from 'vue-router'
+import SearchPanel from '@/components/SearchPanel/index.vue'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import FormDia from './components/formDia.vue'
+import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue'
+import {
+ listProject,
+ delProject,
+ submitProject,
+ auditProject,
+ reverseAuditProject,
+ getProject,
+ saveStage
+} from '@/api/projectManagement/project'
+import { listPlan } from '@/api/projectManagement/projectType'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import useUserStore from '@/store/modules/user'
+
+const { proxy } = getCurrentInstance()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+const router = useRouter()
+const userStore = useUserStore()
+
+const loading = ref(false)
+const ids = ref([])
+const tableData = ref([])
+const formDiaRef = ref()
+const progressReportVisible = ref(false)
+const progressProjectId = ref(undefined)
+const progressProjectInfo = ref({})
+const progressPlanNodes = ref([])
+const progressDefaultPlanNodeId = ref(undefined)
+const progressBtnLoadingId = ref(null)
+const submitLoading = ref(false)
+const auditLoading = ref(false)
+const reverseAuditLoading = ref(false)
+const deleteLoading = ref(false)
+
+const data = reactive({
+ queryParams: {
+ projectNameOrCode: undefined,
+ customerName: undefined,
+ billStatus: undefined,
+ projectStage: undefined,
+ auditStatus: undefined,
+ salesperson: undefined,
+ pageNum: 1,
+ pageSize: 10
+ },
+ pagination: {
+ current: 1,
+ size: 10,
+ total: 0,
+ layout: 'total, sizes, prev, pager, next, jumper'
+ }
+})
+
+const { queryParams, pagination } = toRefs(data)
+
+const searchSchema = [
+ { prop: 'projectNameOrCode', label: '椤圭洰鍚嶇О/缂栧彿', type: 'input', placeholder: '璇疯緭鍏ラ」鐩悕绉�/缂栧彿' },
+ { prop: 'customerName', label: '瀹㈡埛鍚嶇О', type: 'input', placeholder: '璇疯緭鍏ュ鎴峰悕绉�' },
+ { prop: 'billStatus', label: '鍗曟嵁鐘舵��', slot: 'billStatus' },
+ { prop: 'projectStage', label: '璁″垝鐘舵��', slot: 'projectStage' },
+ { prop: 'auditStatus', label: '瀹℃牳鐘舵��', slot: 'auditStatus' },
+ { prop: 'salesperson', label: '涓氬姟浜哄憳', type: 'input', placeholder: '璇疯緭鍏ヤ笟鍔′汉鍛�' }
+]
+
+const columns = [
+ { label: '鍗曟嵁缂栧彿', prop: 'billNo', align: 'center', width: '150' },
+ { label: '椤圭洰鍚嶇О', prop: 'projectName', align: 'center' },
+ { label: '瀹℃牳鐘舵��', prop: 'auditStatus', align: 'center', dataType: 'slot', slot: 'auditStatus' },
+ { label: '瀹㈡埛鍚嶇О', prop: 'customerName', align: 'center' },
+ { label: '绔嬮」鏃ユ湡', prop: 'setupDate', align: 'center', width: '120' },
+ { label: '椤圭洰鏉ユ簮', prop: 'projectSource', align: 'center' },
+ { label: '椤圭洰鍒嗙被', prop: 'projectClassification', align: 'center' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: '250', dataType: 'slot', slot: 'action', fixed: 'right' }
+]
+
+function getList() {
+ loading.value = true
+ const params = {
+ noOrName: queryParams.value.projectNameOrCode,
+ clientName: queryParams.value.customerName,
+ salesmanName: queryParams.value.salesperson,
+ reviewStatus: queryParams.value.auditStatus,
+ stage: queryParams.value.projectStage,
+ current: queryParams.value.pageNum,
+ size: queryParams.value.pageSize
+ }
+ listProject(params)
+ .then(response => {
+ const records = response?.data?.records || response?.rows || response?.records || []
+ const billFilter = queryParams.value.billStatus
+ const filtered = billFilter === undefined || billFilter === null || billFilter === ''
+ ? records
+ : records.filter(r => String(r.billStatus ?? r.status) === String(billFilter))
+ tableData.value = filtered.map(r => ({
+ id: r.id,
+ billNo: r.no ?? r.billNo,
+ projectName: r.title ?? r.projectName,
+ billStatus: r.billStatus ?? r.status,
+ auditStatus: r.reviewStatus ?? r.auditStatus,
+ projectStage: r.stage ?? r.projectStage,
+ customerName: r.clientName ?? r.customerName,
+ parentProject: r.parentTitle ?? r.parentName ?? r.parentProject,
+ setupDate: r.establishTime ?? r.setupDate,
+ projectType: r.planName ?? r.projectType,
+ projectSource: r.source ?? r.projectSource,
+ projectClassification: r.departmentName ?? r.projectClassification,
+ raw: r
+ }))
+ pagination.value.total = response?.total || response?.data?.total || 0
+ })
+ .finally(() => {
+ loading.value = false
+ })
+}
+
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ pagination.value.current = 1
+ getList()
+}
+
+function resetQuery() {
+ queryParams.value = {
+ projectNameOrCode: undefined,
+ customerName: undefined,
+ billStatus: undefined,
+ projectStage: undefined,
+ auditStatus: undefined,
+ salesperson: undefined,
+ pageNum: 1,
+ pageSize: 10
+ }
+ handleQuery()
+}
+
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.id)
+}
+
+function handlePagination({ page, limit }) {
+ queryParams.value.pageNum = page
+ queryParams.value.pageSize = limit
+ pagination.value.current = page
+ pagination.value.size = limit
+ getList()
+}
+
+function handleAdd() {
+ formDiaRef.value?.openDialog({ operationType: 'add' })
+}
+
+async function handleDelete() {
+ const delIds = ids.value
+ if (delIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁椤�')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭鍒犻櫎鎵�閫夋暟鎹」?', '璀﹀憡', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ deleteLoading.value = true
+ await delProject(delIds)
+ getList()
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ } catch {} finally {
+ deleteLoading.value = false
+ }
+}
+
+async function handleSubmit() {
+ const submitIds = ids.value
+ if (submitIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佹彁浜ょ殑鏁版嵁椤�')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭鎻愪氦鎵�閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ submitLoading.value = true
+ await Promise.all(submitIds.map(id => submitProject({ id })))
+ getList()
+ ElMessage.success('鎻愪氦鎴愬姛')
+ } catch {} finally {
+ submitLoading.value = false
+ }
+}
+
+async function handleAudit() {
+ const auditIds = ids.value
+ if (auditIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸鏍哥殑鏁版嵁椤�')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭瀹℃牳鎵�閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ auditLoading.value = true
+ await Promise.all(auditIds.map(id => auditProject({ id })))
+ getList()
+ ElMessage.success('瀹℃牳鎴愬姛')
+ } catch {} finally {
+ auditLoading.value = false
+ }
+}
+
+async function handleReverseAudit() {
+ const reverseAuditIds = ids.value
+ if (reverseAuditIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸弽瀹℃牳鐨勬暟鎹」')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭鍙嶅鏍告墍閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ reverseAuditLoading.value = true
+ await Promise.all(reverseAuditIds.map(id => reverseAuditProject({ id })))
+ getList()
+ ElMessage.success('鍙嶅鏍告垚鍔�')
+ } catch {} finally {
+ reverseAuditLoading.value = false
+ }
+}
+
+function handleGenerateBill(command) {
+ ElMessage.info(`鐢熸垚鍗曟嵁: ${command}`)
+}
+
+function computeDefaultPlanNodeId(stageVal, nodes) {
+ const list = Array.isArray(nodes) ? nodes : []
+ if (list.length === 0) return undefined
+ const direct = list.find(n => String(n.id) === String(stageVal))
+ if (direct?.id) return direct.id
+ const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ const idx = Number(stageVal)
+ if (Number.isFinite(idx)) {
+ const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0]
+ if (byIndex?.id) return byIndex.id
+ }
+ return sorted[0]?.id
+}
+
+async function handleProgressReport(row) {
+ if (!row?.id) return
+ try {
+ progressBtnLoadingId.value = row.id
+ const res = await getProject(row.id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ const info = detail?.info || {}
+ progressProjectId.value = info.id
+ progressProjectInfo.value = info
+
+ const planId = info.projectManagementPlanId
+ if (planId) {
+ const planRes = await listPlan({ current: 1, size: 999 })
+ const records = planRes?.data?.records || planRes?.records || planRes?.rows || []
+ const plan = (records || []).find(p => String(p.id) === String(planId)) || {}
+ progressPlanNodes.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : []
+ } else {
+ progressPlanNodes.value = []
+ }
+
+ progressDefaultPlanNodeId.value = computeDefaultPlanNodeId(info.stage, progressPlanNodes.value)
+ progressReportVisible.value = true
+ } catch (e) {
+ ElMessage.error('鑾峰彇椤圭洰璇︽儏澶辫触')
+ } finally {
+ progressBtnLoadingId.value = null
+ }
+}
+
+async function handleProgressSubmitted(payload) {
+ try {
+ const nodes = Array.isArray(progressPlanNodes.value) ? progressPlanNodes.value : []
+ const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
+ const description = payload.remark
+ ? `${payload.reportDate || ''} ${payload.remark}`.trim()
+ : `${payload.reportDate || ''} 杩涘害姹囨姤`.trim()
+ const req = {
+ id: null,
+ projectManagementPlanNodeId: payload.planNodeId,
+ projectManagementInfoId: progressProjectId.value,
+ description,
+ actualLeaderId: userStore.id || progressProjectInfo.value?.managerId,
+ actualLeaderName: userStore.nickName || progressProjectInfo.value?.managerName,
+ estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
+ planStartTime: payload.planStartTime || progressProjectInfo.value?.planStartTime,
+ planEndTime: payload.planEndTime || progressProjectInfo.value?.planEndTime,
+ actualStartTime: payload.actualStartTime || null,
+ actualEndTime: payload.actualEndTime || null,
+ progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0,
+ attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : []
+ }
+ const res = await saveStage(req)
+ if (res?.code === 200) {
+ ElMessage.success('鎻愪氦鎴愬姛')
+ getList()
+ return
+ }
+ ElMessage.error(res?.msg || '鎻愪氦澶辫触')
+ } catch (e) {
+ ElMessage.error('鎻愪氦澶辫触')
+ }
+}
+
+function handleDetail(row) {
+ if (!row?.id) return
+ router.push(`/projectManagement/Management/detail/${row.id}`)
+}
+
+function handleEdit(row) {
+ formDiaRef.value?.openDialog({ operationType: 'edit', row })
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+.app-container {
+ padding: 20px;
+}
+.table-container {
+ background-color: #fff;
+ padding: 20px;
+ border-radius: 4px;
+}
+.table-actions {
+ margin-bottom: 15px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+</style>
diff --git a/src/views/projectManagement/Management/projectDetail.vue b/src/views/projectManagement/Management/projectDetail.vue
new file mode 100644
index 0000000..c54a389
--- /dev/null
+++ b/src/views/projectManagement/Management/projectDetail.vue
@@ -0,0 +1,538 @@
+<template>
+ <div class="app-container">
+ <el-card class="header-card" shadow="never">
+ <div class="header-row">
+ <div class="header-title">椤圭洰璇︽儏</div>
+ <div class="header-actions">
+ <el-button style="color: white;background: #002FA7;" @click="openProgressReport">杩涘害姹囨姤</el-button>
+ <!-- <el-button type="danger" @click="openDiscussProgress">娲借皥杩涘害</el-button> -->
+ <el-button @click="goBack">杩斿洖</el-button>
+ </div>
+ </div>
+ <el-steps v-if="steps.length > 0" :active="activeStep" align-center finish-status="success">
+ <el-step v-for="(s, idx) in steps" :key="idx" :title="s" />
+ </el-steps>
+ </el-card>
+
+ <el-card class="content-card" shadow="never" v-loading="loading">
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="鍩虹璧勬枡" name="base">
+ <el-descriptions :column="4" border>
+ <el-descriptions-item label="椤圭洰ID">{{ info.id ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曟嵁缂栧彿">{{ info.no || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鍚嶇О">{{ info.title || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ info.clientName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛ID">{{ info.clientId ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鐖堕」鐩�">{{ parentProjectLabel }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰绫诲瀷">{{ projectTypeLabel }}</el-descriptions-item>
+ <el-descriptions-item label="绔嬮」鏃ユ湡">{{ info.establishTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鏉ユ簮">{{ info.source || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="绔嬮」浜�">{{ info.managerName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="涓氬姟鍛�">{{ info.salesmanName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閮ㄩ棬">{{ info.departmentName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曟嵁鐘舵��">
+ <dict-tag :options="bill_status" :value="String(info.status ?? '')" />
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳鐘舵��">
+ <dict-tag :options="project_management" :value="String(info.reviewStatus ?? '')" />
+ </el-descriptions-item>
+ <el-descriptions-item label="璁″垝鐘舵��">
+ <dict-tag :options="plan_status" :value="String(info.stage ?? '')" />
+ </el-descriptions-item>
+ <el-descriptions-item label="棰勮宸ユ湡(澶�)">{{ estimatedDays }}</el-descriptions-item>
+ <el-descriptions-item label="璁″垝寮�濮嬫棩鏈�">{{ info.planStartTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="璁″垝瀹屾垚鏃ユ湡">{{ info.planEndTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹為檯寮�濮嬫棩鏈�">{{ info.actualStartTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹為檯瀹屾垚鏃ユ湡">{{ info.actualEndTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="璁㈠崟鏃ユ湡">{{ info.orderDate || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰閲戦">{{ info.orderAmount ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞" :span="4">{{ info.remark || '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="attachment-block" v-if="attachments.length > 0">
+ <div class="attachment-title">闄勪欢</div>
+ <div class="attachment-list">
+ <div v-for="(att, idx) in attachments" :key="att.id || att.url || idx" class="attachment-item">
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </div>
+
+ <el-divider content-position="left">浜у搧淇℃伅</el-divider>
+ <el-table :data="productRows" border show-summary :summary-method="summarizeProductTable">
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="90" />
+ <el-table-column label="鏁伴噺" prop="quantity" width="90" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" width="90" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍙戠エ绫诲瀷" prop="invoiceType" width="110" />
+ </el-table>
+
+ <el-divider content-position="left">椤圭洰鍥㈤槦</el-divider>
+ <el-table :data="teamRows" border>
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="濮撳悕" prop="userName" show-overflow-tooltip />
+ <el-table-column label="椤圭洰缁勮鑹�" prop="userRoleName" show-overflow-tooltip />
+ <el-table-column label="杩涘叆鏃ユ湡" prop="joinTime" width="140" />
+ <el-table-column label="绂诲紑鏃ユ湡" prop="departTime" width="140" />
+ <el-table-column label="鑱旂郴鏂瑰紡" prop="contact" show-overflow-tooltip />
+ <el-table-column label="澶囨敞" prop="remark" show-overflow-tooltip />
+ </el-table>
+
+ <el-divider content-position="left">鏀惰揣鍦板潃</el-divider>
+ <el-descriptions :column="3" border>
+ <el-descriptions-item label="鏀惰揣浜�">{{ shippingAddress.consignee || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鏂瑰紡">{{ shippingAddress.contract || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍦板潃">{{ shippingAddress.address || '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <el-divider content-position="left">鑱旂郴淇℃伅</el-divider>
+ <el-descriptions :column="4" border>
+ <el-descriptions-item label="鑱旂郴浜�">{{ contractInfo.name || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鎬у埆">{{ contractInfo.sex || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鐢熸棩">{{ contractInfo.birthday || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閮ㄩ棬">{{ contractInfo.department || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鑱屽姟">{{ contractInfo.job || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満鍙�">{{ contractInfo.phoneNumber || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閭">{{ contractInfo.email || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="QQ">{{ contractInfo.qq || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍥哄畾鐢佃瘽">{{ contractInfo.lineaFissa || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="寰俊">{{ contractInfo.wx || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="绫嶈疮">{{ contractInfo.origineEtnica || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="娉曚汉浠h〃">{{ contractInfo.rappresentanteLegale || '-' }}</el-descriptions-item>
+ </el-descriptions>
+ </el-tab-pane>
+
+ <el-tab-pane label="椤圭洰绫诲瀷" name="plan">
+ <el-descriptions :column="4" border>
+ <el-descriptions-item label="绫诲瀷鍚嶇О">{{ projectPlan.name || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞" :span="3">{{ projectPlan.description || '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="attachment-block" v-if="planAttachments.length > 0">
+ <div class="attachment-title">绫诲瀷闄勪欢</div>
+ <div class="attachment-list">
+ <div v-for="(att, idx) in planAttachments" :key="att.id || att.url || idx" class="attachment-item">
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </div>
+
+ <el-table :data="planNodeRows" border style="margin-top: 14px;">
+ <el-table-column align="center" label="姝ラ" type="index" width="80" />
+ <el-table-column label="闃舵鍚嶇О" prop="name" min-width="160" show-overflow-tooltip />
+ <el-table-column label="璐熻矗浜�" prop="leaderName" width="140" show-overflow-tooltip />
+ <el-table-column label="棰勮宸ユ湡(澶�)" prop="estimatedDuration" width="140" />
+ <el-table-column label="宸ユ椂鍗曚环" prop="hourlyRate" width="120" />
+ <el-table-column label="浣滀笟鍐呭" prop="workContent" min-width="180" show-overflow-tooltip />
+ </el-table>
+ </el-tab-pane>
+
+ <el-tab-pane label="椤圭洰闃舵" name="stage">
+ <el-table :data="stageNodeRows" border style="margin-top: 14px;">
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="闃舵" prop="stageName" min-width="160" show-overflow-tooltip />
+ <el-table-column label="鎻忚堪" prop="description" min-width="220" show-overflow-tooltip />
+ <el-table-column label="瀹為檯璐熻矗浜�" prop="actualLeaderName" width="140" show-overflow-tooltip />
+ <el-table-column label="杩涘害(%)" prop="progress" width="110" />
+ <el-table-column label="璁″垝寮�濮�" prop="planStartTime" width="120" />
+ <el-table-column label="璁″垝缁撴潫" prop="planEndTime" width="120" />
+ <el-table-column label="瀹為檯寮�濮�" prop="actualStartTime" width="120" />
+ <el-table-column label="瀹為檯缁撴潫" prop="actualEndTime" width="120" />
+ <el-table-column label="棰勮宸ユ湡(澶�)" prop="estimatedDuration" width="130" />
+ <el-table-column label="闄勪欢" width="90" align="center">
+ <template #default="{ row }">
+ <span>{{ row.attachmentCount }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column v-if="false" label="鎿嶄綔" width="100" align="center" fixed="right" >
+ <template #default="{ row }">
+ <el-button link type="danger" size="small" @click="handleDeleteStage(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </div>
+
+ <ProgressReportDialog
+ v-model="progressReportVisible"
+ :project-id="info.id"
+ :project-info="info"
+ :plan-nodes="planNodeRows"
+ :default-plan-node-id="defaultPlanNodeId"
+ @submitted="handleProgressSubmitted"
+ />
+ <DiscussProgressDialog
+ v-model="discussProgressVisible"
+ :project-id="info.id"
+ :plan-nodes="planNodeRows"
+ :default-plan-node-id="defaultPlanNodeId"
+ @submitted="handleDiscussSubmitted"
+ />
+</template>
+
+<script setup name="ProjectManagementDetail">
+import { computed, getCurrentInstance, onMounted, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { Document } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getProject, saveStage, listStage, deleteStage } from '@/api/projectManagement/project'
+import { listPlan } from '@/api/projectManagement/projectType'
+import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue'
+import DiscussProgressDialog from '@/components/ProjectManagement/DiscussProgressDialog.vue'
+import useUserStore from '@/store/modules/user'
+
+const { proxy } = getCurrentInstance()
+const route = useRoute()
+const router = useRouter()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+
+const loading = ref(false)
+const activeTab = ref('base')
+const progressReportVisible = ref(false)
+const discussProgressVisible = ref(false)
+const userStore = useUserStore()
+
+const info = ref({})
+const shippingAddress = ref({})
+const contractInfo = ref({})
+const productRows = ref([])
+const teamRows = ref([])
+const attachments = ref([])
+const projectTypeMap = ref(new Map())
+const projectPlan = ref({})
+const planNodeRows = ref([])
+const planAttachments = ref([])
+const stageNodeRows = ref([])
+
+const estimatedDays = computed(() => {
+ const raw = info.value?.estimatedDays
+ const n = Number(raw)
+ if (Number.isFinite(n) && n > 0) return n
+ const start = info.value?.planStartTime
+ const end = info.value?.planEndTime
+ if (!start || !end) return 0
+ const startTime = new Date(`${start}T00:00:00`).getTime()
+ const endTime = new Date(`${end}T00:00:00`).getTime()
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime < startTime) return 0
+ return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
+})
+
+const projectTypeLabel = computed(() => {
+ const id = info.value?.projectManagementPlanId
+ if (id === undefined || id === null || id === '') return '-'
+ const p = projectTypeMap.value.get(Number(id))
+ return p?.name || String(id)
+})
+
+const parentProjectLabel = computed(() => {
+ return (
+ info.value?.parentTitle ||
+ info.value?.projectManagementInfoParentName ||
+ info.value?.projectManagementInfoParentId ||
+ '-'
+ )
+})
+
+const planStageEnum = computed(() => {
+ const list = Array.isArray(plan_status) ? plan_status : []
+ return list.map(i => ({ value: String(i.value), label: i.label }))
+})
+
+const steps = computed(() => {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ const labels = sorted.map(i => i.name || i.workContent).filter(Boolean)
+ return labels.length > 0 ? labels : planStageEnum.value.map(i => i.label)
+})
+
+const activeStep = computed(() => {
+ const statusOrStage = info.value?.stage ?? info.value?.status
+ const enumList = planStageEnum.value
+ // 浼樺厛浣跨敤 planStageEnum 鐨� value 鍖归厤
+ const found = enumList.find(i => i.value === String(statusOrStage))
+ const label = found?.label
+ // 鍦ㄩ」鐩被鍨嬭妭鐐逛腑鏌ユ壘瀵瑰簲 label 鐨勪笅鏍�
+ const nodeLabels = steps.value
+ const idxByLabel = label ? nodeLabels.findIndex(l => String(l) === String(label)) : -1
+ if (idxByLabel >= 0) return idxByLabel + 1
+ // 鍥為��锛氬鏋� statusOrStage 鏄暟瀛楃储寮�
+ const n = Number(statusOrStage)
+ if (Number.isFinite(n) && n > 0) return Math.min(n, nodeLabels.length)
+ return 0
+})
+
+function goBack() {
+ router.back()
+}
+
+function openProgressReport() {
+ progressReportVisible.value = true
+}
+
+function openDiscussProgress() {
+ discussProgressVisible.value = true
+}
+
+const defaultPlanNodeId = computed(() => {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ if (nodes.length === 0) return undefined
+ const stageVal = info.value?.stage
+ const direct = nodes.find(n => String(n.id) === String(stageVal))
+ if (direct?.id) return direct.id
+ const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ const idx = Number(stageVal)
+ if (Number.isFinite(idx)) {
+ const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0]
+ if (byIndex?.id) return byIndex.id
+ }
+ return sorted[0]?.id
+})
+
+async function handleProgressSubmitted(payload) {
+ try {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
+ const description = payload.remark
+ ? `${payload.reportDate || ''} ${payload.remark}`.trim()
+ : `${payload.reportDate || ''} 杩涘害姹囨姤`.trim()
+ const req = {
+ id: null,
+ projectManagementPlanNodeId: payload.planNodeId,
+ projectManagementInfoId: info.value?.id,
+ description,
+ actualLeaderId: userStore.id || info.value?.managerId,
+ actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName,
+ estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
+ planStartTime: payload.planStartTime || info.value?.planStartTime,
+ planEndTime: payload.planEndTime || info.value?.planEndTime,
+ actualStartTime: payload.actualStartTime || null,
+ actualEndTime: payload.actualEndTime || null,
+ progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0,
+ attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : []
+ }
+ const res = await saveStage(req)
+ if (res?.code === 200) {
+ ElMessage.success('鎻愪氦鎴愬姛')
+ await Promise.all([loadDetail(), loadStageList()])
+ return
+ }
+ ElMessage.error(res?.msg || '鎻愪氦澶辫触')
+ } catch (e) {
+ ElMessage.error('鎻愪氦澶辫触')
+ }
+}
+
+async function handleDiscussSubmitted(payload) {
+ try {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
+ const req = {
+ id: null,
+ projectManagementPlanNodeId: payload.planNodeId,
+ projectManagementInfoId: info.value?.id,
+ description: payload.remark,
+ actualLeaderId: userStore.id || info.value?.managerId,
+ actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName,
+ estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
+ planStartTime: info.value?.planStartTime,
+ planEndTime: info.value?.planEndTime,
+ actualStartTime: info.value?.actualStartTime || null,
+ actualEndTime: info.value?.actualEndTime || null,
+ progress: Number(info.value?.progress ?? 0) || 0,
+ attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : []
+ }
+ const res = await saveStage(req)
+ if (res?.code === 200) {
+ ElMessage.success('鎻愪氦鎴愬姛')
+ await Promise.all([loadDetail(), loadStageList()])
+ return
+ }
+ ElMessage.error(res?.msg || '鎻愪氦澶辫触')
+ } catch (e) {
+ ElMessage.error('鎻愪氦澶辫触')
+ }
+}
+
+function downloadAttachment(att) {
+ if (att?.url) {
+ try {
+ proxy.$download.resource(att.url)
+ return
+ } catch (e) {}
+ }
+ if (att?.name) {
+ try {
+ proxy.$download.name(att.name, false)
+ return
+ } catch (e) {}
+ }
+ ElMessage.warning('闄勪欢鏆傛棤涓嬭浇鍦板潃')
+}
+
+async function loadProjectTypeMap() {
+ try {
+ const res = await listPlan({ current: 1, size: 999 })
+ const records = res?.data?.records || res?.records || res?.rows || []
+ projectTypeMap.value = new Map((records || []).map(r => [Number(r.id), r]))
+ } catch {
+ projectTypeMap.value = new Map()
+ }
+}
+
+function getPlanNodeName(planNodeId) {
+ const list = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const node = list.find(n => String(n.id) === String(planNodeId))
+ return node?.name || node?.workContent || String(planNodeId ?? '')
+}
+
+async function loadStageList() {
+ const projectId = info.value?.id
+ if (!projectId) {
+ stageNodeRows.value = []
+ return
+ }
+ try {
+ const res = await listStage(projectId)
+ const data = res?.data?.data ?? res?.data ?? res
+ const list = data?.records || data?.rows || data?.list || data || []
+ const records = Array.isArray(list) ? list : []
+ stageNodeRows.value = records.map(r => {
+ const attachmentList = Array.isArray(r.attachmentList) ? r.attachmentList : []
+ const attachmentIds = Array.isArray(r.attachmentIds) ? r.attachmentIds : []
+ return {
+ ...r,
+ stageName: getPlanNodeName(r.projectManagementPlanNodeId),
+ attachmentCount: attachmentList.length || attachmentIds.length || 0
+ }
+ })
+ } catch {
+ stageNodeRows.value = []
+ }
+}
+
+async function handleDeleteStage(row) {
+ const stageId = row?.id
+ if (!stageId) return
+ try {
+ await ElMessageBox.confirm('鏄惁纭鍒犻櫎璇ラ」鐩樁娈碉紵', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ const res = await deleteStage(stageId)
+ if (res?.code === 200) {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await loadStageList()
+ return
+ }
+ ElMessage.error(res?.msg || '鍒犻櫎澶辫触')
+ } catch {}
+}
+
+function syncProjectPlan() {
+ const id = info.value?.projectManagementPlanId
+ if (id === undefined || id === null || id === '') {
+ projectPlan.value = {}
+ planNodeRows.value = []
+ planAttachments.value = []
+ return
+ }
+ const plan = projectTypeMap.value.get(Number(id)) || {}
+ projectPlan.value = plan || {}
+ planNodeRows.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : []
+ planAttachments.value = Array.isArray(plan?.attachmentList) ? plan.attachmentList : []
+}
+
+async function loadDetail() {
+ const id = route.params?.id
+ if (!id) return
+ loading.value = true
+ try {
+ const res = await getProject(id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ info.value = detail?.info || {}
+ shippingAddress.value = detail?.shippingAddress || {}
+ contractInfo.value = detail?.contractInfo || {}
+ productRows.value = Array.isArray(detail?.salesLedgerProductList) ? detail.salesLedgerProductList : []
+ teamRows.value = Array.isArray(detail?.info?.teamList) ? detail.info.teamList : []
+ attachments.value = Array.isArray(detail?.info?.attachmentList) ? detail.info.attachmentList : []
+ syncProjectPlan()
+ await loadStageList()
+ } finally {
+ loading.value = false
+ }
+}
+
+onMounted(async () => {
+ await loadProjectTypeMap()
+ await loadDetail()
+})
+</script>
+
+<style scoped lang="scss">
+.section-bar {
+ width: 3px;
+ height: 14px;
+ background: #002FA7;
+ border-radius: 2px;
+}
+.header-card {
+ margin-bottom: 14px;
+}
+
+.header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.header-title {
+ font-weight: 600;
+ font-size: 16px;
+}
+
+.content-card {
+ border-radius: 8px;
+}
+
+.attachment-block {
+ margin-top: 14px;
+}
+
+.attachment-title {
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.attachment-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.attachment-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.attachment-name {
+ max-width: 520px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+</style>
+
diff --git a/src/views/projectManagement/projectType/components/ProjectTypeDialog.vue b/src/views/projectManagement/projectType/components/ProjectTypeDialog.vue
new file mode 100644
index 0000000..2888516
--- /dev/null
+++ b/src/views/projectManagement/projectType/components/ProjectTypeDialog.vue
@@ -0,0 +1,471 @@
+<template>
+ <el-dialog
+ :title="title"
+ v-model="visible"
+ width="1000px"
+ append-to-body
+ @close="handleClose"
+ >
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="0">
+ <!-- 椤堕儴鍩虹淇℃伅 -->
+ <div class="base-info-row">
+ <div class="info-item">
+ <span class="item-label required">鍚嶇О</span>
+ <el-input
+ v-model="form.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ maxlength="10"
+ show-word-limit
+ style="width: 220px"
+ />
+ </div>
+ <div class="info-item">
+ <span class="item-label">澶囨敞</span>
+ <el-input
+ v-model="form.description"
+ placeholder="璇疯緭鍏ュ娉�"
+ maxlength="20"
+ show-word-limit
+ style="width: 220px"
+ />
+ </div>
+ <div class="info-item">
+ <span class="item-label">闄勪欢</span>
+ <el-upload
+ v-if="isEdit"
+ :action="uploadUrl"
+ :headers="uploadHeaders"
+ :on-success="handleUploadSuccess"
+ :on-remove="handleRemove"
+ v-model:file-list="uploadFileList"
+ :limit="3"
+ name="files"
+ multiple
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <span v-else class="text-gray-400 text-sm">璇峰厛淇濆瓨鍚庡啀涓婁紶闄勪欢</span>
+ </div>
+ </div>
+
+ <!-- 姝ラ閰嶇疆琛ㄦ牸 -->
+ <p class="top-tip">璇锋寜鐓ч『搴忛厤缃」鐩樁娈碉紝鎷栨嫿<b>姝ラ</b>鎺掑簭鍗冲彲</p>
+ <div class="step-table-container">
+ <el-table
+ :data="form.savePlanNodeList"
+ border
+ style="width: 100%"
+ row-key="id"
+ class="drag-table"
+ >
+ <el-table-column label="姝ラ" width="80" align="center" class-name="drag-handle">
+ <template #default="scope">
+ <div class="step-index" style="cursor: move;">
+ {{ scope.$index + 1 }}
+ </div>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="闃舵鍚嶇О" min-width="150">
+ <template #header>
+ <span class="required-star">*</span> 闃舵鍚嶇О
+ </template>
+ <template #default="scope">
+ <el-form-item
+ :prop="'savePlanNodeList.' + scope.$index + '.name'"
+ :rules="[{ required: true, message: '璇疯緭鍏ラ樁娈靛悕绉�', trigger: 'blur' }]"
+ >
+ <el-input v-model="scope.row.name" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="璐熻矗浜�" width="180">
+ <template #header>
+ <span class="required-star">*</span> 璐熻矗浜�
+ </template>
+ <template #default="scope">
+ <el-form-item
+ :prop="'savePlanNodeList.' + scope.$index + '.leaderId'"
+ :rules="[{ required: true, message: '璇烽�夋嫨璐熻矗浜�', trigger: 'change' }]"
+ >
+ <el-select
+ v-model="scope.row.leaderId"
+ placeholder="娴嬭瘯"
+ @change="(val) => handleLeaderChange(val, scope.row)"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="棰勮宸ユ湡 (澶�)" width="150">
+ <template #header>
+ 棰勮宸ユ湡 (澶�)
+ <el-tooltip content="瀹屾垚璇ラ樁娈甸璁¢渶瑕佺殑澶╂暟" placement="top">
+ <el-icon class="info-icon"><QuestionFilled /></el-icon>
+ </el-tooltip>
+ </template>
+ <template #default="scope">
+ <el-input-number
+ v-model="scope.row.estimatedDuration"
+ :min="0"
+ controls-position="right"
+ style="width: 100%"
+ />
+ </template>
+ </el-table-column>
+
+ <el-table-column label="宸ユ椂鍗曚环" width="120">
+ <template #default="scope">
+ <el-input v-model="scope.row.hourlyRate" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+
+ <el-table-column label="浣滀笟鍐呭" min-width="150">
+ <template #default="scope">
+ <el-input v-model="scope.row.workContent" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" min-width="150">
+ <template #default="scope">
+ <el-button type="danger" size="mini" @click="removeStep(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="add-row-btn" @click="addStep">
+ <el-icon><Plus /></el-icon> 鏂板涓�琛�
+ </div>
+ </div>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="submitForm">鎻愪氦</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref, watch, onMounted, nextTick } from 'vue';
+import { Plus, QuestionFilled } from '@element-plus/icons-vue';
+import { userListNoPageByTenantId } from '@/api/system/user';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { getToken } from '@/utils/auth';
+import Sortable from 'sortablejs';
+
+const props = defineProps({
+ modelValue: Boolean,
+ title: String,
+ data: Object
+});
+
+const emit = defineEmits(['update:modelValue', 'submit']);
+
+const visible = ref(false);
+const formRef = ref(null);
+const userOptions = ref([]);
+const isEdit = ref(false);
+const uploadHeaders = { Authorization: "Bearer " + getToken() };
+// 涓婁紶鍦板潃
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload";
+let sortable = null;
+
+const form = ref({
+ id: undefined,
+ name: '',
+ description: '',
+ attachmentIds: [],
+ savePlanNodeList: []
+});
+const uploadFileList = ref([]);
+
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }]
+};
+
+// 鐩戝惉寮圭獥鏄剧ず/闅愯棌
+watch(() => props.modelValue, (val) => {
+ visible.value = val;
+ if (val) {
+ if (props.data) {
+ // 缂栬緫妯″紡 - 鍥炴樉鏁版嵁
+ isEdit.value = true;
+ form.value = {
+ id: props.data.id,
+ name: props.data.name,
+ description: props.data.description,
+ attachmentIds: Array.isArray(props.data.attachmentIds)
+ ? props.data.attachmentIds
+ : (props.data.attachmentList || []).map(f => f.id).filter(Boolean),
+ savePlanNodeList: []
+ };
+
+ // 鍥炴樉姝ラ鑺傜偣
+ if (props.data.planNodeList && props.data.planNodeList.length > 0) {
+ form.value.savePlanNodeList = props.data.planNodeList.map(node => ({
+ id: node.id,
+ projectManagementPlanId: node.projectManagementPlanId,
+ sort: node.sort,
+ name: node.name,
+ leaderId: node.leaderId,
+ leaderName: node.leaderName,
+ estimatedDuration: node.estimatedDuration,
+ hourlyRate: node.hourlyRate,
+ workContent: node.workContent
+ }));
+ } else {
+ form.value.savePlanNodeList = [createDefaultNode()];
+ }
+ } else {
+ // 鏂板妯″紡
+ isEdit.value = false;
+ resetForm();
+ }
+ // 鍒濆鍖栨嫋鎷�
+ nextTick(() => {
+ initSortable();
+ });
+ }
+});
+
+watch(visible, (val) => {
+ emit('update:modelValue', val);
+});
+
+/** 鍒濆鍖栨嫋鎷� */
+function initSortable() {
+ const el = document.querySelector('.drag-table .el-table__body-wrapper tbody');
+ if (!el) return;
+
+ if (sortable) {
+ sortable.destroy();
+ }
+
+ sortable = Sortable.create(el, {
+ handle: '.drag-handle',
+ animation: 150,
+ onEnd: ({ newIndex, oldIndex }) => {
+ const targetRow = form.value.savePlanNodeList.splice(oldIndex, 1)[0];
+ form.value.savePlanNodeList.splice(newIndex, 0, targetRow);
+ }
+ });
+}
+
+/** 鍒涘缓榛樿鑺傜偣瀵硅薄 */
+function createDefaultNode() {
+ return {
+ name: '',
+ leaderId: null,
+ leaderName: null,
+ estimatedDuration: null,
+ hourlyRate: null,
+ workContent: null
+ };
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+function resetForm() {
+ form.value = {
+ id: undefined,
+ name: '',
+ description: '',
+ attachmentIds: [],
+ savePlanNodeList: [createDefaultNode()]
+ };
+ uploadFileList.value = [];
+ if (formRef.value) {
+ formRef.value.resetFields();
+ }
+}
+
+/** 鑾峰彇鐢ㄦ埛鍒楄〃 */
+async function getUserList() {
+ try {
+ const res = await userListNoPageByTenantId();
+ if (res.code === 200) {
+ userOptions.value = res.data || [];
+ }
+ } catch (error) {
+ console.error('鑾峰彇鐢ㄦ埛鍒楄〃澶辫触:', error);
+ }
+}
+
+/** 澶勭悊璐熻矗浜哄彉鍖� */
+function handleLeaderChange(val, row) {
+ const user = userOptions.value.find(u => u.userId === val);
+ if (user) {
+ row.leaderName = user.nickName;
+ }
+}
+
+/** 澶勭悊鏂囦欢涓婁紶鎴愬姛 */
+function handleUploadSuccess(response, file, fileList) {
+ if (response.code === 200) {
+ const newFile = response.data;
+ const list = Array.isArray(newFile) ? newFile : [newFile];
+ list.forEach(element => {
+ const id = element?.id;
+ if (id && !form.value.attachmentIds.includes(id)) {
+ form.value.attachmentIds.push(id);
+ }
+ });
+ } else {
+ ElMessage.error(response.msg || '涓婁紶澶辫触');
+ }
+}
+
+/** 澶勭悊鏂囦欢绉婚櫎 */
+function handleRemove(file) {
+ const removedId = file?.id || file?.response?.data?.id;
+ if (!removedId) return;
+ form.value.attachmentIds = form.value.attachmentIds.filter(id => id !== removedId);
+}
+
+/** 娣诲姞姝ラ */
+function addStep() {
+ form.value.savePlanNodeList.push(createDefaultNode());
+}
+
+/** 绉婚櫎姝ラ */
+function removeStep(index) {
+ if (form.value.savePlanNodeList.length <= 1) {
+ ElMessage.warning('鑷冲皯淇濈暀涓�涓楠�');
+ return;
+ }
+
+ ElMessageBox.confirm('鏄惁纭鍒犻櫎璇ユ楠わ紵', '绯荤粺鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ form.value.savePlanNodeList.splice(index, 1);
+ }).catch(() => {});
+}
+
+/** 绉诲姩姝ラ */
+function moveStep(index, direction) {
+ const targetIndex = index + direction;
+ if (targetIndex < 0 || targetIndex >= form.value.savePlanNodeList.length) return;
+
+ const list = form.value.savePlanNodeList;
+ const temp = list[index];
+ list[index] = list[targetIndex];
+ list[targetIndex] = temp;
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+async function submitForm() {
+ if (!formRef.value) return;
+
+ try {
+ const valid = await formRef.value.validate();
+ if (valid) {
+ // 鎻愪氦鍓嶈嚜鍔ㄥ~鍏� sort 瀛楁锛屾寜褰撳墠鏁扮粍椤哄簭鎺掑簭
+ form.value.savePlanNodeList.forEach((node, index) => {
+ node.sort = index;
+ });
+ emit('submit', form.value);
+ }
+ } catch (error) {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', error);
+ }
+}
+
+/** 鍏抽棴寮圭獥 */
+function handleClose() {
+ resetForm();
+}
+
+onMounted(() => {
+ getUserList();
+});
+</script>
+
+<style scoped lang="scss">
+.base-info-row {
+ display: flex;
+ gap: 40px;
+ margin-bottom: 25px;
+ padding: 0 10px;
+
+ .info-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .item-label {
+ font-size: 14px;
+ color: #606266;
+ white-space: nowrap;
+
+ &.required::before {
+ content: '*';
+ color: #f56c6c;
+ margin-right: 4px;
+ }
+ }
+ }
+}
+
+.step-table-container {
+ padding: 0 10px;
+
+ :deep(.el-form-item) {
+ margin-bottom: 0;
+ }
+
+ .required-star {
+ color: #f56c6c;
+ margin-right: 4px;
+ }
+
+ .info-icon {
+ font-size: 14px;
+ color: #909399;
+ margin-left: 4px;
+ cursor: pointer;
+ }
+
+ .add-row-btn {
+ margin-top: 15px;
+ height: 40px;
+ border: 1px dashed #dcdfe6;
+ border-radius: 4px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.3s;
+
+ &:hover {
+ border-color: #409eff;
+ background-color: #f0f7ff;
+ }
+ }
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 15px;
+ padding-top: 10px;
+}
+.top-tip {
+
+ font-size: 14px;
+ color: #606266;
+ margin:0 0 10px 10px;
+}
+</style>
diff --git a/src/views/projectManagement/projectType/index.vue b/src/views/projectManagement/projectType/index.vue
new file mode 100644
index 0000000..c2ea441
--- /dev/null
+++ b/src/views/projectManagement/projectType/index.vue
@@ -0,0 +1,516 @@
+<template>
+ <div class="app-container">
+ <div class="header-section">
+ <div class="title-container">
+ <span class="blue-bar"></span>
+ <span class="title-text">椤圭洰绫诲瀷</span>
+ </div>
+ <el-button type="primary" class="add-btn" @click="handleAdd">鏂板</el-button>
+ </div>
+
+ <div class="content-section" v-loading="loading">
+ <div class="card-list-scroll">
+ <div v-for="item in projectTypeList" :key="item.id" class="project-type-card">
+ <div class="card-header">
+ <div class="info-group">
+ <span class="label">绫诲瀷鍚嶇О:</span>
+ <span class="value">{{ item.name }}</span>
+ </div>
+ <div class="info-group">
+ <span class="label">澶囨敞:</span>
+ <span class="value">{{ item.description || '--' }}</span>
+ </div>
+ <div class="info-group">
+ <span class="label">闄勪欢:</span>
+ <div
+ class="attachment-info"
+ v-if="(item.attachmentList?.length || 0) > 0"
+ @click="handleExpand(item)"
+ >
+ {{ item.attachmentList[0]?.fileName || item.attachmentList[0]?.name }}
+ <span v-if="item.attachmentList.length > 1" class="file-count">
+ +{{ item.attachmentList.length - 1 }}
+ </span>
+ <span class="expand-link">{{ item.expanded ? '鏀惰捣' : '灞曞紑' }}</span>
+ </div>
+ <span class="value" v-else>--</span>
+ </div>
+ <div class="actions">
+ <el-button link type="primary" @click="handleUpdate(item)">缂栬緫</el-button>
+ <el-button link type="primary" @click="handleCopy(item)">澶嶅埗</el-button>
+ <el-button link type="danger" @click="handleDelete(item)">鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <el-collapse-transition>
+ <div v-show="item.expanded" class="expanded-content">
+ <div class="attachment-list">
+ <div
+ v-for="att in (item.attachmentList || [])"
+ :key="att.id || att.url || att.fileUrl || att.fileName || att.name"
+ class="attachment-item"
+ >
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.fileName || att.name || '--' }}</span>
+ <el-button link type="primary" size="small" @click="handleDownload(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </div>
+ </el-collapse-transition>
+
+ <div class="card-body">
+ <div class="workflow-container">
+ <div v-for="(step, index) in item.steps" :key="index" class="workflow-step">
+ <div class="step-main">
+ <div class="step-circle">{{ index + 1 }}</div>
+ <div v-if="index < item.steps.length - 1" class="step-line"></div>
+ </div>
+ <div class="step-label">{{ step.label }}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="pagination-container">
+ <el-pagination
+ v-model:current-page="queryParams.current"
+ v-model:page-size="queryParams.size"
+ :page-sizes="[10, 20, 30, 50]"
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="total"
+ @size-change="getList"
+ @current-change="getList"
+ />
+ </div>
+ </div>
+
+ <!-- 娣诲姞鎴栦慨鏀归」鐩被鍨嬪璇濇 -->
+ <ProjectTypeDialog
+ v-model="open"
+ :title="title"
+ :data="editData"
+ @submit="handleDialogSubmit"
+ />
+ </div>
+</template>
+
+<script setup name="ProjectType">
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
+import { Document, Download, ArrowDown } from '@element-plus/icons-vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { listPlan, savePlan, deletePlan } from '@/api/projectManagement/projectType';
+import ProjectTypeDialog from './components/ProjectTypeDialog.vue';
+
+const { proxy } = getCurrentInstance();
+
+// 閬僵灞�
+const loading = ref(false);
+// 鎬绘潯鏁�
+const total = ref(0);
+// 椤圭洰绫诲瀷琛ㄦ牸鏁版嵁
+const projectTypeList = ref([]);
+// 寮瑰嚭灞傛爣棰�
+const title = ref("");
+// 鏄惁鏄剧ず寮瑰嚭灞�
+const open = ref(false);
+// 缂栬緫鏁版嵁
+const editData = ref(null);
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10,
+});
+
+/** 鏌ヨ椤圭洰绫诲瀷鍒楄〃 */
+async function getList() {
+ loading.value = true;
+ try {
+ const res = await listPlan(queryParams);
+ if (res.code === 200) {
+ projectTypeList.value = res.data.records.map(item => ({
+ ...item,
+ expanded: false,
+ attachmentList: Array.isArray(item.attachmentList) ? item.attachmentList : [],
+ // 鍚庣杩斿洖鐨勮妭鐐瑰垪琛ㄥ彲鑳芥槸 planNodeList 鎴� savePlanNodeList
+ steps: (item.planNodeList || item.savePlanNodeList || []).map(node => ({
+ label: node.name
+ }))
+ }));
+ total.value = res.data.total;
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇鍒楄〃澶辫触");
+ }
+ } catch (error) {
+ console.error("鑾峰彇鍒楄〃澶辫触:", error);
+ // 濡傛灉鎺ュ彛涓嶉�氾紝鏆傛椂淇濈暀妯℃嫙鏁版嵁渚涙紨绀�
+ if (projectTypeList.value.length === 0) {
+ projectTypeList.value = [
+ {
+ id: 1,
+ name: 'A椤圭洰',
+ description: '',
+ attachmentList: [{ id: 1, fileName: 'precaution...' }],
+ steps: [{ label: '绔嬮」' }, { label: '璁捐' }, { label: '閲囪喘' }, { label: '鐢熶骇' }, { label: '鍑鸿揣' }],
+ expanded: false
+ }
+ ];
+ total.value = 1;
+ }
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ editData.value = null;
+ open.value = true;
+ title.value = "娣诲姞椤圭洰绫诲瀷";
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ editData.value = row;
+ open.value = true;
+ title.value = "淇敼椤圭洰绫诲瀷";
+}
+
+/** 寮圭獥鎻愪氦澶勭悊 */
+async function handleDialogSubmit(formData) {
+ try {
+ const res = await savePlan(formData);
+ if (res.code === 200) {
+ ElMessage.success(formData.id !== undefined ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+ open.value = false;
+ getList();
+ } else {
+ ElMessage.error(res.msg || "淇濆瓨澶辫触");
+ }
+ } catch (error) {
+ console.error("淇濆瓨澶辫触:", error);
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ ElMessageBox.confirm('鏄惁纭鍒犻櫎椤圭洰绫诲瀷缂栧彿涓�"' + row.id + '"鐨勬暟鎹」锛�', "绯荤粺鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }).then(async function() {
+ try {
+ const res = await deletePlan(row.id);
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ console.error("鍒犻櫎澶辫触:", error);
+ }
+ }).catch(() => {});
+}
+
+/** 澶嶅埗鎸夐挳鎿嶄綔 */
+async function handleCopy(row) {
+ const copyData = {
+ name: row.name + " - 鍓湰",
+ description: row.description,
+ attachmentIds: Array.isArray(row.attachmentIds)
+ ? row.attachmentIds
+ : (row.attachmentList || []).map(x => x.id).filter(Boolean),
+ savePlanNodeList: (row.planNodeList || row.savePlanNodeList || []).map(node => ({
+ name: node.name,
+ leaderId: node.leaderId,
+ leaderName: node.leaderName,
+ estimatedDuration: node.estimatedDuration,
+ hourlyRate: node.hourlyRate,
+ workContent: node.workContent
+ }))
+ };
+ try {
+ const res = await savePlan(copyData);
+ if (res.code === 200) {
+ ElMessage.success("澶嶅埗鎴愬姛");
+ getList();
+ } else {
+ ElMessage.error(res.msg || "澶嶅埗澶辫触");
+ }
+ } catch (error) {
+ console.error("澶嶅埗澶辫触:", error);
+ }
+}
+
+/** 灞曞紑/鏀惰捣闄勪欢 */
+function handleExpand(item) {
+ item.expanded = !item.expanded;
+}
+
+/** 涓嬭浇闄勪欢 */
+function handleDownload(attachment) {
+ const url = attachment?.url || attachment?.fileUrl || attachment?.tempPath || attachment?.fileName;
+ if (!url) {
+ ElMessage.warning("鏈壘鍒板彲涓嬭浇鐨勬枃浠跺湴鍧�");
+ return;
+ }
+ proxy.$download.name(url);
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.app-container {
+ background-color: #f5f7fa;
+ height: calc(100vh - 84px);
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.header-section {
+ flex-shrink: 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #fff;
+ padding: 15px 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+
+ .title-container {
+ display: flex;
+ align-items: center;
+
+ .blue-bar {
+ width: 4px;
+ height: 18px;
+ background-color: #409eff;
+ margin-right: 10px;
+ border-radius: 2px;
+ }
+
+ .title-text {
+ font-size: 16px;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .add-btn {
+ padding: 8px 20px;
+ }
+}
+
+.content-section{
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+
+.card-list-scroll {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px;
+}
+
+.project-type-card {
+ background-color: #fff;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ border: 1px solid #ebeef5;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .card-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 25px;
+ position: relative;
+
+ .info-group {
+ margin-right: 40px;
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+
+ .label {
+ color: #666;
+ margin-right: 8px;
+ }
+
+ .value {
+ color: #333;
+ }
+
+ .attachment-info {
+ display: flex;
+ align-items: center;
+ background-color: #f0f4ff;
+ padding: 4px 10px;
+ border-radius: 4px;
+ color: #409eff;
+ cursor: pointer;
+
+ .file-icon {
+ margin-right: 5px;
+ }
+
+ .file-name {
+ margin-right: 5px;
+ max-width: 100px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .file-count {
+ margin-right: 8px;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .download-icon {
+ font-size: 12px;
+ margin-right: 10px;
+ }
+
+ .expand-link {
+ color: #409eff;
+ margin-right: 5px;
+ }
+
+ .arrow-icon {
+ color: #409eff;
+ font-size: 12px;
+ }
+ }
+ }
+
+ .actions {
+ margin-left: auto;
+ }
+}
+
+.expanded-content {
+ padding: 0 20px 20px;
+ border-bottom: 1px dashed #ebeef5;
+ margin-bottom: 20px;
+
+ .attachment-list {
+ background-color: #f8f9fa;
+ padding: 15px;
+ border-radius: 4px;
+
+ .attachment-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 14px;
+ color: #606266;
+
+ .el-icon {
+ font-size: 16px;
+ color: #409eff;
+ }
+
+ .attachment-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
+}
+
+.card-body {
+ .workflow-container {
+ display: flex;
+ align-items: flex-start;
+ padding: 10px 0;
+ overflow-x: auto;
+
+ .workflow-step {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 120px;
+ flex-shrink: 0;
+
+ .step-main {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ position: relative;
+
+ .step-circle {
+ width: 24px;
+ height: 24px;
+ background-color: #409eff;
+ color: #fff;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 12px;
+ font-weight: bold;
+ z-index: 2;
+ margin: 0 auto;
+ }
+
+ .step-line {
+ position: absolute;
+ height: 2px;
+ background-color: #d9e6ff;
+ left: calc(50% + 12px);
+ right: calc(-50% + 12px);
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 1;
+ }
+ }
+
+ .step-label {
+ margin-top: 10px;
+ font-size: 13px;
+ color: #333;
+ text-align: center;
+ }
+ }
+ }
+ }
+}
+
+.pagination-container {
+ flex-shrink: 0;
+ display: flex;
+ justify-content: flex-end;
+ padding: 10px 20px;
+ background-color: #fff;
+ border-top: 1px solid #ebeef5;
+ margin-top: 0;
+}
+
+.step-config-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+</style>
diff --git a/src/views/projectManagement/roles/index.vue b/src/views/projectManagement/roles/index.vue
new file mode 100644
index 0000000..b3abe10
--- /dev/null
+++ b/src/views/projectManagement/roles/index.vue
@@ -0,0 +1,295 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px">
+ <el-form-item label="瑙掕壊鍚嶇О" prop="roleName">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヨ鑹插悕绉�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="瑙掕壊鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option label="鍚敤" value="0" />
+ <el-option label="绂佺敤" value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:role:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:role:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:role:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <!-- 琛ㄦ牸鏁版嵁 -->
+ <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="瑙掕壊缂栧彿" prop="no" />
+ <el-table-column label="瑙掕壊鍚嶇О" prop="name" :show-overflow-tooltip="true" />
+ <el-table-column label="鐘舵��" align="center" width="100">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" align="center" width="120">
+ <template #default="scope">
+ <el-tooltip content="淇敼" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀硅鑹查厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="roleRef" :model="form" :rules="rules" label-width="100px">
+ <el-form-item label="瑙掕壊缂栧彿" prop="no">
+ <el-input
+ v-model="form.no"
+ style="max-width: 600px"
+ :placeholder="form.isDefaultNo ? '浣跨敤绯荤粺缂栧彿' : '璇疯緭鍏ヨ鑹茬紪鍙�'"
+ :disabled="form.isDefaultNo"
+ >
+ <template #append>
+ <el-checkbox v-model="form.isDefaultNo" size="large" />
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="瑙掕壊鍚嶇О" prop="name">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ヨ鑹插悕绉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Role">
+import {onMounted} from "vue";
+import {createRole, deleteRoles, findRoleListPage, updateRole} from "@/api/projectManagement/role.js";
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const roleList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const editingId = ref(undefined)
+const form = reactive({
+ isDefaultNo: true,
+ no: "",
+ name: "",
+});
+
+const rules = computed(() => ({
+ no: [{ required: !form.isDefaultNo, message: "瑙掕壊缂栧彿涓嶈兘涓虹┖", trigger: "blur" }],
+ name: [{ required: true, message: "瑙掕壊鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+}));
+
+const data = reactive({
+ queryParams: {
+ current: 1,
+ size: 10,
+ name: undefined,
+ status: undefined
+ },
+})
+
+const { queryParams } = toRefs(data)
+
+/** 鏌ヨ瑙掕壊鍒楄〃 */
+function getList() {
+ loading.value = true
+ findRoleListPage(queryParams.value).then(res => {
+ roleList.value = res.data.records
+ total.value = res.data.total
+ loading.value = false
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.current = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ queryParams.value.name = undefined
+ queryParams.value.status = undefined
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ let roleIds = []
+ if (row.id) {
+ roleIds = [row.id]
+ } else {
+ roleIds = ids.value
+ }
+ proxy.$modal.confirm('纭畾瑕佸垹闄よ鏁版嵁鍚�?').then(function () {
+ return deleteRoles(roleIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.id)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 瑙掕壊鐘舵�佷慨鏀� */
+function handleStatusChange(row) {
+ let text = row.status === 0 ? "鍚敤" : "鍋滅敤"
+ proxy.$modal.confirm('纭瑕�"' + text + '""' + row.name + '"瑙掕壊鍚�?').then(function () {
+ return updateRole(row)
+ }).then(() => {
+ proxy.$modal.msgSuccess(text + "鎴愬姛")
+ }).catch(function () {
+ row.status = row.status === 0 ? 1 : 0
+ })
+}
+
+/** 閲嶇疆鏂板鐨勮〃鍗曚互鍙婂叾浠栨暟鎹� */
+function reset() {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ Object.assign(form, {
+ isDefaultNo: true,
+ no: "",
+ name: "",
+ })
+ editingId.value = undefined
+}
+
+/** 娣诲姞瑙掕壊 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞瑙掕壊"
+}
+
+/** 淇敼瑙掕壊 */
+function handleUpdate(row) {
+ reset()
+ editingId.value = row.id
+ form.no = row.no
+ form.name = row.name
+ title.value = "淇敼瑙掕壊"
+ open.value = true
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["roleRef"].validate(valid => {
+ if (valid) {
+ if (editingId.value) {
+ const data = {
+ id: editingId.value,
+ ...form
+ }
+ updateRole(data).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ }).finally(() => {
+ proxy.$refs["roleRef"].resetFields()
+ open.value = false
+ getList()
+ })
+ } else {
+ createRole(form).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ }).finally(() => {
+ proxy.$refs["roleRef"].resetFields()
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/qualityManagement/finalInspection/components/formDia.vue b/src/views/qualityManagement/finalInspection/components/formDia.vue
index 6a3e774..5f4c975 100644
--- a/src/views/qualityManagement/finalInspection/components/formDia.vue
+++ b/src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -58,7 +58,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="鏁伴噺锛�" prop="quantity">
- <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="璇疯緭鍏�" clearable :precision="2"/>
+ <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="璇疯緭鍏�" clearable :precision="2" :disabled="quantityDisabled"/>
</el-form-item>
</el-col>
</el-row>
@@ -123,7 +123,7 @@
</template>
<script setup>
-import {ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
+import {ref, reactive, toRefs, computed, getCurrentInstance, nextTick} from "vue";
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
@@ -164,6 +164,11 @@
},
});
const { form, rules } = toRefs(data);
+// 缂栬緫鏃讹細productMainId 鎴� purchaseLedgerId 浠讳竴鏈夊�煎垯鏁伴噺缃伆
+const quantityDisabled = computed(() => {
+ const v = form.value || {};
+ return !!(v.productMainId != null || v.purchaseLedgerId != null);
+});
const supplierList = ref([]);
const productOptions = ref([]);
const tableColumn = ref([
diff --git a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
index d33ddad..0c6562c 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
@@ -57,7 +57,9 @@
<el-row :gutter="30">
<el-col :span="12">
<el-form-item label="妫�楠屽憳锛�" prop="checkName">
- <el-input v-model="form.checkName" placeholder="璇疯緭鍏�" clearable/>
+ <el-select v-model="form.checkName" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
+ </el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -91,7 +93,9 @@
<el-row :gutter="30">
<el-col :span="12">
<el-form-item label="澶勭悊浜猴細" prop="dealName">
- <el-input v-model="form.dealName" placeholder="璇疯緭鍏�" clearable/>
+ <el-select v-model="form.dealName" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
+ </el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -120,14 +124,17 @@
</template>
<script setup>
-import {ref} from "vue";
+import {ref, reactive, toRefs} from "vue";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {
getQualityUnqualifiedInfo,
qualityUnqualifiedAdd,
qualityUnqualifiedUpdate
} from "@/api/qualityManagement/nonconformingManagement.js";
+import {userListNoPage} from "@/api/system/user.js";
+import useUserStore from "@/store/modules/user";
const { proxy } = getCurrentInstance()
+const userStore = useUserStore()
const emit = defineEmits(['close'])
const dialogFormVisible = ref(false);
@@ -148,28 +155,56 @@
inspectType: '',
defectivePhenomena: '',
dealResult: '',
+ dealName: '',
+ dealTime: '',
},
rules: {
checkTime: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" },],
process: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- checkName: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkName: [{ required: true, message: "璇烽�夋嫨妫�楠屽憳", trigger: "change" }],
productId: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
unit: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
checkCompany: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
checkResult: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ dealName: [{ required: true, message: "璇烽�夋嫨澶勭悊浜�", trigger: "change" }],
},
});
const { form, rules } = toRefs(data);
const productOptions = ref([]);
-const modelOptions = ref([])
+const modelOptions = ref([]);
+const userList = ref([]); // 妫�楠屽憳/澶勭悊浜轰笅鎷夊垪琛�
// 鎵撳紑寮规
-const openDialog = (type, row) => {
+const openDialog = async (type, row) => {
operationType.value = type;
+ try {
+ const userRes = await userListNoPage();
+ userList.value = userRes.data || [];
+ } catch (e) {
+ console.error("鍔犺浇鐢ㄦ埛鍒楄〃澶辫触", e);
+ userList.value = [];
+ }
dialogFormVisible.value = true;
- form.value = {}
+ if (operationType.value === 'add') {
+ form.value = {
+ checkName: userStore.nickName || '',
+ dealName: '',
+ dealTime: '',
+ dealResult: '',
+ defectivePhenomena: '',
+ inspectType: '',
+ checkTime: '',
+ productId: '',
+ model: '',
+ unit: '',
+ quantity: '',
+ productName: '',
+ };
+ } else {
+ form.value = {};
+ }
getProductOptions();
if (operationType.value === 'edit') {
getQualityUnqualifiedInfo(row.id).then(res => {
diff --git a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
index 467a0d3..8f4492a 100644
--- a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
+++ b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
@@ -82,7 +82,7 @@
<el-col :span="12">
<el-form-item label="澶勭悊缁撴灉锛�" prop="dealResult">
<el-select v-model="form.dealResult" placeholder="璇烽�夋嫨" clearable>
- <el-option :label="item.label" :value="item.value" v-for="item in rejection_handling" :key="item.value" />
+ <el-option :label="item.label" :value="item.value" v-for="item in filteredRejectionHandling" :key="item.value" />
</el-select>
</el-form-item>
</el-col>
@@ -90,7 +90,9 @@
<el-row :gutter="30">
<el-col :span="12">
<el-form-item label="澶勭悊浜猴細" prop="dealName">
- <el-input v-model="form.dealName" placeholder="璇疯緭鍏�" clearable/>
+ <el-select v-model="form.dealName" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
+ </el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -119,12 +121,13 @@
</template>
<script setup>
-import {ref} from "vue";
+import {ref, reactive, toRefs, computed} from "vue";
import {productTreeList} from "@/api/basicData/product.js";
import {
getQualityUnqualifiedInfo,
qualityUnqualifiedDeal
} from "@/api/qualityManagement/nonconformingManagement.js";
+import {userListNoPage} from "@/api/system/user.js";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -148,6 +151,7 @@
dealResult: '',
dealName: '',
dealTime: '',
+ method: undefined
},
rules: {
checkTime: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" },],
@@ -161,22 +165,41 @@
checkResult: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
defectivePhenomena: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
dealResult: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
- dealName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ dealName: [{ required: true, message: "璇烽�夋嫨澶勭悊浜�", trigger: "change" }],
dealTime: [{ required: true, message: "璇疯緭鍏�", trigger: "change" }],
},
});
const { form, rules } = toRefs(data);
const productOptions = ref([]);
+const userList = ref([]); // 澶勭悊浜轰笅鎷夊垪琛�
+
+const filteredRejectionHandling = computed(() => {
+ const data = rejection_handling.value;
+ if (form.value.method) {
+ return data.filter(item => item && item.label && item.label !== '杩斿伐' && item.label !== '杩斾慨')
+ }
+ return data
+})
+
// 鎵撳紑寮规
-const openDialog = (type, row) => {
+const openDialog = async (type, row) => {
operationType.value = type;
+ // 澶勭悊浜轰笅鎷夊垪琛�
+ try {
+ const userRes = await userListNoPage();
+ userList.value = userRes.data || [];
+ } catch (e) {
+ console.error("鍔犺浇鐢ㄦ埛鍒楄〃澶辫触", e);
+ userList.value = [];
+ }
dialogFormVisible.value = true;
- form.value = {}
+ form.value = {};
getProductOptions();
if (operationType.value === 'edit') {
getQualityUnqualifiedInfo(row.id).then(res => {
const { inspectState, ...rest } = (res.data || {})
+ // 鏈夋暟鎹氨鏄剧ず榛樿鍊硷紝娌℃湁灏变笉鏄剧ず
form.value = { ...rest }
})
}
diff --git a/src/views/qualityManagement/nonconformingManagement/index.vue b/src/views/qualityManagement/nonconformingManagement/index.vue
index 327fc88..55d2472 100644
--- a/src/views/qualityManagement/nonconformingManagement/index.vue
+++ b/src/views/qualityManagement/nonconformingManagement/index.vue
@@ -60,7 +60,7 @@
<script setup>
import { Search } from "@element-plus/icons-vue";
-import {onMounted, ref} from "vue";
+import {onMounted, ref, reactive, toRefs, nextTick, getCurrentInstance} from "vue";
import FormDia from "@/views/qualityManagement/nonconformingManagement/components/formDia.vue";
import {ElMessageBox} from "element-plus";
import {qualityUnqualifiedDel, qualityUnqualifiedListPage} from "@/api/qualityManagement/nonconformingManagement.js";
diff --git a/src/views/qualityManagement/processInspection/components/formDia.vue b/src/views/qualityManagement/processInspection/components/formDia.vue
index 1b943e2..c1185d2 100644
--- a/src/views/qualityManagement/processInspection/components/formDia.vue
+++ b/src/views/qualityManagement/processInspection/components/formDia.vue
@@ -10,7 +10,9 @@
<el-row :gutter="30">
<el-col :span="12">
<el-form-item label="宸ュ簭锛�" prop="process">
- <el-input v-model="form.process" placeholder="璇疯緭鍏ュ伐搴�" clearable />
+ <el-select v-model="form.process" placeholder="璇烽�夋嫨宸ュ簭" clearable :disabled="processQuantityDisabled" style="width: 100%">
+ <el-option v-for="item in processList" :key="item.name" :label="item.name" :value="item.name"/>
+ </el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -65,7 +67,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="鏁伴噺锛�" prop="quantity">
- <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="璇疯緭鍏�" clearable :precision="2"/>
+ <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="璇疯緭鍏�" clearable :precision="2" :disabled="processQuantityDisabled"/>
</el-form-item>
</el-col>
</el-row>
@@ -130,15 +132,18 @@
</template>
<script setup>
-import {ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
+import {ref, reactive, toRefs, computed, getCurrentInstance, nextTick} from "vue";
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js";
import {userListNoPage} from "@/api/system/user.js";
import {qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js";
+import { list } from "@/api/productionManagement/productionProcess";
const { proxy } = getCurrentInstance()
const emit = defineEmits(['close'])
+
+
const dialogFormVisible = ref(false);
const operationType = ref('')
@@ -159,7 +164,7 @@
},
rules: {
checkTime: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" },],
- process: [{ required: true, message: "璇疯緭鍏ュ伐搴�", trigger: "blur" }],
+ process: [{ required: true, message: "璇烽�夋嫨宸ュ簭", trigger: "change" }],
checkName: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
productId: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
productModelId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
@@ -172,6 +177,12 @@
});
const userList = ref([]);
const { form, rules } = toRefs(data);
+// 缂栬緫鏃讹細productMainId 鎴� purchaseLedgerId 浠讳竴鏈夊�煎垯宸ュ簭銆佹暟閲忕疆鐏�
+const processQuantityDisabled = computed(() => {
+ const v = form.value || {};
+ return !!(v.productMainId != null || v.purchaseLedgerId != null);
+});
+const processList = ref([]); // 宸ュ簭涓嬫媺鍒楄〃锛堝伐搴忓悕绉� name锛�
const supplierList = ref([]);
const productOptions = ref([]);
const tableColumn = ref([
@@ -210,6 +221,14 @@
getOptions().then((res) => {
supplierList.value = res.data;
});
+ // 鍔犺浇宸ュ簭涓嬫媺鍒楄〃
+ try {
+ const res = await list();
+ processList.value = res.data || [];
+ } catch (e) {
+ console.error("鍔犺浇宸ュ簭鍒楄〃澶辫触", e);
+ processList.value = [];
+ }
let userLists = await userListNoPage();
userList.value = userLists.data;
// 鍏堥噸缃〃鍗曟暟鎹紙淇濇寔瀛楁瀹屾暣锛岄伩鍏嶅脊绐楅娆℃覆鏌撴椂瑙﹀彂蹇呭~绾㈡鈥滈棯涓�涓嬧�濓級
diff --git a/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue b/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
index 21c4323..7e373bf 100644
--- a/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
+++ b/src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -14,6 +14,7 @@
v-model="form.supplier"
placeholder="璇烽�夋嫨"
clearable
+ :disabled="supplierQuantityDisabled"
>
<el-option
v-for="item in supplierList"
@@ -77,7 +78,7 @@
<el-col :span="12">
<el-form-item label="鏁伴噺锛�" prop="quantity">
<el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="璇疯緭鍏�"
- clearable :precision="2"/>
+ clearable :precision="2" :disabled="supplierQuantityDisabled"/>
</el-form-item>
</el-col>
</el-row>
@@ -99,8 +100,9 @@
<el-row :gutter="30">
<el-col :span="12">
<el-form-item label="妫�楠屽憳锛�" prop="checkName">
- <el-input v-model="form.checkName" placeholder="璇疯緭鍏�" clearable/>
-
+ <el-select v-model="form.checkName" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
+ </el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -143,12 +145,13 @@
</template>
<script setup>
-import {ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
+import {ref, reactive, toRefs, computed, getCurrentInstance, nextTick} from "vue";
import {getOptions} from "@/api/procurementManagement/procurementLedger.js";
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {qualityInspectAdd, qualityInspectUpdate} from "@/api/qualityManagement/rawMaterialInspection.js";
import {qualityInspectParamDel, qualityInspectParamInfo} from "@/api/qualityManagement/qualityInspectParam.js";
import {qualityInspectDetailByProductId, getQualityTestStandardParamByTestStandardId} from "@/api/qualityManagement/metricMaintenance.js";
+import {userListNoPage} from "@/api/system/user.js";
const {proxy} = getCurrentInstance()
const emit = defineEmits(['close'])
@@ -216,6 +219,13 @@
const currentProductId = ref(0);
const testStandardOptions = ref([]); // 鎸囨爣閫夋嫨涓嬫媺妗嗘暟鎹�
const modelOptions = ref([]);
+const userList = ref([]); // 妫�楠屽憳涓嬫媺鍒楄〃
+
+// 缂栬緫鏃讹細productMainId 鎴� purchaseLedgerId 浠讳竴鏈夊�煎垯渚涘簲鍟嗐�佹暟閲忕疆鐏�
+const supplierQuantityDisabled = computed(() => {
+ const v = form.value || {};
+ return !!(v.productMainId != null || v.purchaseLedgerId != null);
+});
// 鎵撳紑寮规
const openDialog = async (type, row) => {
@@ -223,6 +233,14 @@
getOptions().then((res) => {
supplierList.value = res.data;
});
+
+ try {
+ const userRes = await userListNoPage();
+ userList.value = userRes.data || [];
+ } catch (e) {
+ console.error("鍔犺浇妫�楠屽憳鍒楄〃澶辫触", e);
+ userList.value = [];
+ }
// 鍏堥噸缃〃鍗曟暟鎹紙淇濇寔瀛楁瀹屾暣锛岄伩鍏嶅脊绐楅娆℃覆鏌撴椂瑙﹀彂蹇呭~绾㈡鈥滈棯涓�涓嬧�濓級
form.value = {
checkTime: "",
diff --git a/src/views/reportAnalysis/productionAnalysis/components/center-center.vue b/src/views/reportAnalysis/productionAnalysis/components/center-center.vue
index 973c68d..96bcada 100644
--- a/src/views/reportAnalysis/productionAnalysis/components/center-center.vue
+++ b/src/views/reportAnalysis/productionAnalysis/components/center-center.vue
@@ -50,8 +50,8 @@
itemHeight: 12,
textStyle: { color: '#B8C8E0', fontSize: 14 },
data: [
- { name: '鍑哄簱', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
- { name: '鍏ュ簱', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
+ { name: '浜у嚭', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
+ { name: '鎶曞叆', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
],
}
@@ -80,7 +80,7 @@
const lineSeries = ref([
{
- name: '鍑哄簱',
+ name: '浜у嚭',
type: 'line',
smooth: false,
showSymbol: true,
@@ -98,7 +98,7 @@
emphasis: { focus: 'series' },
},
{
- name: '鍏ュ簱',
+ name: '鎶曞叆',
type: 'line',
smooth: false,
showSymbol: true,
diff --git a/src/views/safeProduction/accidentReportingRecord/index.vue b/src/views/safeProduction/accidentReportingRecord/index.vue
index e362ba5..ff5b301 100644
--- a/src/views/safeProduction/accidentReportingRecord/index.vue
+++ b/src/views/safeProduction/accidentReportingRecord/index.vue
@@ -235,7 +235,7 @@
<el-descriptions-item label="浜嬫晠绫诲瀷">
<el-tag type="info">{{ accidentTypeLabel(currentKnowledge.accidentType) }}</el-tag>
</el-descriptions-item>
- <el-descriptions-item label="浜哄憳浼や骸鎯呭喌">
+ <el-descriptions-item label="浜哄憳鎹熷け鎯呭喌">
{{ currentKnowledge.personLoss }}
</el-descriptions-item>
<el-descriptions-item label="鐩存帴璐骇鎹熷け锛堝厓锛�">
diff --git a/src/views/safeProduction/hazardousMaterialsControl/index.vue b/src/views/safeProduction/hazardousMaterialsControl/index.vue
index b2048fe..a53490c 100644
--- a/src/views/safeProduction/hazardousMaterialsControl/index.vue
+++ b/src/views/safeProduction/hazardousMaterialsControl/index.vue
@@ -653,15 +653,7 @@
// 閫夋嫨鍙樺寲澶勭悊
const handleSelectionChange = selection => {
- // 涓昏〃鏍间篃鍙繚鐣欐渶鍚庝竴涓�変腑鐨勯」
- if (selection.length > 1) {
- const lastSelected = selection[selection.length - 1];
- selectedIds.value = [lastSelected.id];
- } else if (selection.length === 1) {
- selectedIds.value = [selection[0].id];
- } else {
- selectedIds.value = [];
- }
+ selectedIds.value = selection.map(item => item.id);
};
// 鎵撳紑琛ㄥ崟
diff --git a/src/views/salesManagement/deliveryLedger/index.vue b/src/views/salesManagement/deliveryLedger/index.vue
index de52c68..db21d42 100644
--- a/src/views/salesManagement/deliveryLedger/index.vue
+++ b/src/views/salesManagement/deliveryLedger/index.vue
@@ -34,6 +34,8 @@
<el-table-column label="閿�鍞鍗�" prop="salesContractNo" show-overflow-tooltip />
<el-table-column label="鍙戣揣璁㈠崟鍙�" prop="shippingNo" show-overflow-tooltip />
<el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" show-overflow-tooltip />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" show-overflow-tooltip />
<el-table-column label="鍙戣揣鏃堕棿" prop="shippingDate" show-overflow-tooltip />
<el-table-column label="鍙戣揣杞︾墝鍙�" prop="shippingCarNumber" show-overflow-tooltip />
<el-table-column label="蹇�掑叕鍙�" prop="expressCompany" show-overflow-tooltip />
@@ -180,6 +182,8 @@
<el-descriptions-item label="閿�鍞鍗�">{{ detailRow.salesContractNo || '--' }}</el-descriptions-item>
<el-descriptions-item label="鍙戣揣璁㈠崟鍙�">{{ detailRow.shippingNo || '--' }}</el-descriptions-item>
<el-descriptions-item label="瀹㈡埛鍚嶇О">{{ detailRow.customerName || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ detailRow.productName || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ detailRow.specificationModel || '--' }}</el-descriptions-item>
<el-descriptions-item label="鍙戣揣绫诲瀷">{{ detailRow.type || '--' }}</el-descriptions-item>
<el-descriptions-item label="鍙戣揣鏃ユ湡">{{ detailRow.shippingDate || '--' }}</el-descriptions-item>
<el-descriptions-item label="瀹℃牳鐘舵��">{{ getApprovalStatusText(detailRow.status) }}</el-descriptions-item>
@@ -284,6 +288,8 @@
id: null,
salesContractNo: "",
customerName: "",
+ specificationModel: "",
+ productName: "",
type: "璐ц溅", // 璐ц溅, 蹇��
shippingDate: "",
shippingCarNumber: "",
diff --git a/src/views/salesManagement/indicatorStats/index.vue b/src/views/salesManagement/indicatorStats/index.vue
index 0d2017d..6101920 100644
--- a/src/views/salesManagement/indicatorStats/index.vue
+++ b/src/views/salesManagement/indicatorStats/index.vue
@@ -98,11 +98,11 @@
<label class="filter-label">鏃ユ湡鑼冨洿</label>
<el-date-picker
v-model="indicatorFilter.dateRange"
- type="daterange"
+ type="monthrange"
range-separator="鑷�"
start-placeholder="寮�濮嬫棩鏈�"
end-placeholder="缁撴潫鏃ユ湡"
- value-format="YYYY-MM-DD"
+ value-format="YYYY-MM"
style="width: 100%"
/>
</div>
diff --git a/src/views/salesManagement/invoiceRegistration/index.vue b/src/views/salesManagement/invoiceRegistration/index.vue
index 9cc1d66..2f6e60c 100644
--- a/src/views/salesManagement/invoiceRegistration/index.vue
+++ b/src/views/salesManagement/invoiceRegistration/index.vue
@@ -30,7 +30,12 @@
<div class="flex justify-between">
<div></div>
<div>
- <el-button type="primary" @click="openForm" style="margin-bottom: 8px">
+ <el-button
+ type="primary"
+ @click="openForm"
+ style="margin-bottom: 8px"
+ :disabled="!canInvoice"
+ >
寮�绁ㄧ櫥璁�
</el-button>
</div>
@@ -296,10 +301,14 @@
/>
<el-table-column label="鏈寮�绁ㄦ暟" prop="currentInvoiceNum" width="180">
<template #default="scope">
- <el-input-number :step="0.1" :min="0" style="width: 100%"
- :precision="2"
- v-model="scope.row.currentInvoiceNum"
- @change="invoiceNumBlur(scope.row)"
+ <el-input-number
+ :step="0.1"
+ :min="0"
+ style="width: 100%"
+ :precision="2"
+ v-model="scope.row.currentInvoiceNum"
+ @change="invoiceNumBlur(scope.row)"
+ :disabled="isProductInvoiceDisabled(scope.row)"
></el-input-number>
</template>
</el-table-column>
@@ -309,10 +318,14 @@
width="180"
>
<template #default="scope">
- <el-input-number :step="0.01" :min="0" style="width: 100%"
- :precision="2"
- v-model="scope.row.currentInvoiceAmount"
- @change="invoiceAmountBlur(scope.row)"
+ <el-input-number
+ :step="0.01"
+ :min="0"
+ style="width: 100%"
+ :precision="2"
+ v-model="scope.row.currentInvoiceAmount"
+ @change="invoiceAmountBlur(scope.row)"
+ :disabled="isProductInvoiceDisabled(scope.row)"
></el-input-number>
</template>
</el-table-column>
@@ -375,7 +388,7 @@
<script setup>
import pagination from "@/components/PIMTable/Pagination.vue";
import FormDialog from '@/components/Dialog/FormDialog.vue';
-import { onMounted, ref } from "vue";
+import { onMounted, ref, computed } from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
// import {userListNoPage} from "@/api/system/user.js";
@@ -450,6 +463,27 @@
const formattedInputNumber = (value) => {
return value ? parseFloat(value).toFixed(2) : 0;
+};
+
+// 鍒ゆ柇鏄惁鍙互寮�绁紙鍩轰簬閫変腑鐨勫彴璐︽暟鎹級
+const canInvoice = computed(() => {
+ if (selectedRows.value.length === 0) {
+ return false;
+ }
+ // 妫�鏌ユ墍鏈夐�変腑鐨勫彴璐︼紝鍙鏈変竴涓湭寮�绁ㄩ噾棰濆ぇ浜�0锛屽氨鍙互寮�绁�
+ return selectedRows.value.some(row => {
+ const noInvoiceAmount = parseFloat(row.noInvoiceAmountTotal || 0);
+ return noInvoiceAmount > 0;
+ });
+});
+
+// 鍒ゆ柇鍗曚釜浜у搧鏄惁鍙互寮�绁�
+const isProductInvoiceDisabled = (row) => {
+ // 妫�鏌ユ湭寮�绁ㄩ噾棰濆拰鏈紑绁ㄦ暟锛屽鏋滈兘涓�0鎴栧皬浜庣瓑浜�0锛屽垯绂佺敤
+ // 浼樺厛浣跨敤 tempnoInvoiceAmount 鍜� tempNoInvoiceNum锛堝垵濮嬪�硷級锛屽鏋滄病鏈夊垯浣跨敤 noInvoiceAmount 鍜� originalNoInvoiceNum
+ const noInvoiceAmount = parseFloat(row.tempnoInvoiceAmount || row.noInvoiceAmount || 0);
+ const noInvoiceNum = parseFloat(row.tempNoInvoiceNum || row.originalNoInvoiceNum || row.noInvoiceNum || 0);
+ return noInvoiceAmount <= 0 || noInvoiceNum <= 0;
};
// 鏌ヨ鍒楄〃
@@ -579,6 +613,14 @@
productData.value = allProductData;
+ // 瀵逛簬涓嶈兘寮�绁ㄧ殑浜у搧锛屽皢寮�绁ㄦ暟鍜屽紑绁ㄩ噾棰濊缃负0
+ productData.value.forEach(item => {
+ if (isProductInvoiceDisabled(item)) {
+ item.currentInvoiceNum = 0;
+ item.currentInvoiceAmount = 0;
+ }
+ });
+
dialogFormVisible.value = true;
console.log("productData.value ", productData.value);
});
diff --git a/src/views/salesManagement/receiptPayment/index.vue b/src/views/salesManagement/receiptPayment/index.vue
index 6ded92b..83cf942 100644
--- a/src/views/salesManagement/receiptPayment/index.vue
+++ b/src/views/salesManagement/receiptPayment/index.vue
@@ -40,6 +40,7 @@
<el-table
:data="tableData"
border
+ ref="tableRef"
v-loading="tableLoading"
@selection-change="handleSelectionChange"
:row-key="(row) => row.id"
@@ -313,6 +314,7 @@
});
const total = ref(0);
const expandedRowKeys = ref([]);
+const tableRef = ref(null);
// 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
const dialogFormVisible = ref(false);
@@ -491,6 +493,7 @@
dialogFormVisible.value = false;
// 閬垮厤浜屾鎵撳紑寮圭獥鏃朵粛鎼哄甫涓婁竴娆$殑閫夋嫨瀵艰嚧鈥滃鍑轰竴琛�/鑴忔暟鎹��
selectedRows.value = [];
+ tableRef.value?.clearSelection();
};
// 鍒犻櫎鍥炴璁板綍
diff --git a/src/views/salesManagement/returnOrder/components/formDia.vue b/src/views/salesManagement/returnOrder/components/formDia.vue
new file mode 100644
index 0000000..82b3766
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/components/formDia.vue
@@ -0,0 +1,489 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible" :title="operationType === 'edit' ? '缂栬緫閫�璐у崟' : '鏂板閫�璐у崟'" width="90%" @close="closeDia">
+ <div>
+ <span class="descriptions">鍩烘湰淇℃伅</span>
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="4">
+ <el-form-item label="閫�璐у崟鍙凤細" prop="returnNo">
+ <el-input
+ :disabled="operationType === 'edit' || form.returnNoCheckbox"
+ v-model="form.returnNo"
+ placeholder="浣跨敤绯荤粺缂栧彿"
+ class="input-with-select"
+ >
+ <template v-if="operationType !== 'edit'" #append>
+ <el-checkbox v-model="form.returnNoCheckbox" @change="handleReturnNoCheckboxChange"></el-checkbox>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerId">
+ <el-select v-model="form.customerId" filterable placeholder="璇烽�夋嫨瀹㈡埛" @change="customerNameChange">
+ <el-option
+ v-for="item in customerNameOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍏宠仈鍑哄簱鍗曞彿锛�" prop="shippingId">
+ <el-select v-model="form.shippingId" filterable placeholder="璇烽�夋嫨鍑哄簱鍗曞彿" @change="outboundNoChange">
+ <el-option
+ v-for="item in outboundOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="椤圭洰闃舵锛�" prop="projectStage">
+ <el-input v-model="form.projectStage" placeholder="椤圭洰闃舵" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍒跺崟浜猴細" prop="maker">
+ <el-select v-model="form.maker" filterable placeholder="璇烽�夋嫨鍒跺崟浜�">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍒跺崟鏃堕棿锛�" prop="makeTime">
+ <el-date-picker v-model="form.makeTime" type="datetime" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="缁撶畻浜猴細" prop="settler">
+ <el-select v-model="form.settler" filterable placeholder="璇烽�夋嫨缁撶畻浜�">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鐘舵�侊細" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option label="寰呭鏍�" :value="0" />
+ <el-option label="瀹℃牳涓�" :value="1" />
+ <el-option label="宸插鏍�" :value="2" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <hr>
+ <div style="padding-top: 20px">
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
+ <span class="descriptions" style="margin-bottom:0">浜у搧鍒楄〃</span>
+ <el-button type="primary" @click="openProductSelection" :disabled="!form.shippingId">娣诲姞浜у搧</el-button>
+ </div>
+ <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
+ <template #returnQuantity="{ row }">
+ <el-input
+ v-model="row.returnQuantity"
+ style="width:100px"
+ placeholder="璇疯緭鍏�"
+ type="number"
+ @input="(val) => handleReturnQuantityChange(val, row)"
+ />
+ </template>
+ <template #action="{ row, index }">
+ <el-button type="danger" link @click="deleteRow(index)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <el-dialog v-model="productSelectionVisible" title="閫夋嫨浜у搧" width="70%" append-to-body>
+ <el-table
+ :data="availableProducts"
+ style="width: 100%"
+ @selection-change="handleSelectionChange"
+ ref="productTableRef"
+ row-key="id"
+ >
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" prop="productCategory" label="浜у搧澶х被" />
+ <el-table-column align="center" prop="specificationModel" label="瑙勬牸鍨嬪彿" />
+ <el-table-column align="center" prop="unit" label="鍗曚綅" />
+ <el-table-column align="center" prop="quantity" label="鎬绘暟閲�" />
+ <el-table-column align="center" prop="unQuantity" label="鏈��璐ф暟閲�" />
+ <el-table-column align="center" label="宸查��璐ф暟閲�">
+ <template #default="{ row }">{{ calcAlreadyReturned(row) }}</template>
+ </el-table-column>
+
+ </el-table>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="confirmProductSelection">纭娣诲姞</el-button>
+ <el-button @click="productSelectionVisible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, getCurrentInstance } from "vue";
+import { returnManagementAdd, returnManagementUpdate, returnManagementGetByShippingId, getSalesLedger, returnManagementGetById } from "@/api/salesManagement/returnOrder.js";
+import { getAllCustomerList } from "@/api/customerService/index.js";
+import useUserStore from "@/store/modules/user.js";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { listProject } from "@/api/oaSystem/projectManagement.js";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('');
+const formRef = ref(null);
+const userStore = useUserStore();
+
+const data = reactive({
+ form: {
+ returnNoCheckbox: true,
+ returnNo: "",
+ customerId: "",
+ shippingId: "",
+ projectId: "",
+ projectStage: "",
+ maker: "",
+ makeTime: "",
+ settler: "",
+ status: 0,
+ },
+ rules: {
+ returnNo: [{
+ validator: (rule, value, callback) => {
+ if (form.value.returnNoCheckbox) return callback();
+ if (!value) return callback(new Error("璇疯緭鍏ラ��璐у崟鍙�"));
+ callback();
+ }, trigger: "blur"
+ }],
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ shippingId: [{ required: true, message: "璇烽�夋嫨鍏宠仈鍑哄簱鍗曞彿", trigger: "change" }],
+ }
+});
+const { form, rules } = toRefs(data);
+
+const calcAlreadyReturned = (row) => {
+ const total = Number(row?.quantity ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
+ const un = Number(row?.unQuantity ?? 0);
+ if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
+ return Math.max(total - un, 0);
+};
+
+const tableColumn = ref([
+ {align: "center", label: "浜у搧澶х被", prop: "productCategory" },
+ {align: "center", label: "瑙勬牸鍨嬪彿", prop: "specificationModel" },
+ {align: "center", label: "鍗曚綅", prop: "unit", width: 80 },
+ {align: "center", label: "鎬绘暟閲�", prop: "quantity", width: 120 },
+ {align: "center", label: "宸查��璐ф暟閲�", prop: "totalReturnNum", width: 120 },
+ {align: "center", label: "鏈��璐ф暟閲�", prop: "unQuantity", width: 120 },
+ {align: "center", label: "閫�璐ф暟閲�", prop: "returnQuantity", dataType: "slot", slot: "returnQuantity", width: 120 },
+ {align: "center", label: "鎿嶄綔" , prop: "action", dataType: "slot", slot: "action", width: 120 },
+]);
+const tableData = ref([]);
+const customerNameOptions = ref([]);
+const outboundOptions = ref([]);
+const userOptions = ref([]);
+const projectOptions = ref([]);
+
+const deleteRow = (index) => {
+ tableData.value.splice(index, 1);
+};
+
+const normalizeDetailRow = (raw) => {
+ const productId = raw?.returnSaleLedgerProductId ?? raw?.saleLedgerProductId ?? raw?.id;
+ const returnSaleProductId = raw?.returnSaleProductId ?? raw?.id;
+ const num = Number(raw?.num ?? raw?.returnQuantity ?? 0);
+ return {
+ ...raw,
+ id: productId,
+ returnSaleProductId,
+ returnSaleLedgerProductId: productId,
+ num,
+ returnQuantity: Number.isFinite(num) ? num : 0,
+ };
+};
+
+const setFormForEdit = async (row) => {
+ const res = await returnManagementGetById({ returnManagementId: row?.id });
+ console.log("res", res);
+ const detail = res?.data ?? res ?? {};
+
+ Object.assign(form.value, detail);
+ form.value.returnNoCheckbox = true;
+
+ if (form.value.customerId) {
+ await customerNameChange(form.value.customerId, false);
+ }
+ if (form.value.shippingId) {
+ await outboundNoChange(form.value.shippingId, false);
+ }
+
+ const list =
+ detail?.returnSaleProducts ||
+ detail?.returnSaleProductList ||
+ detail?.returnSaleProductDtoData ||
+ [];
+
+ tableData.value = Array.isArray(list)
+ ? list.map((raw) => {
+ const normalized = normalizeDetailRow(raw);
+ const product = availableProducts.value.find((p) => p.id === normalized.id);
+ return product ? { ...product, ...normalized } : normalized;
+ })
+ : [];
+};
+
+const openDialog = async (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ proxy.resetForm("formRef");
+ await Promise.all([initCustomers(), initUsers(), initProjects()]);
+ if (type === "edit") {
+ await setFormForEdit(row);
+ } else {
+ tableData.value = [];
+ Object.assign(form.value, {
+ returnNoCheckbox: true,
+ returnNo: "",
+ customerId: "",
+ shippingId: "",
+ projectId: "",
+ projectStage: "",
+ maker: "",
+ makeTime: "",
+ settler: "",
+ status: 0,
+ });
+ form.value.maker = userStore.nickName || userStore.name || "";
+ form.value.makeTime = new Date().toISOString().replace('T', ' ').split('.')[0]; // Default to now
+ form.value.status = 0; // Default status
+ }
+};
+
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (!valid) return;
+ const returnSaleProducts = (tableData.value || []).map(el => ({
+ returnSaleLedgerProductId: el.returnSaleLedgerProductId ?? el.id,
+ num: Number(el.num ?? el.returnQuantity ?? 0),
+ id: operationType.value === "edit" ? (el.returnSaleProductId ?? "") : ""
+ }));
+ const payload = { ...form.value, returnSaleProducts };
+ delete payload.returnNoCheckbox;
+ if (operationType.value === "add" && form.value.returnNoCheckbox) delete payload.returnNo;
+ if (operationType.value === "add") {
+ returnManagementAdd(payload).then(() => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ closeDia();
+ });
+ } else {
+ returnManagementUpdate(payload).then(() => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeDia();
+ });
+ }
+ });
+};
+
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close');
+};
+
+const initCustomers = async () => {
+ const res = await getAllCustomerList({});
+ if (res?.records) {
+ customerNameOptions.value = res.records.map(item => ({
+ label: item.customerName,
+ value: item.customerName, // Keep value as name if needed for other logic, but request says customerId
+ id: item.id,
+ code: item.customerCode
+ }));
+ }
+};
+
+const initUsers = async () => {
+ const res = await userListNoPageByTenantId();
+ if (res?.data) {
+ userOptions.value = res.data.map(u => ({ label: u.nickName || u.userName, value: u.nickName || u.userName }));
+ }
+};
+
+const initProjects = async () => {
+ try {
+ const res = await listProject({ pageSize: 1000 });
+ if (res?.rows) {
+ projectOptions.value = res.rows.map(p => ({ label: p.projectName, value: p.id }));
+ }
+ } catch (e) {
+ console.error("Failed to load projects", e);
+ }
+};
+
+const handleReturnNoCheckboxChange = (checked) => {
+ if (checked) form.value.returnNo = "";
+ formRef.value?.validateField('returnNo');
+};
+
+const customerNameChange = async (val, clearDownstream = true) => {
+ // val is customerId now
+ if (clearDownstream) {
+ form.value.shippingId = "";
+ outboundOptions.value = [];
+ }
+
+ // Find customer name for getSalesLedger if it requires name
+ const customer = customerNameOptions.value.find(c => c.id === val);
+ if (!customer) return;
+
+ // Assuming getSalesLedger takes customerName. If it takes ID, adjust accordingly.
+ // Previous code used customerName. Let's try passing customerName.
+ getSalesLedger({
+ customerName: customer.label,
+ }).then(res => {
+ if(res.code === 200){
+ outboundOptions.value = res.data.map(item => ({
+ label: item.salesContractNo, // Or whatever the outbound number field is
+ value: item.id,
+ }))
+ }
+ })
+};
+
+const outboundNoChange = async (val, clearTable = true) => {
+ // val is shippingId
+ let res = await returnManagementGetByShippingId({ shippingId: val });
+ if(res.code === 200){
+ // If backend returns project info, set it
+ if (res.data.projectId) form.value.projectId = res.data.projectId;
+ if (res.data.projectStage) form.value.projectStage = res.data.projectStage;
+
+ // Store available products for selection
+ availableProducts.value = res.data.productDtoData || [];
+ if (clearTable) tableData.value = [];
+ }
+};
+
+const handleReturnQuantityChange = (val, row) => {
+ if (val === "" || val === null) return;
+ const max = row.unQuantity === undefined || row.unQuantity === null ? Infinity : Number(row.unQuantity || 0);
+ const current = Number(val);
+
+ if (current > max) {
+ // Need nextTick to ensure update if user typed too fast or pasted
+ proxy.$nextTick(() => {
+ row.returnQuantity = max;
+ row.num = max;
+ });
+ proxy.$modal.msgWarning(`閫�璐ф暟閲忎笉鑳借秴杩囨湭閫�璐ф暟閲�(${max})`);
+ } else if (current < 0) {
+ proxy.$nextTick(() => {
+ row.returnQuantity = 0;
+ row.num = 0;
+ });
+ } else {
+ row.num = current;
+ }
+};
+
+const availableProducts = ref([]);
+const productSelectionVisible = ref(false);
+const selectedProducts = ref([]);
+
+const openProductSelection = () => {
+ productSelectionVisible.value = true;
+ // Pre-select items already in tableData
+ proxy.$nextTick(() => {
+ if (proxy.$refs.productTableRef) {
+ proxy.$refs.productTableRef.clearSelection();
+ availableProducts.value.forEach(row => {
+ if (tableData.value.some(item => item.id === row.id)) {
+ proxy.$refs.productTableRef.toggleRowSelection(row, true);
+ }
+ });
+ }
+ });
+};
+
+const handleSelectionChange = (val) => {
+ selectedProducts.value = val;
+};
+
+// Removed checkSelectable to allow toggling existing items
+const confirmProductSelection = () => {
+ // Rebuild tableData based on selection, preserving existing data (returnQuantity)
+ const newTableData = [];
+
+ selectedProducts.value.forEach(product => {
+ // Check if product was already in tableData to preserve user input
+ const existing = tableData.value.find(item => item.id === product.id);
+ if (existing) {
+ newTableData.push(existing);
+ } else {
+ // Create new entry
+ newTableData.push({
+ ...product, // Keep all product display fields (productName, model, unit, etc.)
+
+ // Map to backend entity structure for submission
+ returnSaleLedgerProductId: product.id,
+ returnQuantity: 0, // Default input
+ num: 0, // Backend quantity field
+
+ // Ensure display fields are available if they come from 'product'
+ // If product has different field names than tableColumn expects, map them here
+ productName: product.productName,
+ specificationModel: product.specificationModel,
+ unit: product.unit,
+ quantity: product.quantity,
+ totalReturnNum: product.totalReturnNum,
+ unQuantity: product.unQuantity
+ });
+ }
+ });
+
+ tableData.value = newTableData;
+ productSelectionVisible.value = false;
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+.descriptions {
+ margin-bottom: 20px;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: 600;
+ padding-left: 12px;
+ position: relative;
+}
+.descriptions::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 1rem;
+ background-color: #002FA7;
+ border-radius: 2px;
+}
+</style>
diff --git a/src/views/salesManagement/returnOrder/index.vue b/src/views/salesManagement/returnOrder/index.vue
new file mode 100644
index 0000000..a83dd24
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/index.vue
@@ -0,0 +1,295 @@
+<template>
+ <div class="app-container">
+ <div class="search-wrapper">
+ <el-form :model="searchForm" class="demo-form-inline">
+ <el-row :gutter="20">
+ <el-col :span="4">
+ <el-form-item>
+ <el-input v-model="searchForm.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-input v-model="searchForm.customerName" placeholder="瀹㈡埛鍚嶇О" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-input v-model="searchForm.salesContractNo" placeholder="閿�鍞崟鍙�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-input v-model="searchForm.shippingNo" placeholder="鍏宠仈鍑哄簱鍗曞彿" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div class="table_header" style="display:flex;justify-content:space-between;align-items:center;">
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板缓閿�鍞��璐�</el-button>
+ </div>
+ <div>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ <el-button @click="columnsDialogVisible = true">鍒楄〃瀛楁</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="visibleColumns"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ />
+ </div>
+ <form-dia ref="formDia" @close="handleQuery" />
+
+ <el-dialog v-model="columnsDialogVisible" title="鑷畾涔夋樉绀哄垪椤�" width="600px">
+ <div class="columns-tip">娉細鍒楄〃椤规樉绀轰笉寰楀皯浜�5椤癸紱鎷栧姩鍙充晶鎶婃墜鍙皟鏁存樉绀洪『搴�</div>
+ <ul class="columns-list">
+ <li v-for="(col, idx) in allColumns" :key="col.prop"
+ class="columns-item"
+ draggable="true"
+ @dragstart="onDragStart(idx)"
+ @dragover.prevent
+ @drop="onDrop(idx)">
+ <el-checkbox v-model="col.selected">{{ col.label }}</el-checkbox>
+ <span class="drag-handle">鈮�</span>
+ </li>
+ </ul>
+ <template #footer>
+ <el-button @click="resetColumns">鎭㈠榛樿</el-button>
+ <el-button type="primary" @click="saveColumns">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, computed, getCurrentInstance, nextTick, onMounted } from "vue";
+import { ElMessageBox } from "element-plus";
+import FormDia from "./components/formDia.vue";
+import { returnManagementList, returnManagementDel, returnManagementHandle } from "@/api/salesManagement/returnOrder.js";
+const { proxy } = getCurrentInstance();
+
+const formDia = ref();
+const openForm = (type, row) => {
+ nextTick(() => formDia.value?.openDialog(type, row));
+};
+
+const handleRowDelete = (row) => {
+ if (!row?.id) return;
+ ElMessageBox.confirm("璇ラ��璐у崟灏嗚鍒犻櫎锛屾槸鍚︾‘璁ゅ垹闄わ紵", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementDel([row.id]).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ });
+};
+
+const handleRowHandle = (row) => {
+ if (!row?.id) return;
+ ElMessageBox.confirm("鏄惁澶勭悊璇ラ��璐у崟锛熷鐞嗗悗灏嗘棤娉曚慨鏀�", "澶勭悊鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementHandle({ returnManagementId: String(row.id) }).then(() => {
+ proxy.$modal.msgSuccess("澶勭悊鎴愬姛");
+ getList();
+ });
+ });
+}
+
+const data = reactive({
+ searchForm: {
+ returnNo: "",
+ status: "",
+ customerName: "",
+ salesContractNo: "",
+ salesman: "",
+ shippingNo: "",
+ projectName: "",
+ salesLedgerId: "",
+ makeTime: ""
+ }
+});
+const { searchForm } = toRefs(data);
+
+const documentStatusOptions = ref([
+ { label: "寰呭鏍�", value: 0 },
+ { label: "瀹℃牳涓�", value: 1 },
+ { label: "宸插鏍�", value: 2 }
+]);
+
+const defaultColumns = [
+ { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: 160 },
+ { label: "鍗曟嵁鐘舵��", prop: "status", minWidth: 120, formatData: (v) => ({ "0": "寰呭鏍�", "1": "瀹℃牳涓�", "2": "宸插鏍�" }[String(v)] ?? v) },
+ { label: "鍒跺崟鏃堕棿", prop: "makeTime", minWidth: 170 },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: 220 },
+ { label: "閿�鍞崟鍙�", prop: "salesContractNo", minWidth: 160 },
+ { label: "涓氬姟鍛�", prop: "salesman", minWidth: 120 },
+ { label: "鍏宠仈鍑哄簱鍗曞彿", prop: "shippingNo", minWidth: 170 },
+ { label: "椤圭洰鍚嶇О", prop: "projectName", minWidth: 180 },
+ { label: "椤圭洰闃舵", prop: "projectStage", minWidth: 120 },
+ { label: "鍒跺崟浜�", prop: "maker", minWidth: 120 },
+ { label: "缁撶畻浜�", prop: "settler", minWidth: 120 },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "action",
+ align: "center",
+ fixed: "right",
+ width: 160,
+ operation: [
+ { name: "缂栬緫", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => openForm("edit", row) },
+ { name: "澶勭悊", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => handleRowHandle(row) },
+ { name: "鍒犻櫎", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => handleRowDelete(row) },
+ ],
+ },
+];
+const COLUMNS_KEY = "return_order_columns_v2";
+const columnsDialogVisible = ref(false);
+const allColumns = ref([]);
+
+const initColumns = () => {
+ const saved = localStorage.getItem(COLUMNS_KEY);
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ // 鍚堝苟榛樿鍒椾笌宸蹭繚瀛橀厤缃紝閬垮厤鍚庣画鏂板鍒椾涪澶�
+ const map = new Map(parsed.map(c => [c.prop, c]));
+ allColumns.value = defaultColumns.map(d => {
+ const found = map.get(d.prop);
+ return { ...d, selected: found ? !!found.selected : true };
+ });
+ // 浠ヤ繚瀛樼殑椤哄簭涓哄噯
+ const order = parsed.map(p => p.prop);
+ allColumns.value.sort((a, b) => order.indexOf(a.prop) - order.indexOf(b.prop));
+ return;
+ } catch {}
+ }
+ allColumns.value = defaultColumns.map(c => ({ ...c, selected: true }));
+};
+initColumns();
+
+const visibleColumns = computed(() => allColumns.value.filter(c => c.selected));
+
+let dragFrom = -1;
+const onDragStart = (idx) => {
+ dragFrom = idx;
+};
+const onDrop = (to) => {
+ if (dragFrom < 0 || dragFrom === to) return;
+ const arr = [...allColumns.value];
+ const [moved] = arr.splice(dragFrom, 1);
+ arr.splice(to, 0, moved);
+ allColumns.value = arr;
+ dragFrom = -1;
+};
+
+const resetColumns = () => {
+ allColumns.value = defaultColumns.map(c => ({ ...c, selected: true }));
+ localStorage.removeItem(COLUMNS_KEY);
+};
+const saveColumns = () => {
+ const toSave = allColumns.value.map(({ label, prop, width, selected }) => ({ label, prop, width, selected }));
+ localStorage.setItem(COLUMNS_KEY, JSON.stringify(toSave));
+ columnsDialogVisible.value = false;
+};
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({ current: 1, size: 10, total: 0 });
+const selectedRows = ref([]);
+const tableHeight = computed(() => "calc(100% - 80px)");
+
+const handleReset = () => {
+ Object.keys(searchForm.value).forEach(k => searchForm.value[k] = "");
+};
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ returnManagementList({ ...searchForm.value, ...page }).then(res => {
+ tableLoading.value = false;
+ tableData.value = res?.data?.records || [];
+ page.total = res?.data?.total || 0;
+ }).finally(() => tableLoading.value = false);
+};
+const handleOut = () => {
+ ElMessageBox.alert("瀵煎嚭鍔熻兘寰呮帴鍏ユ帴鍙�", "鎻愮ず");
+};
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ids = selectedRows.value.map(i => i.id);
+ console.log(ids);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementDel( ids ).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.search-wrapper {
+ background: white;
+ padding: 1rem 1rem 0 1rem;
+ border: 8px;
+ border-radius: 16px;
+}
+.table_list {
+ height: calc(100vh - 230px);
+ min-height: 360px;
+ background: #fff;
+ margin-top: 20px;
+ display: flex;
+ flex-direction: column;
+}
+.columns-tip{color:#909399;margin-bottom:10px;font-size:12px;}
+.columns-list{list-style:none;padding:0;margin:0;max-height:360px;overflow:auto;}
+.columns-item{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border:1px solid #f0f0f0;border-radius:6px;margin-bottom:8px;cursor:move;background:#fff;}
+.columns-item .drag-handle{color:#909399;padding-left:12px;user-select:none;}
+.table_header {
+ margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/salesManagement/salesLedger/index.vue b/src/views/salesManagement/salesLedger/index.vue
index dfbbaec..1833bc1 100644
--- a/src/views/salesManagement/salesLedger/index.vue
+++ b/src/views/salesManagement/salesLedger/index.vue
@@ -121,7 +121,7 @@
<el-table-column label="澶囨敞" prop="remarks" width="200" show-overflow-tooltip />
<el-table-column fixed="right" label="鎿嶄綔" min-width="100" align="center">
<template #default="scope">
- <el-button link type="primary" size="small" @click="openForm('edit', scope.row)">缂栬緫</el-button>
+ <el-button link type="primary" size="small" @click="openForm('edit', scope.row)" :disabled="!scope.row.isEdit">缂栬緫</el-button>
<!-- <el-button link type="primary" size="small" @click="openForm('view', scope.row)">璇︽儏</el-button>-->
<el-button link type="primary" size="small" @click="downLoadFile(scope.row)">闄勪欢</el-button>
<!-- <el-button link type="primary" size="small" @click="openDeliveryForm(scope.row)">鍙戣揣</el-button>-->
@@ -318,6 +318,15 @@
</template>
</el-table-column>
</el-table>
+
+ <pagination
+ v-show="quotationPage.total > 0"
+ :total="quotationPage.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="quotationPage.current"
+ :limit="quotationPage.size"
+ @pagination="quotationPaginationChange"
+ />
<template #footer>
<el-button @click="quotationDialogVisible = false">鍏抽棴</el-button>
@@ -781,6 +790,12 @@
quotationNo: "",
customer: "",
});
+// 鎶ヤ环鍗曞脊妗嗗垎椤�
+const quotationPage = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
const selectedQuotation = ref(null);
// 鍙戣揣鐩稿叧
@@ -1075,6 +1090,8 @@
const openQuotationDialog = async () => {
if (operationType.value === "view") return;
quotationDialogVisible.value = true;
+ // 鎵撳紑寮圭獥鏃堕噸缃垎椤靛埌绗竴椤�
+ quotationPage.current = 1;
// 鍏堢‘淇濆鎴峰垪琛ㄥ凡鍔犺浇锛屼究浜庡悗缁洖濉� customerId
if (!customerOption.value || customerOption.value.length === 0) {
try {
@@ -1091,14 +1108,15 @@
quotationLoading.value = true;
try {
const params = {
- // 鍏煎鍚庣鍒嗛〉瀛楁锛氳繖閲屾部鐢ㄦ姤浠烽〉闈㈠凡鏈夊彲鐢ㄧ殑瀛楁鍛藉悕
- currentPage: 1,
- pageSize: 100,
+ // 鍚庣鍒嗛〉瀛楁锛歝urrent / size
+ current: quotationPage.current,
+ size: quotationPage.size,
...quotationSearchForm,
status: "閫氳繃",
};
const res = await getQuotationList(params);
quotationList.value = res?.data?.records || [];
+ quotationPage.total = res?.data?.total || 0;
} finally {
quotationLoading.value = false;
}
@@ -1107,9 +1125,17 @@
const resetQuotationSearch = async () => {
quotationSearchForm.quotationNo = "";
quotationSearchForm.customer = "";
+ quotationPage.current = 1;
await fetchQuotationList();
};
+// 鎶ヤ环鍗曞脊妗嗗垎椤靛垏鎹�
+const quotationPaginationChange = (obj) => {
+ quotationPage.current = obj.page;
+ quotationPage.size = obj.limit;
+ fetchQuotationList();
+};
+
// 閫変腑鎶ヤ环鍗曞悗鍥炲~鍒板彴璐﹁〃鍗�
const applyQuotation = (row) => {
if (!row) return;
diff --git a/src/views/salesManagement/salesQuotation/index.vue b/src/views/salesManagement/salesQuotation/index.vue
index 666360a..5bd5cef 100644
--- a/src/views/salesManagement/salesQuotation/index.vue
+++ b/src/views/salesManagement/salesQuotation/index.vue
@@ -453,15 +453,6 @@
// 璁$畻灞炴��
const filteredList = computed(() => {
let list = quotationList.value
- if (searchForm.quotationNo) {
- list = list.filter(item => item.quotationNo.includes(searchForm.quotationNo))
- }
- if (searchForm.customer) {
- list = list.filter(item => item.customer === searchForm.customer)
- }
- if (searchForm.status) {
- list = list.filter(item => item.status === searchForm.status)
- }
return list
})
@@ -480,6 +471,9 @@
searchForm.quotationNo = ''
searchForm.customer = ''
searchForm.status = ''
+ // 閲嶇疆鍒扮涓�椤靛苟閲嶆柊鏌ヨ
+ pagination.currentPage = 1
+ handleSearch()
}
const handleAdd = async () => {
@@ -848,10 +842,14 @@
const handleCurrentChange = (val) => {
pagination.currentPage = val.page
pagination.pageSize = val.limit
+ // 鍒嗛〉鍙樺寲鏃堕噸鏂版煡璇㈠垪琛�
+ handleSearch()
}
const handleSearch = ()=>{
const params = {
- ...pagination,
+ // 鍚庣鍒嗛〉鍙傛暟锛歝urrent / size
+ current: pagination.currentPage,
+ size: pagination.pageSize,
...searchForm
}
getQuotationList(params).then(res=>{
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
index f28463b..9d16126 100644
--- a/src/views/system/user/index.vue
+++ b/src/views/system/user/index.vue
@@ -15,7 +15,7 @@
</pane>
<!--鐢ㄦ埛鏁版嵁-->
<pane size="84">
- <el-col style="padding: 10px">
+ <el-col style="padding: 10px; height: 100%; display: flex; flex-direction: column;">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="鐧诲綍璐﹀彿" prop="userName">
<el-input v-model="queryParams.userName" placeholder="璇疯緭鍏ョ櫥褰曡处鍙�" clearable style="width: 240px" @keyup.enter="handleQuery" />
@@ -56,45 +56,47 @@
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
- <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
- <el-table-column type="selection" width="50" align="center" />
- <el-table-column label="鐢ㄦ埛缂栧彿" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
- <el-table-column label="鐧诲綍璐﹀彿" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
- <el-table-column label="鐢ㄦ埛鏄电О" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
- <el-table-column label="閮ㄩ棬" align="center" key="deptNames" prop="deptNames" v-if="columns[3].visible" :show-overflow-tooltip="true" />
- <el-table-column label="鎵嬫満鍙风爜" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
- <el-table-column label="鐘舵��" align="center" key="status" v-if="columns[5].visible">
- <template #default="scope">
- <el-switch
- v-model="scope.row.status"
- active-value="0"
- inactive-value="1"
- @change="handleStatusChange(scope.row)"
- ></el-switch>
- </template>
- </el-table-column>
- <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" v-if="columns[6].visible" width="160">
- <template #default="scope">
- <span>{{ parseTime(scope.row.createTime) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="鎿嶄綔" align="center" width="150" class-name="small-padding fixed-width">
- <template #default="scope">
- <el-tooltip content="淇敼" placement="top" v-if="scope.row.userId !== 1">
- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
- </el-tooltip>
- <el-tooltip content="鍒犻櫎" placement="top" v-if="scope.row.userId !== 1">
- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button>
- </el-tooltip>
- <el-tooltip content="閲嶇疆瀵嗙爜" placement="top" v-if="scope.row.userId !== 1">
- <el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button>
- </el-tooltip>
- <el-tooltip content="鍒嗛厤瑙掕壊" placement="top" v-if="scope.row.userId !== 1">
- <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
- </el-tooltip>
- </template>
- </el-table-column>
- </el-table>
+ <div style="flex: 1; overflow: hidden;">
+ <el-table v-loading="loading" :data="userList" height="100%" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
+ <el-table-column label="鐧诲綍璐﹀彿" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="鐢ㄦ埛鏄电О" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="閮ㄩ棬" align="center" key="deptNames" prop="deptNames" v-if="columns[3].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="鎵嬫満鍙风爜" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
+ <el-table-column label="鐘舵��" align="center" key="status" v-if="columns[5].visible">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ active-value="0"
+ inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" v-if="columns[6].visible" width="160">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="150" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="淇敼" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="閲嶇疆瀵嗙爜" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒嗛厤瑙掕壊" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-col>
</pane>
--
Gitblit v1.9.3