gaoluyang
8 天以前 fe167dd71a1300aeae07522db990d6b3fdb77a0e
Merge remote-tracking branch 'origin/dev_New' into dev_中兴实强

# Conflicts:
# multiple/assets/favicon/favicon.ico
# multiple/assets/screen/PCDZView.png
# multiple/assets/screen/ZXSQView.png
# multiple/config.json
已添加48个文件
已修改61个文件
20382 ■■■■ 文件已修改
index.html 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/PCDZico.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/PCDZLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/screen/login-background.png 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 274 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFile.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/product.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/customerVisit.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/customerService/index.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/salesRefund.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/attendanceRules.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/bank.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/class.js 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/monthlyStatistics.js 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/personalAttendanceRecords.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/socialSecuritySet.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/staffOnJob.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/staffSalaryMain.js 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/purchase_return_order.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/projectManagement/project.js 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/projectManagement/projectType.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/projectManagement/role.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/returnOrder.js 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/FormDialog.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProjectManagement/DiscussProgressDialog.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProjectManagement/ProgressReportDialog.vue 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SearchPanel/index.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/FileUpload.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/NotificationCenter/index.vue 591 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 1995 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/ImportExcel/index.vue 75 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/ProductSelectDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/BlacklistTab.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/HomeTab.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/customerVisit/index.vue 269 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/afterSalesHandling/components/formDia.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/afterSalesHandling/index.vue 493 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/expiryAfterSales/components/formDia.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/expiryAfterSales/index.vue 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 461 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/index.vue 641 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/document/index.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/revenueManagement/Modal.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue 226 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/salesRefund/index.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue 515 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/index.vue 822 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/classsSheduling/index.vue 1283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/components/formDia.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue 539 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/index.vue 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/components/auditDia.vue 216 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/components/formDia.vue 804 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/index.vue 407 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/components/formDia.vue 470 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/index.vue 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/invoiceEntry/components/Modal.vue 138 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/invoiceEntry/index.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementInvoiceLedger/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/New.vue 618 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/index.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 244 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/formDia.vue 1503 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/index.vue 430 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/projectDetail.vue 538 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/components/ProjectTypeDialog.vue 471 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/index.vue 516 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/roles/index.vue 295 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/components/formDia.vue 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/accidentReportingRecord/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/hazardousMaterialsControl/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/indicatorStats/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/invoiceRegistration/index.vue 62 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPayment/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/formDia.vue 489 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/index.vue 295 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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";
multiple/assets/favicon/PCDZico.ico
multiple/assets/logo/PCDZLogo.png
multiple/assets/screen/login-background.png
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
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'
    })
}
src/api/basicData/product.js
@@ -56,3 +56,12 @@
        params: query
    })
}
//  ä¸‹è½½äº§å“å¯¼å…¥æ¨¡æ¿
export function downloadProductModelImportTemplate() {
    return request({
        url: '/basic/product/export',
        method: 'get',
        responseType: 'blob'
    })
}
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
  })
}
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,
  })
}
}
// æŸ¥è¯¢æ‰€æœ‰å®¢æˆ·ä¿¡æ¯
// /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,
    })
}
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,
  });
}
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",
  });
}
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,
  });
}
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,
    })
}
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,
  });
}
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,
    });
}
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,
  });
}
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({
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,
  });
}
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
    });
}
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'
  })
}
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'
  })
}
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,
    });
}
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,
  });
}
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)
})
// è¯¦æƒ…模式不展示“确认”按钮,其它类型正常显示
const showConfirm = computed(() => props.operationType !== 'detail')
const computedTitle = computed(() => {
  if (typeof props.title === 'function') {
    return props.title(props.operationType)
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>
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>
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>
src/components/Upload/FileUpload.vue
@@ -49,9 +49,14 @@
  emits("remove", file);
};
const clearFiles = () => {
  fileList.value = [];
};
defineExpose({
  fileList,
  uploadApi,
  clearFiles,
});
</script>
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 {
          // è§£æž jumpPath,分离路径和查询参数
          const [path, queryString] = item.jumpPath.split('?')
          let query = {}
          if (queryString) {
            // è§£æžæŸ¥è¯¢å‚æ•°
            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 {
            // è§£æž jumpPath,分离路径和查询参数
            const [path, queryString] = item.jumpPath.split("?");
            let query = {};
            if (queryString) {
              // è§£æžæŸ¥è¯¢å‚æ•°
              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 ä¸­å¤„理)
// è¿™é‡Œåªè´Ÿè´£æ•°æ®åŠ è½½ï¼Œä¸æŽ§åˆ¶æ˜¾ç¤º
  // ç›‘听父组件传递的 visible çŠ¶æ€ï¼ˆé€šè¿‡ watch åœ¨ Navbar ä¸­å¤„理)
  // è¿™é‡Œåªè´Ÿè´£æ•°æ®åŠ è½½ï¼Œä¸æŽ§åˆ¶æ˜¾ç¤º
// æš´éœ²æ–¹æ³•供外部调用
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>
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" },
      },
    ],
  },
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
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>仅允许导入xls、xlsx格式文件。</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">
                æ”¯æŒä¸Šä¼ å›¾ç‰‡ã€æ–‡æ¡£ç­‰æ–‡ä»¶ï¼Œå•个文件不超过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() };
  // åŠ¨æ€æž„å»ºä¸Šä¼ URL
  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", {}, "客户档案.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", {}, "客户档案.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);
          // è¿™é‡Œå¯ä»¥è°ƒç”¨æ›´æ–°æŽ¥å£
          // å®žé™…项目中需要根据后端接口进行调整
          // ç¤ºä¾‹ï¼šupdateCustomerFollow(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(() => {
        // è¿™é‡Œå¯ä»¥è°ƒç”¨åˆ é™¤æŽ¥å£
        // å®žé™…项目中需要根据后端接口进行调整
        // ç¤ºä¾‹ï¼šdeleteCustomerFollow(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;
    // è½¬æ¢ä¸ºç¬¦åˆElement 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",
      }));
      // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
      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;
    // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
    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);
              // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
              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>
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>
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>
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"
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);
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);
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;
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>
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) => {
  // ç¦ç”¨ä»Šå¤©ä¹‹å‰çš„æ—¥æœŸ
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>
</style>
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:接口是没有对接的,需要新增接口,为写报告添加的
// æ‰“开附件弹框
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>
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('提交数据失败,请稍后重试');
            });
        }
    });
}
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;
    // å–消注释并使用真实API
    // 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;
            // å–消注释并使用真实API
            // 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("已取消");
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>
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>
.descriptions::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 4px;
  height: 1rem;
  background-color: #002FA7; /* Element é»˜è®¤çº¢è‰² */
  border-radius: 2px;
}
</style>
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>
.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>
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;
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();
};
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">
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>
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 -->
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
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>
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>
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">
              æ­£å¸¸
            </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: "正常",
    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: "正常",
    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 || "正常";
});
// è¡Œæ ·å¼ï¼šå¼‚常高亮
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" ? "迟到" : "正常";
    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 = "正常";
  // æ‰“卡按钮文本
  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 "正常";
      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 "正常";
      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;
      }
      // æ£€æŸ¥æ˜¯å¦ä½¿ç”¨HTTPS
      const isSecureContext =
        window.isSecureContext || window.location.protocol === "https:";
      console.log(
        "当前协议:",
        window.location.protocol,
        "是否安全上下文:",
        isSecureContext
      );
      if (!isSecureContext) {
        console.warn("当前不是HTTPS协议,地理位置API可能受限");
      }
      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 += "(注意:生产环境需要使用HTTPS协议才能获取位置)";
          }
          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>
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>
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: "",
  };
src/views/personnelManagement/dimission/index.vue
@@ -76,6 +76,10 @@
    },
  },
  {
    label: "离职日期",
    prop: "leaveDate",
  },
  {
    label: "员工编号",
    prop: "staffNo",
  },
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>
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>
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>
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>
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>
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",
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>
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>
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="速算扣除数/元"
          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);
});
// æ ¹æ®å®¡æ ¸äººID获取审核人名称
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>
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>
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>
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>
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;
        // å…¼å®¹ä¸åŒçš„字段名:purchaseContractNumber æˆ– 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;
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("请至少选中一条数据");
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) => {
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>
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="请输入整单折扣额"/>
        </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="请输入整单折扣率"/>
        </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>
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>
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>
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>
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>
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>
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,
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,
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;
      })
src/views/productionManagement/productionReporting/index.vue
@@ -163,6 +163,11 @@
      width: 120,
    },
    {
      label: "工序",
      prop: "process",
      width: 120,
    },
    {
      label: "工单编号",
      prop: "workOrderNo",
      width: 120,
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);
    // å¦‚果是NaN,保持原值
    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 = "";
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?.('正在上传文件,请稍候...')
  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>
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>
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="法人代表">{{ 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>
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>
在上述文件截断后对比
src/views/projectManagement/projectType/index.vue src/views/projectManagement/roles/index.vue src/views/qualityManagement/finalInspection/components/formDia.vue src/views/qualityManagement/nonconformingManagement/components/formDia.vue src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue src/views/qualityManagement/nonconformingManagement/index.vue src/views/qualityManagement/processInspection/components/formDia.vue src/views/qualityManagement/rawMaterialInspection/components/formDia.vue src/views/reportAnalysis/productionAnalysis/components/center-center.vue src/views/safeProduction/accidentReportingRecord/index.vue src/views/safeProduction/hazardousMaterialsControl/index.vue src/views/salesManagement/deliveryLedger/index.vue src/views/salesManagement/indicatorStats/index.vue src/views/salesManagement/invoiceRegistration/index.vue src/views/salesManagement/receiptPayment/index.vue src/views/salesManagement/returnOrder/components/formDia.vue src/views/salesManagement/returnOrder/index.vue src/views/salesManagement/salesLedger/index.vue src/views/salesManagement/salesQuotation/index.vue src/views/system/user/index.vue