c435f1c060982615c4ad0c3049b14ca84babe0df..a4d0446d7c1c1e56641fd4e887ad4d0ecd0534ca
9 天以前 zhangwencui
排班管理页面完成70%
a4d044 对比 | 目录
9 天以前 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
e2317a 对比 | 目录
9 天以前 huminmin
项目管理:增加项目角色页面,对接项目角色接口
499b38 对比 | 目录
9 天以前 buhuazhen
fix(会议申请): 修复当天会议时间选择逻辑
1c0f6b 对比 | 目录
9 天以前 gongchunyi
fix: 批量删除危险物料管控记录时却只能删除一个
55690b 对比 | 目录
9 天以前 ZN
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
ac13bd 对比 | 目录
9 天以前 ZN
feat: 更新售后管理模块,新增产品选择弹窗和统计卡片
2307a3 对比 | 目录
9 天以前 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
0fd3ad 对比 | 目录
9 天以前 gaoluyang
进销存升级 1.添加社会保险设置页面,开发与联调 2.丰富新增入职所填字段字段并于用户管理关联 3.修改人员薪资页面样式和逻辑
ae04c1 对比 | 目录
9 天以前 zhangwencui
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
7ad819 对比 | 目录
9 天以前 zhangwencui
客户档案回访提醒
634e41 对比 | 目录
9 天以前 gaoluyang
进销存升级 1.添加社会保险设置页面,开发与联调 2.丰富新增入职所填字段字段并于用户管理关联 3.修改人员薪资页面样式和逻辑
2763ef 对比 | 目录
9 天以前 liyong
feat(inventory): 发货台账添加产品名称和规格型号
212c58 对比 | 目录
已添加12个文件
已修改17个文件
8424 ■■■■ 文件已修改
pnpm-lock.yaml 274 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFile.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/customerService/index.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/attendanceRules.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/class.js 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/socialSecuritySet.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/projectManagement/role.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue 135 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/afterSalesHandling/index.vue 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 452 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/index.vue 640 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/classsSheduling/index.vue 2209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue 643 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/components/formDia.vue 819 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/index.vue 503 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/components/formDia.vue 369 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/index.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/roles/index.vue 295 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/hazardousMaterialsControl/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
@@ -74,4 +74,20 @@
        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/customerService/index.js
@@ -99,4 +99,34 @@
    url: '/afterSalesNearExpiryService/delete?ids=' + ids,
    method: 'delete',
  })
}
}
// æŸ¥è¯¢æ‰€æœ‰å®¢æˆ·ä¿¡æ¯
// /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/personnelManagement/attendanceRules.js
@@ -1,6 +1,6 @@
import request from "@/utils/request";
// èŽ·å–æ‰“å¡è§„åˆ™åˆ—è¡¨
// èŽ·å–ç­æ¬¡åˆ—è¡¨
export function getAttendanceRules(query) {
  return request({
    url: "/personalAttendanceLocationConfig/listPage",
@@ -9,7 +9,7 @@
  });
}
// æ–°å¢žæ‰“卡规则
// æ–°å¢žç­æ¬¡
export function addAttendanceRule(data) {
  return request({
    url: "/personalAttendanceLocationConfig/add",
@@ -18,7 +18,7 @@
  });
}
// æ›´æ–°æ‰“卡规则
// æ›´æ–°ç­æ¬¡
export function updateAttendanceRule(data) {
  return request({
    url: "/attendanceRules/update",
@@ -27,7 +27,7 @@
  });
}
// åˆ é™¤æ‰“卡规则
// åˆ é™¤ç­æ¬¡
export function deleteAttendanceRule(ids) {
  return request({
    url: `/personalAttendanceLocationConfig/del`,
@@ -36,7 +36,7 @@
  });
}
// èŽ·å–å•ä¸ªæ‰“å¡è§„åˆ™è¯¦æƒ…
// èŽ·å–å•ä¸ªç­æ¬¡è¯¦æƒ…
export function getAttendanceRuleDetail(id) {
  return request({
    url: `/attendanceRules/detail/${id}`,
src/api/personnelManagement/class.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
// ç­æ¬¡ç›¸å…³æŽ¥å£
import request from "@/utils/request";
// ç»©æ•ˆç®¡ç†-班次-分页查询
export function page(query) {
  return request({
    url: "/performanceShift/page",
    method: "get",
    params: query,
  });
}
// ç»©æ•ˆç®¡ç†-班次-年份分页查询
export function pageYear(query) {
  return request({
    url: "/performanceShift/pageYear",
    method: "get",
    params: query,
  });
}
// ç»©æ•ˆç®¡ç†-班次-排班
export function add(data) {
  return request({
    url: "/performanceShift/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: "/performanceShift/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: "/performanceShift/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'
  })
}
src/api/personnelManagement/socialSecuritySet.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
// ç¤¾ä¼šä¿é™©è®¾ç½®
import request from "@/utils/request";
// åˆ†é¡µæŸ¥è¯¢åˆ—表
export function socialSecurityListPage(query) {
  return request({
    url: "/socialSecurity/plan/listPage",
    method: "get",
    params: query,
  });
}
// æŸ¥è¯¢è¯¦æƒ…
export function socialSecurityInfo(id) {
  return request({
    url: "/socialSecurity/plan/" + id,
    method: "get",
  });
}
// æ–°å¢ž
export function socialSecurityAdd(data) {
  return request({
    url: "/socialSecurity/plan/add",
    method: "post",
    data,
  });
}
// ä¿®æ”¹
export function socialSecurityUpdate(data) {
  return request({
    url: "/socialSecurity/plan/update",
    method: "post",
    data,
  });
}
// åˆ é™¤
export function socialSecurityDelete(ids) {
  return request({
    url: "/socialSecurity/plan/delete",
    method: "delete",
    data: ids,
  });
}
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/views/basicData/customerFile/index.vue
@@ -267,9 +267,9 @@
        <el-form-item label="提醒时间:"
                      prop="reminderTime">
          <el-date-picker v-model="reminderForm.reminderTime"
                          type="date"
                          value-format="YYYY-MM-DD"
                          format="YYYY-MM-DD"
                          type="datetime"
                          value-format="YYYY-MM-DD HH:mm:ss"
                          format="YYYY-MM-DD HH:mm:ss"
                          placeholder="请选择提醒时间"
                          style="width: 100%" />
        </el-form-item>
@@ -369,7 +369,7 @@
    <!-- å®¢æˆ·è¯¦æƒ…对话框 -->
    <el-dialog title="客户详情"
               v-model="detailDialogVisible"
               width="800px"
               width="1000px"
               @close="closeDetailDialog">
      <!-- å®¢æˆ·åŸºæœ¬ä¿¡æ¯ -->
      <div class="detail-section">
@@ -481,14 +481,29 @@
                           label="跟进方式"
                           width="100" />
          <el-table-column prop="followUpLevel"
                           label="跟进程度"
                           width="120" />
                           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">
@@ -519,12 +534,90 @@
        </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>
  </div>
</template>
<script setup>
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
  import {
    addCustomer,
    delCustomer,
@@ -534,6 +627,8 @@
    addCustomerFollow,
    updateCustomerFollow,
    delCustomerFollow,
    addReturnVisit,
    getReturnVisit,
  } from "@/api/basicData/customerFile.js";
  import { ElMessageBox } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
@@ -545,6 +640,7 @@
  // å›žè®¿æé†’相关
  const reminderDialogVisible = ref(false);
  const reminderFormRef = ref();
  const currentCustomerId = ref();
  const reminderForm = reactive({
    customerName: "",
    reminderSwitch: false,
@@ -602,6 +698,22 @@
    maintenanceTime: "",
  });
  const negotiationRecords = ref([]);
  // é™„件相关
  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([
    {
@@ -1003,10 +1115,25 @@
  // æ‰“开回访提醒弹窗
  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;
  };
@@ -1016,20 +1143,48 @@
    proxy.resetForm("reminderFormRef");
    reminderDialogVisible.value = false;
  };
  const submitvalue = ref({});
  // æäº¤å›žè®¿æé†’
  const submitReminderForm = () => {
    console.log("提交回访提醒数据:", userStore.id, userStore);
    proxy.$refs.reminderFormRef.validate(valid => {
      if (valid) {
        // è¿™é‡Œå‡è®¾ä¸€ä¸ªæŽ¥å£æ¥æäº¤å›žè®¿æé†’数据
        // å®žé™…项目中需要根据后端接口进行调整
        console.log("提交回访提醒数据:", reminderForm);
        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,
          };
        }
        // æ¨¡æ‹ŸæŽ¥å£è°ƒç”¨
        setTimeout(() => {
          proxy.$modal.msgSuccess("回访提醒设置成功");
          closeReminderDialog();
        }, 1000);
        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("设置失败");
          });
      }
    });
  };
@@ -1188,6 +1343,143 @@
      });
  };
  // æ‰“开附件弹窗
  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();
@@ -1255,4 +1547,34 @@
    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/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,18 +208,106 @@
// æ—¶é—´é€‰é¡¹ï¼ˆä»¥åŠå°æ—¶ä¸ºé—´éš”)
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) {
    if (hour < currentHour && isSameDay) {
      continue
    }
    if (hour === currentHour && currentMinute > 30) {
    if (hour === currentHour && currentMinute > 30 && isSameDay) {
      continue
    }
    // æ¯ä¸ªå°æ—¶æ·»åŠ ä¸¤ä¸ªé€‰é¡¹ï¼šæ•´ç‚¹å’ŒåŠç‚¹
@@ -249,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/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"
@@ -81,69 +137,107 @@
    },
});
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: "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:"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: "disRes",
    width:300,
  },
  {
    label: "关联部门",
    prop: "deptName",
    width: 200,
    align: "center"
  },
    {
        dataType: "action",
        label: "操作",
@@ -197,6 +291,12 @@
const fileListDialogVisible = ref(false)
const currentFileRow = ref(null)
// é‡ç½®
const handleReset = () => {
  Object.keys(searchForm.value).forEach(key => {
    searchForm.value[key] = ""
  })
}
// æ‰“开附件弹框
const openFilesFormDia = async (row) => {
@@ -386,5 +486,10 @@
</script>
<style scoped>
.search-wrapper {
  background: white;
  padding: 1rem 1rem 0 1rem;
  border: 8px;
  border-radius: 16px;
}
</style>
src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,245 @@
<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="id"
      @selection-change="handleSelectionChange" @select="handleSelect">
      <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({
  productName: "",
  model: "",
});
const page = reactive({
  pageNum: 1,
  pageSize: 10,
});
const loading = ref(false);
const tableData = ref([]);
const total = ref(0);
const multipleSelection = ref([]);
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 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) => {
  if (props.single && val.length > 1) {
    // å¦‚果限制为单个选择,只保留最后一个选中的
    const lastSelected = val[val.length - 1];
    multipleSelection.value = [lastSelected];
    // æ¸…空表格选中状态,然后重新选中最后一个
    nextTick(() => {
      if (tableRef.value) {
        tableRef.value.clearSelection();
        tableRef.value.toggleRowSelection(lastSelected, true);
      }
    });
  } else {
    multipleSelection.value = val;
  }
}
// å¤„理单个选择
const handleSelect = (selection, row) => {
  if (props.single) {
    // å¦‚果限制为单个,清空其他选择,只保留当前行
    if (selection.includes(row)) {
      // é€‰ä¸­å½“前行时,清空其他选中
      multipleSelection.value = [row];
      nextTick(() => {
        if (tableRef.value) {
          tableData.value.forEach((item) => {
            if (item.id !== row.id) {
              tableRef.value.toggleRowSelection(item, false);
            }
          });
        }
      });
    }
  }
}
function onSearch() {
  page.pageNum = 1;
  loadData();
}
function onReset() {
  query.productName = "";
  query.model = "";
  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 {
    multipleSelection.value = []; // ç¿»é¡µ/搜索后清空选择更符合预期
    let filtered = props.products || [];
    // æœ¬åœ°æœç´¢è¿‡æ»¤
    if (query.productName) {
      filtered = filtered.filter(item => item.productName && item.productName.includes(query.productName));
    }
    if (query.model) {
      filtered = filtered.filter(item => item.model && item.model.includes(query.model));
    }
    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) {
        tableData.value.forEach(row => {
          if (props.selectedIds && props.selectedIds.includes(row.id)) {
            tableRef.value.toggleRowSelection(row, true);
          }
        });
      }
    });
  } finally {
    loading.value = false;
  }
}
// ç›‘听弹窗打开,重置选择
watch(() => props.modelValue, (visible) => {
  if (visible) {
    // æ¯æ¬¡æ‰“开时重新初始化选中状态(multipleSelection ä¼šé€šè¿‡ loadData ä¸­çš„回显逻辑自动更新,但初始需置空避免重复)
    multipleSelection.value = [];
    page.pageNum = 1;
    loadData();
  }
});
// ç›‘听数据源变化
watch(() => props.products, () => {
  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,286 @@
                </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 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 => i.id !== row.id)
        },
      },
    ],
  },
])
const tableData = ref([])
// é€‰æ‹©äº§å“å¼¹çª—
const isShowProductSelectDialog = ref(false)
const handleSelectProducts = (rows) => {
  if (!Array.isArray(rows)) return
  const existingIds = new Set(tableData.value.map(i => i.id))
  const mapped = rows
    .filter(r => !existingIds.has(r.id))
    .map(r => ({
      id: r.id,
      productCategory: r.productName,
      specificationModel: r.model,
      unit: r.unit || '',
      approveStatus: null,
      shippingStatus: '',
      expressCompany: '',
      expressNumber: '',
      shippingCarNumber: '',
      shippingDate: '',
      quantity: 0,
      taxRate: 0,
      taxInclusiveUnitPrice: 0,
      taxInclusiveTotalPrice: 0,
      taxExclusiveTotalPrice: 0,
    }))
  tableData.value = tableData.value.concat(mapped)
}
const currentSelectedProductIds = computed(() => {
  return tableData.value.map(item => item.id)
})
const associatedSalesOrderNumberChange = () => {
  const opt = associatedSalesOrderNumberOptions.value.find(
    (item) => item.value === form.value.salesContractNo
  )
  tableData.value = opt?.productData || []
  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 || []
})
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 +426,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,513 @@
<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 === 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: "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] = ""
  })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
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/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
@@ -140,9 +140,9 @@
  });
  const dialogTitle = computed(() => {
    if (props.operationType === "add") return "新增打卡规则";
    if (props.operationType === "edit") return "编辑打卡规则";
    return "查看打卡规则";
    if (props.operationType === "add") return "新增班次";
    if (props.operationType === "edit") return "编辑班次";
    return "查看班次";
  });
  // è¡¨å•数据
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue
@@ -2,14 +2,14 @@
  <div class="app-container">
    <!-- é¡µé¢æ ‡é¢˜å’Œæ“ä½œæŒ‰é’® -->
    <div class="page-header">
      <div class="title">打卡规则配置</div>
      <div class="title">班次配置</div>
      <div class="actions">
        <el-button type="primary"
                   @click="openForm('add')">
          <el-icon>
            <Plus />
          </el-icon>
          æ–°å¢žè§„则
          æ–°å¢žç­æ¬¡
        </el-button>
      </div>
    </div>
@@ -50,7 +50,7 @@
        </el-button>
      </el-form-item>
    </el-form> -->
    <!-- è§„则列表 -->
    <!-- ç­æ¬¡åˆ—表 -->
    <el-card shadow="never"
             class="mb16">
      <el-table :data="tableData"
@@ -110,7 +110,7 @@
                  @pagination="paginationChange"
                  class="mt10" />
    </el-card>
    <!-- æ–°å¢ž/编辑规则弹窗 -->
    <!-- æ–°å¢ž/编辑班次弹窗 -->
    <rule-form ref="ruleFormRef"
               v-model="dialogVisible"
               :operation-type="operationType"
@@ -206,7 +206,7 @@
    return "";
  };
  // æŸ¥è¯¢è§„则列表
  // æŸ¥è¯¢ç­æ¬¡åˆ—表
  const fetchData = () => {
    tableLoading.value = true;
    getAttendanceRules({ ...page, ...searchForm })
@@ -240,9 +240,9 @@
    dialogVisible.value = true;
  };
  // åˆ é™¤è§„则
  // åˆ é™¤ç­æ¬¡
  const handleDelete = id => {
    ElMessageBox.confirm("确定要删除这条规则吗?", "删除确认", {
    ElMessageBox.confirm("确定要删除这条班次吗?", "删除确认", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
src/views/personnelManagement/attendanceCheckin/index.vue
@@ -60,6 +60,15 @@
      </el-descriptions>
    </el-card> -->
    <div class="attendance-operation">
      <el-button @click="handleBack"
                 type="default"
                 size="small"
                 style="margin-right: 16px">
        <el-icon>
          <ArrowLeft />
        </el-icon>
        è¿”回排班管理
      </el-button>
      <!-- æŸ¥è¯¢æ¡ä»¶ï¼ˆç®¡ç†å‘˜è€ƒå‹¤æ—¥æŠ¥ï¼‰ -->
      <el-form :model="searchForm"
               :inline="true"
@@ -170,6 +179,7 @@
<script setup>
  import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
  import { useRouter } from "vue-router";
  import { ElMessage, ElMessageBox } from "element-plus";
  import {
    createPersonalAttendanceRecord,
@@ -178,9 +188,10 @@
  } from "@/api/personnelManagement/personalAttendanceRecords.js";
  import Pagination from "@/components/Pagination/index.vue";
  import { deptTreeSelect } from "@/api/system/user.js";
  import { Refresh, Search } from "@element-plus/icons-vue";
  import { Refresh, Search, ArrowLeft } from "@element-plus/icons-vue";
  const { proxy } = getCurrentInstance();
  const router = useRouter();
  const tableLoading = ref(false);
  // åˆ†é¡µå‚æ•°
  const page = reactive({
@@ -445,6 +456,13 @@
    fetchDeptOptions();
  });
  // è¿”回排班管理页面
  const handleBack = () => {
    router.push({
      path: "/personnelManagement/classsSheduling/index",
    });
  };
  onBeforeUnmount(() => {
    if (timer) {
      clearInterval(timer);
src/views/personnelManagement/classsSheduling/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2209 @@
<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.deptId"
                            :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 === '0',
                      'shift-box-mid': m.shift === '1',
                      'shift-box-night': m.shift === '2',
                      'shift-box-rest': m.shift === '3',
                      'shift-box-leave': m.shift === '4',
                      'shift-box-other': m.shift === '5',
                      'shift-box-business': m.shift === '6',
                    }">
                      <span class="shift-text">{{ getShiftByDic(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.locationName
                          }}</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.userId"
                       :label="item.nickName"
                       :value="item.userId">
            </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="item.locationName"
                       :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,
    selectUserCondition,
  } from "@/api/personnelManagement/class";
  import { deptTreeSelect } from "@/api/system/user.js";
  import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js";
  const { proxy } = getCurrentInstance();
  const router = useRouter();
  // æŸ¥è¯¢æ¡ä»¶
  const query = reactive({
    userName: "",
    deptId: "",
    year: new Date(),
    month: new Date().getMonth() + 1,
  });
  // æœˆä»½é€‰é¡¹
  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([
    { weekNum: 1, week: "周一", day: "01" },
    { weekNum: 1, week: "周二", day: "02" },
    { weekNum: 1, week: "周三", day: "03" },
    { weekNum: 1, week: "周四", day: "04" },
    { weekNum: 1, week: "周五", day: "05" },
    { weekNum: 1, week: "周六", day: "06" },
    { weekNum: 2, week: "周日", day: "07" },
    { weekNum: 2, week: "周一", day: "08" },
    { weekNum: 2, week: "周二", day: "09" },
    { weekNum: 2, week: "周三", day: "10" },
    { weekNum: 2, week: "周四", day: "11" },
    { weekNum: 2, week: "周五", day: "12" },
    { weekNum: 2, week: "周六", day: "13" },
    { weekNum: 3, week: "周日", day: "14" },
    { weekNum: 3, week: "周一", day: "15" },
    { weekNum: 3, week: "周二", day: "16" },
    { weekNum: 3, week: "周三", day: "17" },
    { weekNum: 3, week: "周四", day: "18" },
    { weekNum: 3, week: "周五", day: "19" },
    { weekNum: 3, week: "周六", day: "20" },
    { weekNum: 4, week: "周日", day: "21" },
    { weekNum: 4, week: "周一", day: "22" },
    { weekNum: 4, week: "周二", day: "23" },
    { weekNum: 4, week: "周三", day: "24" },
    { weekNum: 4, week: "周四", day: "25" },
    { weekNum: 4, week: "周五", day: "26" },
    { weekNum: 4, week: "周六", day: "27" },
    { weekNum: 5, week: "周日", day: "28" },
    { weekNum: 5, week: "周一", day: "29" },
    { weekNum: 5, week: "周二", day: "30" },
  ]);
  // ç­æ¬¡ç±»åž‹
  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([
    {
      id: 1,
      name: "张三",
      monthlyAttendance: {
        totalAttendance: 22,
        æ—©ç­: 10,
        ä¸­ç­: 8,
        å¤œç­: 4,
        ä¼‘息: 6,
        è¯·å‡: 0,
        å‡ºå·®: 0,
      },
      day0: 10,
      day1: 8,
      day2: 4,
      day3: 6,
      day4: 0,
      day6: 0,
      list: [
        { id: 1, shift: "0" },
        { id: 2, shift: "0" },
        { id: 3, shift: "1" },
        { id: 4, shift: "1" },
        { id: 5, shift: "2" },
        { id: 6, shift: "2" },
        { id: 7, shift: "3" },
        { id: 8, shift: "0" },
        { id: 9, shift: "0" },
        { id: 10, shift: "1" },
        { id: 11, shift: "1" },
        { id: 12, shift: "2" },
        { id: 13, shift: "2" },
        { id: 14, shift: "3" },
        { id: 15, shift: "0" },
        { id: 16, shift: "0" },
        { id: 17, shift: "1" },
        { id: 18, shift: "1" },
        { id: 19, shift: "2" },
        { id: 20, shift: "2" },
        { id: 21, shift: "3" },
        { id: 22, shift: "0" },
        { id: 23, shift: "0" },
        { id: 24, shift: "1" },
        { id: 25, shift: "1" },
        { id: 26, shift: "2" },
        { id: 27, shift: "2" },
        { id: 28, shift: "3" },
        { id: 29, shift: "0" },
        { id: 30, shift: "0" },
      ],
    },
    {
      id: 2,
      name: "李四",
      monthlyAttendance: {
        totalAttendance: 20,
        æ—©ç­: 8,
        ä¸­ç­: 6,
        å¤œç­: 6,
        ä¼‘息: 8,
        è¯·å‡: 2,
        å‡ºå·®: 0,
      },
      day0: 8,
      day1: 6,
      day2: 6,
      day3: 8,
      day4: 2,
      day6: 0,
      list: [
        { id: 31, shift: "1" },
        { id: 32, shift: "1" },
        { id: 33, shift: "2" },
        { id: 34, shift: "2" },
        { id: 35, shift: "3" },
        { id: 36, shift: "0" },
        { id: 37, shift: "0" },
        { id: 38, shift: "1" },
        { id: 39, shift: "1" },
        { id: 40, shift: "2" },
        { id: 41, shift: "2" },
        { id: 42, shift: "3" },
        { id: 43, shift: "0" },
        { id: 44, shift: "0" },
        { id: 45, shift: "1" },
        { id: 46, shift: "1" },
        { id: 47, shift: "2" },
        { id: 48, shift: "2" },
        { id: 49, shift: "3" },
        { id: 50, shift: "0" },
        { id: 51, shift: "0" },
        { id: 52, shift: "4" },
        { id: 53, shift: "4" },
        { id: 54, shift: "1" },
        { id: 55, shift: "1" },
        { id: 56, shift: "2" },
        { id: 57, shift: "2" },
        { id: 58, shift: "3" },
        { id: 59, shift: "0" },
        { id: 60, shift: "0" },
      ],
    },
    {
      id: 3,
      name: "王五",
      monthlyAttendance: {
        totalAttendance: 23,
        æ—©ç­: 9,
        ä¸­ç­: 9,
        å¤œç­: 5,
        ä¼‘息: 5,
        è¯·å‡: 0,
        å‡ºå·®: 2,
      },
      day0: 9,
      day1: 9,
      day2: 5,
      day3: 5,
      day4: 0,
      day6: 2,
      list: [
        { id: 61, shift: "2" },
        { id: 62, shift: "2" },
        { id: 63, shift: "3" },
        { id: 64, shift: "0" },
        { id: 65, shift: "0" },
        { id: 66, shift: "1" },
        { id: 67, shift: "1" },
        { id: 68, shift: "2" },
        { id: 69, shift: "2" },
        { id: 70, shift: "3" },
        { id: 71, shift: "0" },
        { id: 72, shift: "0" },
        { id: 73, shift: "1" },
        { id: 74, shift: "1" },
        { id: 75, shift: "2" },
        { id: 76, shift: "2" },
        { id: 77, shift: "3" },
        { id: 78, shift: "0" },
        { id: 79, shift: "0" },
        { id: 80, shift: "1" },
        { id: 81, shift: "1" },
        { id: 82, shift: "6" },
        { id: 83, shift: "6" },
        { id: 84, shift: "2" },
        { id: 85, shift: "2" },
        { id: 86, shift: "3" },
        { id: 87, shift: "0" },
        { id: 88, shift: "0" },
        { id: 89, shift: "1" },
        { id: 90, shift: "1" },
      ],
    },
    {
      id: 4,
      name: "张三",
      monthlyAttendance: {
        totalAttendance: 22,
        æ—©ç­: 10,
        ä¸­ç­: 8,
        å¤œç­: 4,
        ä¼‘息: 6,
        è¯·å‡: 0,
        å‡ºå·®: 0,
      },
      day0: 10,
      day1: 8,
      day2: 4,
      day3: 6,
      day4: 0,
      day6: 0,
      list: [
        { id: 1, shift: "0" },
        { id: 2, shift: "0" },
        { id: 3, shift: "1" },
        { id: 4, shift: "1" },
        { id: 5, shift: "2" },
        { id: 6, shift: "2" },
        { id: 7, shift: "3" },
        { id: 8, shift: "0" },
        { id: 9, shift: "0" },
        { id: 10, shift: "1" },
        { id: 11, shift: "1" },
        { id: 12, shift: "2" },
        { id: 13, shift: "2" },
        { id: 14, shift: "3" },
        { id: 15, shift: "0" },
        { id: 16, shift: "0" },
        { id: 17, shift: "1" },
        { id: 18, shift: "1" },
        { id: 19, shift: "2" },
        { id: 20, shift: "2" },
        { id: 21, shift: "3" },
        { id: 22, shift: "0" },
        { id: 23, shift: "0" },
        { id: 24, shift: "1" },
        { id: 25, shift: "1" },
        { id: 26, shift: "2" },
        { id: 27, shift: "2" },
        { id: 28, shift: "3" },
        { id: 29, shift: "0" },
        { id: 30, shift: "0" },
      ],
    },
    {
      id: 5,
      name: "张三",
      monthlyAttendance: {
        totalAttendance: 22,
        æ—©ç­: 10,
        ä¸­ç­: 8,
        å¤œç­: 4,
        ä¼‘息: 6,
        è¯·å‡: 0,
        å‡ºå·®: 0,
      },
      day0: 10,
      day1: 8,
      day2: 4,
      day3: 6,
      day4: 0,
      day6: 0,
      list: [
        { id: 1, shift: "0" },
        { id: 2, shift: "0" },
        { id: 3, shift: "1" },
        { id: 4, shift: "1" },
        { id: 5, shift: "2" },
        { id: 6, shift: "2" },
        { id: 7, shift: "3" },
        { id: 8, shift: "0" },
        { id: 9, shift: "0" },
        { id: 10, shift: "1" },
        { id: 11, shift: "1" },
        { id: 12, shift: "2" },
        { id: 13, shift: "2" },
        { id: 14, shift: "3" },
        { id: 15, shift: "0" },
        { id: 16, shift: "0" },
        { id: 17, shift: "1" },
        { id: 18, shift: "1" },
        { id: 19, shift: "2" },
        { id: 20, shift: "2" },
        { id: 21, shift: "3" },
        { id: 22, shift: "0" },
        { id: 23, shift: "0" },
        { id: 24, shift: "1" },
        { id: 25, shift: "1" },
        { id: 26, shift: "2" },
        { id: 27, shift: "2" },
        { id: 28, shift: "3" },
        { id: 29, shift: "0" },
        { id: 30, shift: "0" },
      ],
    },
    {
      id: 6,
      name: "张三",
      monthlyAttendance: {
        totalAttendance: 22,
        æ—©ç­: 10,
        ä¸­ç­: 8,
        å¤œç­: 4,
        ä¼‘息: 6,
        è¯·å‡: 0,
        å‡ºå·®: 0,
      },
      day0: 10,
      day1: 8,
      day2: 4,
      day3: 6,
      day4: 0,
      day6: 0,
      list: [
        { id: 1, shift: "0" },
        { id: 2, shift: "0" },
        { id: 3, shift: "1" },
        { id: 4, shift: "1" },
        { id: 5, shift: "2" },
        { id: 6, shift: "2" },
        { id: 7, shift: "3" },
        { id: 8, shift: "0" },
        { id: 9, shift: "0" },
        { id: 10, shift: "1" },
        { id: 11, shift: "1" },
        { id: 12, shift: "2" },
        { id: 13, shift: "2" },
        { id: 14, shift: "3" },
        { id: 15, shift: "0" },
        { id: 16, shift: "0" },
        { id: 17, shift: "1" },
        { id: 18, shift: "1" },
        { id: 19, shift: "2" },
        { id: 20, shift: "2" },
        { id: 21, shift: "3" },
        { id: 22, shift: "0" },
        { id: 23, shift: "0" },
        { id: 24, shift: "1" },
        { id: 25, shift: "1" },
        { id: 26, shift: "2" },
        { id: 27, shift: "2" },
        { id: 28, shift: "3" },
        { id: 29, shift: "0" },
        { id: 30, shift: "0" },
      ],
    },
  ]);
  // å½“前页
  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([
    {
      id: 1,
      name: "张三",
      work_time: 260,
      day0: 98,
      day1: 78,
      day2: 46,
      day3: 74,
      day4: 14,
      day6: 10,
      monthList: [
        {
          totalMonthAttendance: 22,
          day0: 10,
          day1: 8,
          day2: 4,
          day3: 6,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 19,
          day0: 7,
          day1: 6,
          day2: 6,
          day3: 9,
          day4: 3,
          day6: 0,
        },
        {
          totalMonthAttendance: 23,
          day0: 9,
          day1: 9,
          day2: 5,
          day3: 5,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 0,
          day6: 1,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 18,
          day0: 6,
          day1: 6,
          day2: 6,
          day3: 8,
          day4: 4,
          day6: 0,
        },
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 0,
          day6: 1,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 19,
          day0: 7,
          day1: 7,
          day2: 5,
          day3: 8,
          day4: 2,
          day6: 0,
        },
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 0,
        },
      ],
    },
    {
      id: 2,
      name: "李四",
      work_time: 252,
      day0: 90,
      day1: 72,
      day2: 50,
      day3: 76,
      day4: 18,
      day6: 8,
      monthList: [
        {
          totalMonthAttendance: 21,
          day0: 9,
          day1: 7,
          day2: 5,
          day3: 7,
          day4: 2,
          day6: 0,
        },
        {
          totalMonthAttendance: 18,
          day0: 6,
          day1: 6,
          day2: 6,
          day3: 10,
          day4: 4,
          day6: 0,
        },
        {
          totalMonthAttendance: 22,
          day0: 8,
          day1: 8,
          day2: 6,
          day3: 6,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 20,
          day0: 7,
          day1: 6,
          day2: 7,
          day3: 8,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 17,
          day0: 5,
          day1: 5,
          day2: 7,
          day3: 9,
          day4: 5,
          day6: 0,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 1,
          day6: 0,
        },
        {
          totalMonthAttendance: 20,
          day0: 7,
          day1: 6,
          day2: 7,
          day3: 8,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 18,
          day0: 6,
          day1: 6,
          day2: 6,
          day3: 9,
          day4: 3,
          day6: 0,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 0,
        },
      ],
    },
    {
      id: 3,
      name: "王五",
      work_time: 268,
      day0: 102,
      day1: 82,
      day2: 48,
      day3: 70,
      day4: 10,
      day6: 14,
      monthList: [
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 24,
          day0: 10,
          day1: 9,
          day2: 5,
          day3: 4,
          day4: 0,
          day6: 3,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 8,
          day2: 5,
          day3: 5,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 24,
          day0: 10,
          day1: 9,
          day2: 5,
          day3: 4,
          day4: 0,
          day6: 3,
        },
      ],
    },
    {
      id: 4,
      name: "赵六",
      work_time: 248,
      day0: 88,
      day1: 70,
      day2: 50,
      day3: 78,
      day4: 20,
      day6: 6,
      monthList: [
        {
          totalMonthAttendance: 20,
          day0: 8,
          day1: 6,
          day2: 6,
          day3: 8,
          day4: 3,
          day6: 0,
        },
        {
          totalMonthAttendance: 17,
          day0: 5,
          day1: 5,
          day2: 7,
          day3: 10,
          day4: 5,
          day6: 0,
        },
        {
          totalMonthAttendance: 21,
          day0: 7,
          day1: 7,
          day2: 7,
          day3: 7,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 19,
          day0: 6,
          day1: 6,
          day2: 7,
          day3: 9,
          day4: 2,
          day6: 1,
        },
        {
          totalMonthAttendance: 20,
          day0: 7,
          day1: 7,
          day2: 6,
          day3: 8,
          day4: 2,
          day6: 0,
        },
        {
          totalMonthAttendance: 16,
          day0: 4,
          day1: 4,
          day2: 8,
          day3: 10,
          day4: 6,
          day6: 0,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 20,
          day0: 7,
          day1: 7,
          day2: 6,
          day3: 8,
          day4: 2,
          day6: 0,
        },
        {
          totalMonthAttendance: 19,
          day0: 6,
          day1: 6,
          day2: 7,
          day3: 9,
          day4: 2,
          day6: 1,
        },
        {
          totalMonthAttendance: 20,
          day0: 7,
          day1: 7,
          day2: 6,
          day3: 8,
          day4: 2,
          day6: 0,
        },
        {
          totalMonthAttendance: 18,
          day0: 6,
          day1: 6,
          day2: 6,
          day3: 9,
          day4: 4,
          day6: 0,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 7,
          day2: 6,
          day3: 7,
          day4: 1,
          day6: 1,
        },
      ],
    },
    {
      id: 5,
      name: "钱七",
      work_time: 266,
      day0: 100,
      day1: 84,
      day2: 46,
      day3: 72,
      day4: 12,
      day6: 12,
      monthList: [
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 21,
          day0: 8,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 23,
          day0: 9,
          day1: 9,
          day2: 5,
          day3: 5,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 20,
          day0: 7,
          day1: 7,
          day2: 6,
          day3: 8,
          day4: 2,
          day6: 0,
        },
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 0,
          day6: 0,
        },
        {
          totalMonthAttendance: 23,
          day0: 9,
          day1: 9,
          day2: 5,
          day3: 5,
          day4: 0,
          day6: 2,
        },
        {
          totalMonthAttendance: 22,
          day0: 9,
          day1: 8,
          day2: 5,
          day3: 6,
          day4: 1,
          day6: 1,
        },
        {
          totalMonthAttendance: 20,
          day0: 7,
          day1: 7,
          day2: 6,
          day3: 8,
          day4: 2,
          day6: 0,
        },
        {
          totalMonthAttendance: 23,
          day0: 10,
          day1: 9,
          day2: 4,
          day3: 5,
          day4: 0,
          day6: 0,
        },
      ],
    },
  ]);
  // å¯¼å‡ºåŠ è½½çŠ¶æ€
  const downLoading = ref(false);
  // èŽ·å–éƒ¨é—¨åˆ—è¡¨
  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 refresh = () => {
    listForm.value = [];
    yearList.value = [];
    currentPage.value = 1;
    query.userName = "";
    query.deptId = "";
    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,
      deptId: query.deptId,
    })
      .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,
      deptId: query.deptId,
    }).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,
      userId: schedulingQuery.userId.join(","),
      shift: 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,
      deptId: query.deptId,
      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,
        shift: e,
      }).then(res => {
        proxy.$modal.msgSuccess("操作成功");
        m.shift = e;
      });
    }
  };
  // æŸ¥è¯¢è§„则列表
  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;
    // });
    selectUserCondition().then(res => {
      let arr = res.data;
      personList.value = arr;
    });
  };
  // æ ¹æ®å­—典获取日期
  const getDayByDic = e => {
    let obj = classType.value.find(m => m.locationName == e);
    if (obj) {
      return obj.id;
    }
  };
  // æ ¹æ®å­—典获取班次
  const getShiftByDic = e => {
    let obj = classType.value.find(m => m.id == e);
    if (obj) {
      return obj.locationName;
    }
    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/employeeRecord/components/BasicInfoSection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,182 @@
<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="aliasName">
          <el-input
            v-model="form.aliasName"
            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="single" />
            <el-option label="已婚" value="married" />
            <el-option label="离异" value="divorced" />
            <el-option label="丧偶" value="widowed" />
          </el-select>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="24">
      <el-col :span="10">
        <el-form-item label="角色" prop="roleIds">
          <el-select
            v-model="form.roleIds"
            multiple
            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.educationList" border>
        <el-table-column label="学历" prop="degree" width="120">
          <template #default="{ row }">
            <el-select
              v-model="row.degree"
              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="school" min-width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.school"
              placeholder="请输入"
              clearable
              maxlength="30"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="入学时间" prop="admissionDate" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.admissionDate"
              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="graduationDate" width="150">
          <template #default="{ row }">
            <el-date-picker
              v-model="row.graduationDate"
              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="academicDegree" width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.academicDegree"
              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.educationList.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.workExperienceList" border>
        <el-table-column label="前公司" prop="company" min-width="180">
          <template #default="{ row }">
            <el-input
              v-model="row.company"
              placeholder="请输入"
              clearable
              maxlength="30"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="前公司部门" prop="department" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.department"
              placeholder="请输入"
              clearable
              maxlength="20"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="前公司职位" prop="position" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.position"
              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="description" min-width="220">
          <template #default="{ row }">
            <el-input
              v-model="row.description"
              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.workExperienceList.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.educationList.push({
    degree: "",
    school: "",
    admissionDate: "",
    graduationDate: "",
    major: "",
    academicDegree: "",
  });
};
const removeEducationRow = (index) => {
  if (form.value.educationList.length <= 1) return;
  form.value.educationList.splice(index, 1);
};
const addWorkRow = () => {
  form.value.workExperienceList.push({
    company: "",
    department: "",
    position: "",
    startDate: "",
    endDate: "",
    description: "",
  });
};
const removeWorkRow = (index) => {
  if (form.value.workExperienceList.length <= 1) return;
  form.value.workExperienceList.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,148 @@
<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.emergencyContacts" border>
        <el-table-column label="紧急联系人姓名" prop="name" min-width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.name"
              placeholder="请输入"
              clearable
              maxlength="50"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人关系" prop="relation" min-width="140">
          <template #default="{ row }">
            <el-input
              v-model="row.relation"
              placeholder="请输入"
              clearable
              maxlength="20"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人手机" prop="phone" width="160">
          <template #default="{ row }">
            <el-input
              v-model="row.phone"
              placeholder="请输入"
              clearable
              maxlength="11"
              show-word-limit
            />
          </template>
        </el-table-column>
        <el-table-column label="紧急联系人住址" prop="address" min-width="220">
          <template #default="{ row }">
            <el-input
              v-model="row.address"
              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.emergencyContacts.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>
    <!-- ææ–™é™„ä»¶ -->
    <el-card class="form-card" shadow="never">
      <template #header>
        <div class="card-title">
          <span class="card-title-line">|</span>
          <span>材料附件</span>
          <span class="upload-tip">
            å›¾ç‰‡æ”¯æŒjpeg、jpg、png等格式,附件文件支持pdf、rar、zip、doc、docx格式。
          </span>
        </div>
      </template>
      <el-form-item label="附件">
        <el-upload
          v-model:file-list="form.attachments"
          action="#"
          :auto-upload="false"
          multiple
          list-type="picture-card"
        >
          <el-icon>
            <Plus />
          </el-icon>
        </el-upload>
      </el-form-item>
    </el-card>
  </div>
</template>
<script setup>
import { toRefs } from "vue";
import { Plus } from "@element-plus/icons-vue";
const props = defineProps({
  form: { type: Object, required: true },
});
const { form } = toRefs(props);
const addEmergencyRow = () => {
  form.value.emergencyContacts.push({
    name: "",
    relation: "",
    phone: "",
    address: "",
  });
};
const removeEmergencyRow = (index) => {
  if (form.value.emergencyContacts.length <= 1) return;
  form.value.emergencyContacts.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;
}
.upload-tip {
  margin-left: 12px;
  font-size: 12px;
  color: #909399;
}
</style>
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,172 @@
<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="jobNo">
          <el-input
            v-model="form.jobNo"
            placeholder="请输入"
            clearable
            maxlength="20"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="员工类型" prop="staffType">
          <el-select
            v-model="form.staffType"
            placeholder="请选择"
            clearable
            style="width: 100%"
          >
            <el-option label="正式员工" value="official" />
            <el-option label="试用员工" value="probation" />
            <el-option label="实习生" value="intern" />
            <el-option label="兼职" value="part_time" />
            <el-option label="劳务/外包" value="outsourcing" />
          </el-select>
        </el-form-item>
      </el-col>
      <el-col :span="5">
        <el-form-item label="入职日期" prop="entryDate">
          <el-date-picker
            v-model="form.entryDate"
            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="probationPeriod">
          <el-input-number
            v-model="form.probationPeriod"
            :min="0"
            :max="24"
            :precision="0"
            :step="1"
            style="width: 100%"
          />
        </el-form-item>
      </el-col>
      <el-col :span="4">
        <el-form-item label="转正日期" prop="regularDate">
          <el-date-picker
            v-model="form.regularDate"
            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="directLeader">
          <el-input
            v-model="form.directLeader"
            placeholder="请输入"
            clearable
            maxlength="50"
            show-word-limit
          />
        </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-row>
    <el-row :gutter="24">
      <el-col :span="5">
        <el-form-item label="职级" prop="jobLevel">
          <el-input
            v-model="form.jobLevel"
            placeholder="请输入"
            clearable
            maxlength="10"
            show-word-limit
          />
        </el-form-item>
      </el-col>
      <el-col :span="4">
        <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,373 +1,322 @@
<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";
  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" }],
      sysDeptId: [{ required: true, message: "请选择", trigger: "change" }],
    },
    postOptions: [], // å²—位选项
    deptOptions: [], // éƒ¨é—¨é€‰é¡¹
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("add");
const id = ref(0);
const formRef = ref(null);
const dialogTitle = () =>
  operationType.value === "add" ? "新增入职" : "编辑人员";
const createEmptyEducation = () => ({
  degree: "",
  school: "",
  admissionDate: "",
  graduationDate: "",
  major: "",
  academicDegree: "",
});
const createEmptyWork = () => ({
  company: "",
  department: "",
  position: "",
  startDate: "",
  endDate: "",
  description: "",
});
const createEmptyEmergency = () => ({
  name: "",
  relation: "",
  phone: "",
  address: "",
});
const createDefaultForm = () => ({
  id: undefined,
  // åŸºæœ¬ä¿¡æ¯
  staffNo: "",
  staffName: "",
  aliasName: "",
  phone: "",
  sex: "",
  birthDate: "",
  age: undefined,
  nativePlace: "",
  nation: "",
  maritalStatus: "",
  politicalStatus: "",
  firstWorkDate: "",
  workingYears: undefined,
  idCardNo: "",
  hukouType: "",
  email: "",
  currentAddress: "",
  // åœ¨èŒä¿¡æ¯
  jobNo: "",
  staffType: "",
  entryDate: "",
  probationPeriod: undefined,
  regularDate: "",
  sysDeptId: undefined,
  directLeader: "",
  sysPostId: undefined,
  jobLevel: "",
  basicSalary: undefined,
  // é“¶è¡Œå¡ä¿¡æ¯
  bankName: "",
  bankCardNo: "",
  // æ•™è‚²ç»åކ
  educationList: [createEmptyEducation()],
  // å·¥ä½œç»åކ
  workExperienceList: [createEmptyWork()],
  // ç´§æ€¥è”系人
  emergencyContacts: [createEmptyEmergency()],
  emergencyContact: "",
  emergencyContactPhone: "",
  // è§’色
  roleIds: [],
  // ææ–™é™„件(仅前端展示)
  attachments: [],
});
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" },
    ],
    jobNo: [{ required: true, message: "请输入工号", trigger: "blur" }],
    staffType: [
      { required: true, message: "请选择员工类型", trigger: "change" },
    ],
    entryDate: [
      { required: true, message: "请选择入职日期", trigger: "change" },
    ],
    sysDeptId: [
      { required: true, message: "请选择部门", trigger: "change" },
    ],
    roleIds: [
      {
        required: true,
        type: "array",
        min: 1,
        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 { form, rules, postOptions, deptOptions } = toRefs(data);
};
  // æ‰“开弹框
  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 };
        if (form.value.sysPostId === 0) {
          form.value.sysPostId = undefined;
        }
        if (form.value.sysDeptId === 0) {
          form.value.sysDeptId = undefined;
        }
        // ç¼–辑时也计算一次合同年限
        calculateContractTerm();
      });
    } else {
      form.value.id = "";
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;
    }
  };
  onMounted(() => {
    fetchPostOptions();
    fetchDeptOptions();
    if (dept.children && dept.children.length) {
      dept.children = filterDisabledDept(dept.children);
    }
    return true;
  });
}
  const fetchPostOptions = () => {
    findPostOptions().then(res => {
      postOptions.value = res.data;
    });
  };
const syncEmergencyToLegacyField = () => {
  const first = form.value.emergencyContacts?.[0];
  form.value.emergencyContact = first?.name || "";
  form.value.emergencyContactPhone = first?.phone || "";
};
  // æŸ¥è¯¢éƒ¨é—¨åˆ—表
  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;
const openDialog = (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  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.educationList) || !form.value.educationList.length) {
        form.value.educationList = [createEmptyEducation()];
      }
      if (dept.children && dept.children.length) {
        dept.children = filterDisabledDept(dept.children);
      if (
        !Array.isArray(form.value.workExperienceList) ||
        !form.value.workExperienceList.length
      ) {
        form.value.workExperienceList = [createEmptyWork()];
      }
      return true;
      if (
        !Array.isArray(form.value.emergencyContacts) ||
        !form.value.emergencyContacts.length
      ) {
        form.value.emergencyContacts = [createEmptyEmergency()];
      }
      if (form.value.sysPostId === 0) {
        form.value.sysPostId = undefined;
      }
      if (form.value.sysDeptId === 0) {
        form.value.sysDeptId = undefined;
      }
    });
  }
};
  // æäº¤äº§å“è¡¨å•
  const submitForm = () => {
    if (!form.value.sysPostId) {
      form.value.sysPostId = undefined;
    }
    if (!form.value.sysDeptId) {
      form.value.sysDeptId = undefined;
    }
    proxy.$refs.formRef.validate(valid => {
      if (valid) {
        if (operationType.value === "add") {
          createStaffOnJob(form.value).then(res => {
            proxy.$modal.msgSuccess("提交成功");
            closeDia();
          });
        } else {
          updateStaffOnJob(id.value, form.value).then(res => {
            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);
onMounted(() => {
  fetchPostOptions();
  fetchDeptOptions();
});
      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);
const submitForm = () => {
  if (!form.value.sysPostId) {
    form.value.sysPostId = undefined;
  }
  if (!form.value.sysDeptId) {
    form.value.sysDeptId = undefined;
  }
  syncEmergencyToLegacyField();
  formRef.value?.validate((valid) => {
    if (valid) {
      if (operationType.value === "add") {
        createStaffOnJob(form.value).then(() => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        });
      } else {
        form.value.contractTerm = 0;
        updateStaffOnJob(id.value, form.value).then(() => {
          proxy.$modal.msgSuccess("提交成功");
          closeDia();
        });
      }
    } else {
      form.value.contractTerm = 0;
    }
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
    dialogFormVisible.value = false;
    emit("close");
  };
  defineExpose({
    openDialog,
  });
};
const closeDia = () => {
  formRef.value?.resetFields();
  dialogFormVisible.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;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/personnelManagement/monthlyStatistics/components/formDia.vue
@@ -1,318 +1,565 @@
<template>
  <el-dialog v-model="dialogVisible"
             :title="title"
             width="700px"
             :close-on-click-modal="false">
    <el-form ref="formRef"
             :model="form"
             :rules="rules"
             label-width="140px"
             label-position="top">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="统计月份"
                        prop="payDate">
            <el-date-picker v-model="form.payDate"
                            type="month"
                            value-format="YYYY-MM"
                            format="YYYY-MM"
                            placeholder="请选择月份"
                            style="width: 100%"
                            :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="姓名"
                        prop="staffId">
            <el-select v-model="form.staffId"
                       placeholder="请选择员工"
                       style="width: 100%"
                       :disabled="operationType === 'view'">
              <el-option v-for="item in userList"
                         :key="item.id"
                         :label="item.staffName"
                         :value="item.id" />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="基本工资"
                        prop="basicSalary">
            <el-input v-model="form.basicSalary"
                      type="number"
                      placeholder="请输入基本工资"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="计件工资"
                        prop="pieceworkSalary">
            <el-input v-model="form.pieceworkSalary"
                      type="number"
                      placeholder="请输入计件工资"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="计时工资"
                        prop="hourlySalary">
            <el-input v-model="form.hourlySalary"
                      type="number"
                      placeholder="请输入计时工资"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="其他收入"
                        prop="otherIncome">
            <el-input v-model="form.otherIncome"
                      type="number"
                      placeholder="请输入其他收入"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="社保个人"
                        prop="socialSecurityIndividuals">
            <el-input v-model="form.socialSecurityIndividuals"
                      type="number"
                      placeholder="请输入社保个人"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="公积金个人"
                        prop="providentFundIndividuals">
            <el-input v-model="form.providentFundIndividuals"
                      type="number"
                      placeholder="请输入公积金个人"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item label="个人所得税"
                        prop="personalIncomeTax">
            <el-input v-model="form.personalIncomeTax"
                      type="number"
                      placeholder="请输入个人所得税"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="其他扣款"
                        prop="otherDeductions">
            <el-input v-model="form.otherDeductions"
                      type="number"
                      placeholder="请输入其他扣款"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </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"
                      placeholder="请输入备注"
                      :rows="3"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  <el-dialog
    v-model="dialogVisible"
    :title="operationType === 'add' ? '新建工资表' : '编辑工资表'"
    width="90%"
    :close-on-click-modal="false"
    destroy-on-close
    @close="closeDia"
  >
    <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="title">
                <el-input
                  v-model="form.title"
                  placeholder="请输入"
                  clearable
                  maxlength="20"
                  show-word-limit
                />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="选择部门" prop="deptId">
                <el-select
                  v-model="form.deptId"
                  placeholder="请选择"
                  clearable
                  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="payMonth">
                <el-date-picker
                  v-model="form.payMonth"
                  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-form>
      </el-card>
      <!-- æ“ä½œæŒ‰é’® -->
      <div class="toolbar">
        <el-button type="primary" @click="handleGenerate">生成工资表</el-button>
        <el-button @click="handleExport">导出</el-button>
        <el-button @click="handleImport">导入</el-button>
        <el-button @click="handleClear">清空</el-button>
        <el-button @click="openAddPerson">新增人员</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="roleName" 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.pieceworkSalary"
                type="number"
                placeholder="0"
                size="small"
                @input="row.pieceworkSalary = parseNum(row.pieceworkSalary)"
              />
            </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.socialSecurityIndividuals"
                type="number"
                placeholder="0"
                size="small"
                @input="row.socialSecurityIndividuals = parseNum(row.socialSecurityIndividuals)"
              />
            </template>
          </el-table-column>
          <el-table-column label="公积金个人" minWidth="120">
            <template #default="{ row }">
              <el-input
                v-model.number="row.providentFundIndividuals"
                type="number"
                placeholder="0"
                size="small"
                @input="row.providentFundIndividuals = parseNum(row.providentFundIndividuals)"
              />
            </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>
    </div>
    <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>
      <div class="dialog-footer">
        <el-button @click="closeDia">取消</el-button>
        <el-button type="primary" @click="submitForm">确定</el-button>
      </div>
    </template>
    <!-- æ–°å¢žäººå‘˜å¼¹çª— -->
    <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>
  </el-dialog>
</template>
<script setup>
  import { ref, reactive, computed, onMounted } from "vue";
  import { ElMessage } from "element-plus";
  import {
    monthlyStatisticsAdd,
    monthlyStatisticsUpdate,
    staffOnJobList,
  } from "@/api/personnelManagement/monthlyStatistics.js";
import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue";
import { ArrowUp } from "@element-plus/icons-vue";
import { listDept } from "@/api/system/dept.js";
import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js";
import {
  monthlyStatisticsAdd,
  monthlyStatisticsUpdate,
  monthlyStatisticsGet,
} from "@/api/personnelManagement/monthlyStatistics.js";
  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: false,
    },
    operationType: {
      type: String,
      default: "add",
    },
    row: {
      type: Object,
      default: () => ({}),
    },
  });
const emit = defineEmits(["update:modelValue", "close"]);
const props = defineProps({
  modelValue: { type: Boolean, default: false },
  operationType: { type: String, default: "add" },
  row: { type: Object, default: () => ({}) },
});
  const emit = defineEmits(["update:modelValue", "close"]);
const { proxy } = getCurrentInstance();
  const dialogVisible = computed({
    get: () => props.modelValue,
    set: val => emit("update:modelValue", val),
  });
const dialogVisible = computed({
  get: () => props.modelValue,
  set: (val) => emit("update:modelValue", val),
});
  const title = computed(() => {
    if (props.operationType === "add") return "新增薪资台账";
    if (props.operationType === "edit") return "编辑薪资台账";
    return "查看薪资台账";
  });
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 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 },
]);
  const formRef = ref();
  const form = reactive({
    id: "",
    payDate: "",
    staffId: "",
    basicSalary: 0,
    pieceworkSalary: 0,
    hourlySalary: 0,
    otherIncome: 0,
    socialSecurityIndividuals: 0,
    providentFundIndividuals: 0,
    personalIncomeTax: 0,
    otherDeductions: 0,
    payableWages: 0,
    deductibleWages: 0,
    actualWages: 0,
function parseNum(v) {
  if (v === "" || v == null) return 0;
  const n = Number(v);
  return isNaN(n) ? 0 : n;
}
// åŸºç¡€èµ„料表单
const data = reactive({
  form: {
    id: undefined,
    title: "",
    deptId: undefined,
    payMonth: "",
    remark: "",
  },
  rules: {
    title: [{ required: true, message: "请输入工资主题", trigger: "blur" }],
    deptId: [{ required: true, message: "请选择部门", trigger: "change" }],
    payMonth: [{ required: true, message: "请选择工资月份", trigger: "change" }],
  },
});
const { form, rules } = toRefs(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 rules = {
    payDate: [{ required: true, message: "请选择统计月份", trigger: "change" }],
    staffId: [{ required: true, message: "请选择员工", trigger: "change" }],
    basicSalary: [{ required: true, message: "请输入基本工资", trigger: "blur" }],
  };
const loadDeptOptions = () => {
  listDept().then((res) => {
    const tree = res.data ?? [];
    deptOptions.value = flattenDept(tree);
  });
};
  const userList = ref([]);
  const loadUserList = () => {
    // userListNoPage().then(res => {
    //   userList.value = res.data || [];
    // });
    staffOnJobList().then(res => {
      userList.value = res.data || [];
// æž„建 éƒ¨é—¨-人员 æ ‘(用于新增人员弹窗)
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) => {
    // é‡ç½®è¡¨å•
    Object.assign(form, {
      id: "",
      payDate: "",
      staffId: "",
const openDialog = (type, row) => {
  nextTick(() => {
    loadDeptOptions();
    employeeList.value = [];
    Object.assign(form.value, {
      id: undefined,
      title: "",
      deptId: undefined,
      payMonth: "",
      remark: "",
    });
    if (type === "edit" && row?.id) {
      monthlyStatisticsGet(row.id).then((res) => {
        const d = res.data || {};
        form.value.id = d.id;
        form.value.title = d.title ?? d.payDateStr ?? "";
        form.value.deptId = d.deptId;
        form.value.payMonth = d.payMonth ?? d.payDate ?? d.payDateStr ?? "";
        form.value.remark = d.remark ?? "";
        employeeList.value = (d.detailList || d.employeeList || []).map((e) => ({
          ...e,
          basicSalary: parseNum(e.basicSalary),
          pieceworkSalary: parseNum(e.pieceworkSalary),
          hourlySalary: parseNum(e.hourlySalary),
          otherIncome: parseNum(e.otherIncome),
          socialSecurityIndividuals: parseNum(e.socialSecurityIndividuals),
          providentFundIndividuals: parseNum(e.providentFundIndividuals),
        }));
      });
    }
  });
};
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({
      staffId: id,
      id: id,
      staffName: node.label,
      roleName: node.roleName ?? node.role ?? "",
      deptName: node.deptName ?? "",
      basicSalary: 0,
      pieceworkSalary: 0,
      hourlySalary: 0,
      otherIncome: 0,
      socialSecurityIndividuals: 0,
      providentFundIndividuals: 0,
      personalIncomeTax: 0,
      otherDeductions: 0,
      payableWages: 0,
      deductibleWages: 0,
      actualWages: 0,
      remark: "",
    });
  });
  addPersonVisible.value = false;
};
    if (type === "add") {
      dialogVisible.value = true;
    } else if (type === "edit" || type === "view") {
      if (row && row.id) {
        Object.assign(form, row);
        dialogVisible.value = true;
      }
const removeEmployee = (row) => {
  employeeList.value = employeeList.value.filter(
    (e) => (e.staffId || e.id) !== (row.staffId || 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.staffId || e.id));
  employeeList.value = employeeList.value.filter(
    (e) => !ids.has(e.staffId || e.id)
  );
};
const handleGenerate = () => {
  proxy.$modal.msgInfo("生成工资表功能需对接后端");
};
const handleExport = () => {
  proxy.$modal.msgInfo("导出功能需对接后端");
};
const handleImport = () => {
  proxy.$modal.msgInfo("导入功能需对接后端");
};
const handleClear = () => {
  proxy.$modal.confirm("确定清空当前员工列表吗?").then(() => {
    employeeList.value = [];
  }).catch(() => {});
};
const handleTaxForm = () => {
  taxDialogVisible.value = true;
};
const submitForm = () => {
  formRef.value?.validate((valid) => {
    if (!valid) return;
    const payload = {
      ...form.value,
      detailList: employeeList.value.map((e) => ({
        staffId: e.staffId ?? e.id,
        staffName: e.staffName,
        basicSalary: parseNum(e.basicSalary),
        pieceworkSalary: parseNum(e.pieceworkSalary),
        hourlySalary: parseNum(e.hourlySalary),
        otherIncome: parseNum(e.otherIncome),
        socialSecurityIndividuals: parseNum(e.socialSecurityIndividuals),
        providentFundIndividuals: parseNum(e.providentFundIndividuals),
      })),
    };
    if (props.operationType === "add") {
      monthlyStatisticsAdd(payload).then(() => {
        proxy.$modal.msgSuccess("新增成功");
        closeDia();
      });
    } else {
      monthlyStatisticsUpdate(payload).then(() => {
        proxy.$modal.msgSuccess("修改成功");
        closeDia();
      });
    }
  };
  const submitForm = () => {
    formRef.value.validate(valid => {
      if (valid) {
        form.basicSalary = Number(form.basicSalary);
        form.pieceworkSalary = Number(form.pieceworkSalary);
        form.hourlySalary = Number(form.hourlySalary);
        form.otherIncome = Number(form.otherIncome);
        form.socialSecurityIndividuals = Number(form.socialSecurityIndividuals);
        form.providentFundIndividuals = Number(form.providentFundIndividuals);
        form.personalIncomeTax = Number(form.personalIncomeTax);
        form.otherDeductions = Number(form.otherDeductions);
        // è®¡ç®—应发工资、应扣工资和实发工资
        const payableWages =
          form.basicSalary +
          form.pieceworkSalary +
          form.hourlySalary +
          form.otherIncome;
        const deductibleWages =
          form.socialSecurityIndividuals +
          form.providentFundIndividuals +
          form.personalIncomeTax +
          form.otherDeductions;
        const actualWages = payableWages - deductibleWages;
        const submitData = {
          ...form,
          payableWages,
          deductibleWages,
          actualWages,
        };
        if (props.operationType === "add") {
          monthlyStatisticsAdd(submitData).then(res => {
            if (res.code === 200) {
              ElMessage.success("新增成功");
              dialogVisible.value = false;
              emit("close");
            } else {
              ElMessage.error(res.msg || "新增失败");
            }
          });
        } else if (props.operationType === "edit") {
          monthlyStatisticsUpdate(submitData).then(res => {
            if (res.code === 200) {
              ElMessage.success("更新成功");
              dialogVisible.value = false;
              emit("close");
            } else {
              ElMessage.error(res.msg || "更新失败");
            }
          });
        }
      }
    });
  };
  onMounted(() => {
    loadUserList();
  });
};
  defineExpose({
    openDialog,
  });
const closeDia = () => {
  dialogVisible.value = false;
  emit("close");
};
defineExpose({ openDialog });
</script>
<style scoped>
  .dialog-footer {
    text-align: right;
  }
</style>
.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;
}
</style>
src/views/personnelManagement/monthlyStatistics/index.vue
@@ -2,302 +2,255 @@
  <div class="app-container">
    <div class="search_form">
      <div>
        <span class="search_title">姓名:</span>
        <el-input v-model="searchForm.staffName"
                  style="width: 240px"
                  placeholder="请输入姓名搜索"
                  @change="handleQuery"
                  clearable
                  :prefix-icon="Search" />
        <span class="search_title ml10">月份:</span>
        <el-date-picker v-model="searchForm.payDateStr"
                        type="month"
                        @change="handleQuery"
                        value-format="YYYY-MM"
                        format="YYYY-MM"
                        placeholder="请选择月份"
                        style="width: 240px"
                        clearable />
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">
        <span class="search_title">主题:</span>
        <el-input
          v-model="searchForm.title"
          style="width: 240px"
          placeholder="请输入主题"
          clearable
          @keyup.enter="handleQuery"
        />
        <span class="search_title ml10">单据状态:</span>
        <el-select
          v-model="searchForm.documentStatus"
          placeholder="请选择单据状态"
          clearable
          style="width: 180px"
        >
          <el-option label="草稿" value="draft" />
          <el-option label="已提交" value="submitted" />
          <el-option label="已审核" value="approved" />
        </el-select>
        <span class="search_title ml10">工资月份:</span>
        <el-date-picker
          v-model="searchForm.payMonth"
          type="month"
          value-format="YYYY-MM"
          format="YYYY-MM"
          placeholder="请选择工资月份"
          style="width: 180px"
          clearable
          @change="handleQuery"
        />
        <span class="search_title ml10">审核状态:</span>
        <el-select
          v-model="searchForm.approvalStatus"
          placeholder="请选择审核状态"
          clearable
          style="width: 180px"
        >
          <el-option label="待审核" value="pending" />
          <el-option label="已通过" value="passed" />
          <el-option label="已驳回" value="rejected" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
      </div>
      <div>
        <el-button @click="handleExport"
                   style="margin-right: 10px">导出</el-button>
        <el-button type="primary"
                   @click="openForm('add')">新增台账</el-button>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
        <el-button @click="handleReset">重置</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"
                :total="page.total"></PIMTable>
      <div style="margin-bottom: 10px">
        <el-button type="primary" @click="openForm('add')">新建工资表</el-button>
        <el-button @click="handleDelete">删除</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="formDia"
              @close="handleQuery"></form-dia>
    <form-dia
      v-model="dialogVisible"
      :operation-type="operationType"
      :row="currentRow"
      ref="formDiaRef"
      @close="handleQuery"
    />
  </div>
</template>
<script setup>
  import { Search } from "@element-plus/icons-vue";
  import {
    onMounted,
    ref,
    reactive,
    toRefs,
    getCurrentInstance,
    nextTick,
  } from "vue";
  import { ElMessageBox } from "element-plus";
  import dayjs from "dayjs";
  import FormDia from "./components/formDia.vue";
  import {
    monthlyStatisticsListPage,
    monthlyStatisticsDelete,
  } from "@/api/personnelManagement/monthlyStatistics.js";
import {
  onMounted,
  ref,
  reactive,
  toRefs,
  getCurrentInstance,
  nextTick,
} from "vue";
import { ElMessageBox } from "element-plus";
import FormDia from "./components/formDia.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import {
  monthlyStatisticsListPage,
  monthlyStatisticsDelete,
} from "@/api/personnelManagement/monthlyStatistics.js";
  const data = reactive({
    searchForm: {
      staffName: "",
      payDateStr: "",
    },
  });
const data = reactive({
  searchForm: {
    title: "",
    documentStatus: "",
    payMonth: "",
    approvalStatus: "",
  },
});
const { searchForm } = toRefs(data);
  const { searchForm } = toRefs(data);
const tableColumn = ref([
  { label: "工资主题", prop: "title", minWidth: 140 },
  { label: "工资月份", prop: "payMonth", width: 120 },
  { label: "单据状态", prop: "documentStatusName", width: 100 },
  { label: "审核状态", prop: "approvalStatusName", width: 100 },
  { label: "工资总额", prop: "totalAmount", width: 120 },
  { label: "支付银行", prop: "paymentBank", width: 120 },
  { label: "发放时间", prop: "issueTime", width: 160 },
  { label: "审批人员", prop: "approver", width: 100 },
  { label: "备注", prop: "remark", minWidth: 120 },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 120,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => openForm("edit", row),
      },
    ],
  },
]);
  const tableColumn = ref([
    {
      label: "员工姓名",
      prop: "staffName",
    },
    {
      label: "部门",
      prop: "deptName",
      width: 140,
    },
    {
      label: "月份",
      prop: "payDate",
    },
    {
      label: "基本工资",
      prop: "basicSalary",
    },
    {
      label: "计件工资",
      prop: "pieceworkSalary",
    },
    {
      label: "计时工资",
      prop: "hourlySalary",
    },
    {
      label: "其他收入",
      prop: "otherIncome",
    },
    {
      label: "社保个人",
      prop: "socialSecurityIndividuals",
    },
    {
      label: "公积金个人",
      prop: "providentFundIndividuals",
      width: 140,
    },
    {
      label: "工资个税",
      prop: "personalIncomeTax",
    },
    {
      label: "其他支出",
      prop: "otherDeductions",
    },
    {
      label: "应发工资",
      prop: "payableWages",
    },
    {
      label: "应扣工资",
      prop: "deductibleWages",
    },
    {
      label: "实发工资",
      prop: "actualWages",
    },
    {
      label: "备注",
      prop: "remark",
      width: 150,
    },
    {
      dataType: "action",
      label: "操作",
      align: "center",
      fixed: "right",
      width: 220,
      operation: [
        {
          name: "编辑",
          type: "text",
          clickFun: row => {
            openForm("edit", row);
          },
        },
        // {
        //   name: "查看",
        //   type: "text",
        //   clickFun: row => {
        //     openForm("view", 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 tableData = ref([]);
  const selectedRows = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
    total: 0,
  });
const handleQuery = () => {
  page.current = 1;
  getList();
};
  const formDia = ref();
  const dialogVisible = ref(false);
  const operationType = ref("add");
  const currentRow = ref({});
  const { proxy } = getCurrentInstance();
const handleReset = () => {
  searchForm.value.title = "";
  searchForm.value.documentStatus = "";
  searchForm.value.payMonth = "";
  searchForm.value.approvalStatus = "";
  page.current = 1;
  getList();
};
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
const pagination = (obj) => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
};
  const pagination = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    monthlyStatisticsListPage({ ...page, ...searchForm.value })
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // æ‰“开弹框
  const openForm = (type, row) => {
    operationType.value = type;
    currentRow.value = row || {};
    dialogVisible.value = true;
    nextTick(() => {
      formDia.value?.openDialog(type, row);
const getList = () => {
  tableLoading.value = true;
  monthlyStatisticsListPage({
    ...searchForm.value,
    current: page.current,
    size: page.size,
  })
    .then((res) => {
      tableLoading.value = false;
      const records = res.data?.records ?? [];
      // å…¼å®¹åŽç«¯å­—段:若接口仍返回台账结构,可在此做映射
      tableData.value = records.map((item) => ({
        ...item,
        title: item.title ?? item.payDateStr ?? "-",
        payMonth: item.payMonth ?? item.payDateStr ?? item.payDate ?? "-",
        documentStatusName: item.documentStatusName ?? "-",
        approvalStatusName: item.approvalStatusName ?? "-",
        totalAmount: item.totalAmount ?? item.actualWages ?? "-",
        paymentBank: item.paymentBank ?? "-",
        issueTime: item.issueTime ?? item.createTime ?? "-",
        approver: item.approver ?? "-",
      }));
      page.total = res.data?.total ?? 0;
    })
    .catch(() => {
      tableLoading.value = false;
    });
  };
};
  // åˆ é™¤
  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(() => {
        monthlyStatisticsDelete(ids).then(res => {
          proxy.$modal.msgSuccess("删除成功");
          getList();
        });
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
  // å¯¼å‡º
  const handleExport = () => {
    ElMessageBox.confirm("是否确认导出人员薪资台账?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        proxy.download(
          "/compensationPerformance/export",
          { ...searchForm.value, ...page },
          "人员薪资台账.xlsx"
        );
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
  onMounted(() => {
    getList();
const openForm = (type, row) => {
  operationType.value = type;
  currentRow.value = row || {};
  dialogVisible.value = true;
  nextTick(() => {
    formDiaRef.value?.openDialog(type, row);
  });
};
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(() => {
      monthlyStatisticsDelete(ids).then(() => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
    })
    .catch(() => {});
};
const handleExport = () => {
  proxy.download(
    "/compensationPerformance/export",
    { ...searchForm.value, current: page.current, size: page.size },
    "工资表.xlsx"
  );
};
onMounted(() => {
  getList();
});
</script>
<style scoped>
  .search_form {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    flex-wrap: wrap;
    gap: 10px;
  }
  .search_title {
    font-weight: 500;
    margin-right: 5px;
  }
  .ml10 {
    margin-left: 10px;
  }
  .table_list {
    margin-top: 20px;
  }
  .dialog-footer {
    text-align: right;
  }
</style>
.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,369 @@
<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">
                  <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 />
              </el-form-item>
              <el-form-item label="备注:" prop="remark">
                <el-input
                  v-model="form.remark"
                  type="textarea"
                  :rows="2"
                  placeholder="请输入"
                  clearable
                />
              </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 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="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%"
                      >
                        <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="checkbox-group-inline">
                        <el-checkbox v-model="item.baseOnSalary">根据基本工资缴纳</el-checkbox>
                        <el-checkbox v-model="item.useBasicSalary">调用基本工资</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"
                        />
                        <span class="ratio-unit">(%)</span>
                        <span class="ratio-plus">+</span>
                        <el-input
                          v-model="item.personalFixed"
                          placeholder="请输入"
                          clearable
                          style="width: 100px"
                          type="number"
                        />
                      </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 } from "vue";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { ArrowUp } from "@element-plus/icons-vue";
import { listDept } from "@/api/system/dept.js";
import {
  socialSecurityInfo,
  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 dialogTitle = () =>
  operationType.value === "add" ? "新增方案" : "编辑方案";
// ä¿é™©ç±»åž‹é€‰é¡¹ï¼ˆå¯æŒ‰å­—典替换)
const insuranceTypeOptions = [
  { label: "养老保险", value: "pension" },
  { label: "医疗保险", value: "medical" },
  { label: "失业保险", value: "unemployment" },
  { label: "工伤保险", value: "work_injury" },
  { label: "生育保险", value: "maternity" },
];
const defaultBenefit = () => ({
  _key: Math.random().toString(36).slice(2),
  insuranceType: "",
  baseOnSalary: false,
  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 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" && row?.id) {
    socialSecurityInfo(row.id).then((res) => {
      const d = res.data || {};
      form.value.id = d.id;
      form.value.title = d.title;
      form.value.remark = d.remark ?? "";
      form.value.deptIds = d.deptIds ?? [];
      form.value.insuranceBenefits =
        (d.insuranceBenefits && d.insuranceBenefits.length)
          ? d.insuranceBenefits.map((b) => ({
              ...b,
              _key: b._key || Math.random().toString(36).slice(2),
            }))
          : [defaultBenefit()];
    });
  }
};
const submitForm = () => {
  formRef.value?.validate((valid) => {
    if (!valid) return;
    const submitData = {
      ...form.value,
      insuranceBenefits: form.value.insuranceBenefits.map(
        ({ _key, ...rest }) => rest
      ),
    };
    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;
}
.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,127 @@
<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">
        <el-button type="primary" @click="openForm('add')">新增方案</el-button>
      </div>
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :tableLoading="tableLoading"
        @pagination="pagination"
        :total="page.total"
      />
    </div>
    <form-dia ref="formDiaRef" @close="handleQuery" />
  </div>
</template>
<script setup>
import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import FormDia from "./components/formDia.vue";
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { socialSecurityListPage } from "@/api/personnelManagement/socialSecuritySet.js";
const data = reactive({
  searchForm: {
    title: "",
  },
});
const { searchForm } = toRefs(data);
const tableColumn = ref([
  { label: "主题", prop: "title", minWidth: 120 },
  { label: "保险类型", prop: "insuranceTypeName", width: 120 },
  { label: "使用范围", prop: "scopeName", width: 120 },
  { label: "备注", prop: "remark", minWidth: 120 },
  { label: "创建时间", prop: "createTime", width: 160 },
  { label: "创建人", prop: "createBy", width: 100 },
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: "right",
    width: 120,
    operation: [
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => openForm("edit", row),
      },
    ],
  },
]);
const tableData = ref([]);
const tableLoading = ref(false);
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 openForm = (type, row) => {
  nextTick(() => {
    formDiaRef.value?.openDialog(type, row);
  });
};
onMounted(() => {
  getList();
});
</script>
<style scoped></style>
src/views/projectManagement/roles/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,295 @@
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px">
      <el-form-item label="角色名称" prop="roleName">
        <el-input
            v-model="queryParams.name"
            placeholder="请输入角色名称"
            clearable
            style="width: 240px"
            @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="状态" prop="status">
        <el-select
            v-model="queryParams.status"
            placeholder="角色状态"
            clearable
            style="width: 240px"
        >
          <el-option label="启用" value="0" />
          <el-option label="禁用" value="1" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
            type="primary"
            plain
            icon="Plus"
            @click="handleAdd"
            v-hasPermi="['system:role:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
            type="success"
            plain
            icon="Edit"
            :disabled="single"
            @click="handleUpdate"
            v-hasPermi="['system:role:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
            type="danger"
            plain
            icon="Delete"
            :disabled="multiple"
            @click="handleDelete"
            v-hasPermi="['system:role:remove']"
        >删除</el-button>
      </el-col>
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <!-- è¡¨æ ¼æ•°æ® -->
    <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="角色编号" prop="no" />
      <el-table-column label="角色名称" prop="name" :show-overflow-tooltip="true" />
      <el-table-column label="状态" align="center" width="100">
        <template #default="scope">
          <el-switch
              v-model="scope.row.status"
              :active-value="0"
              :inactive-value="1"
              @change="handleStatusChange(scope.row)"
          ></el-switch>
        </template>
      </el-table-column>
      <el-table-column label="创建时间" align="center" prop="createTime">
        <template #default="scope">
          <span>{{ parseTime(scope.row.createTime) }}</span>
        </template>
      </el-table-column>
      <el-table-column fixed="right" label="操作" align="center" width="120">
        <template #default="scope">
          <el-tooltip content="修改" placement="top" v-if="scope.row.roleId !== 1">
            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
          </el-tooltip>
          <el-tooltip content="删除" placement="top" v-if="scope.row.roleId !== 1">
            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']"></el-button>
          </el-tooltip>
        </template>
      </el-table-column>
    </el-table>
    <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
    />
    <!-- æ·»åŠ æˆ–ä¿®æ”¹è§’è‰²é…ç½®å¯¹è¯æ¡† -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="roleRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="角色编号" prop="no">
          <el-input
              v-model="form.no"
              style="max-width: 600px"
              :placeholder="form.isDefaultNo ? '使用系统编号' : '请输入角色编号'"
              :disabled="form.isDefaultNo"
          >
            <template #append>
              <el-checkbox v-model="form.isDefaultNo" size="large" />
            </template>
          </el-input>
        </el-form-item>
        <el-form-item label="角色名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入角色名称" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup name="Role">
import {onMounted} from "vue";
import {createRole, deleteRoles, findRoleListPage, updateRole} from "@/api/projectManagement/role.js";
const { proxy } = getCurrentInstance()
const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
const roleList = ref([])
const open = ref(false)
const loading = ref(true)
const showSearch = ref(true)
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref("")
const editingId = ref(undefined)
const form = reactive({
  isDefaultNo: true,
  no: "",
  name: "",
});
const rules = computed(() => ({
  no: [{ required: !form.isDefaultNo, message: "角色编号不能为空", trigger: "blur" }],
  name: [{ required: true, message: "角色名称不能为空", trigger: "blur" }],
}));
const data = reactive({
  queryParams: {
    current: 1,
    size: 10,
    name: undefined,
    status: undefined
  },
})
const { queryParams } = toRefs(data)
/** æŸ¥è¯¢è§’色列表 */
function getList() {
  loading.value = true
  findRoleListPage(queryParams.value).then(res => {
    roleList.value = res.data.records
    total.value = res.data.total
    loading.value = false
  })
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
  queryParams.value.current = 1
  getList()
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  proxy.resetForm("queryRef")
  queryParams.value.name = undefined
  queryParams.value.status = undefined
  handleQuery()
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  let roleIds = []
  if (row.id) {
    roleIds = [row.id]
  } else {
    roleIds = ids.value
  }
  proxy.$modal.confirm('确定要删除该数据吗?').then(function () {
    return deleteRoles(roleIds)
  }).then(() => {
    getList()
    proxy.$modal.msgSuccess("删除成功")
  }).catch(() => {})
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
  ids.value = selection.map(item => item.id)
  single.value = selection.length != 1
  multiple.value = !selection.length
}
/** è§’色状态修改 */
function handleStatusChange(row) {
  let text = row.status === 0 ? "启用" : "停用"
  proxy.$modal.confirm('确认要"' + text + '""' + row.name + '"角色吗?').then(function () {
    return updateRole(row)
  }).then(() => {
    proxy.$modal.msgSuccess(text + "成功")
  }).catch(function () {
    row.status = row.status === 0 ? 1 : 0
  })
}
/** é‡ç½®æ–°å¢žçš„表单以及其他数据  */
function reset() {
  // é‡ç½®è¡¨å•数据
  Object.assign(form, {
    isDefaultNo: true,
    no: "",
    name: "",
  })
  editingId.value = undefined
}
/** æ·»åŠ è§’è‰² */
function handleAdd() {
  reset()
  open.value = true
  title.value = "添加角色"
}
/** ä¿®æ”¹è§’色 */
function handleUpdate(row) {
  reset()
  editingId.value = row.id
  form.no = row.no
  form.name = row.name
  title.value = "修改角色"
  open.value = true
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["roleRef"].validate(valid => {
    if (valid) {
      if (editingId.value) {
        const data = {
          id: editingId.value,
          ...form
        }
        updateRole(data).then(response => {
          proxy.$modal.msgSuccess("修改成功")
        }).finally(() => {
          proxy.$refs["roleRef"].resetFields()
          open.value = false
          getList()
        })
      } else {
        createRole(form).then(response => {
          proxy.$modal.msgSuccess("新增成功")
        }).finally(() => {
          proxy.$refs["roleRef"].resetFields()
          open.value = false
          getList()
        })
      }
    }
  })
}
/** å–消按钮 */
function cancel() {
  open.value = false
  reset()
}
onMounted(() => {
  getList();
});
</script>
src/views/safeProduction/hazardousMaterialsControl/index.vue
@@ -653,15 +653,7 @@
  // é€‰æ‹©å˜åŒ–处理
  const handleSelectionChange = selection => {
    // ä¸»è¡¨æ ¼ä¹Ÿåªä¿ç•™æœ€åŽä¸€ä¸ªé€‰ä¸­çš„项
    if (selection.length > 1) {
      const lastSelected = selection[selection.length - 1];
      selectedIds.value = [lastSelected.id];
    } else if (selection.length === 1) {
      selectedIds.value = [selection[0].id];
    } else {
      selectedIds.value = [];
    }
    selectedIds.value = selection.map(item => item.id);
  };
  // æ‰“开表单
src/views/salesManagement/deliveryLedger/index.vue
@@ -34,6 +34,8 @@
        <el-table-column label="销售订单" prop="salesContractNo" show-overflow-tooltip />
        <el-table-column label="发货订单号" prop="shippingNo" show-overflow-tooltip />
        <el-table-column label="客户名称" prop="customerName" show-overflow-tooltip />
        <el-table-column label="产品名称" prop="productName" show-overflow-tooltip />
        <el-table-column label="规格型号" prop="specificationModel" show-overflow-tooltip />
        <el-table-column label="发货时间" prop="shippingDate" show-overflow-tooltip />
        <el-table-column label="发货车牌号" prop="shippingCarNumber" show-overflow-tooltip />
        <el-table-column label="快递公司" prop="expressCompany" show-overflow-tooltip />
@@ -180,6 +182,8 @@
          <el-descriptions-item label="销售订单">{{ detailRow.salesContractNo || '--' }}</el-descriptions-item>
          <el-descriptions-item label="发货订单号">{{ detailRow.shippingNo || '--' }}</el-descriptions-item>
          <el-descriptions-item label="客户名称">{{ detailRow.customerName || '--' }}</el-descriptions-item>
          <el-descriptions-item label="产品名称">{{ detailRow.productName || '--' }}</el-descriptions-item>
          <el-descriptions-item label="规格型号">{{ detailRow.specificationModel || '--' }}</el-descriptions-item>
          <el-descriptions-item label="发货类型">{{ detailRow.type || '--' }}</el-descriptions-item>
          <el-descriptions-item label="发货日期">{{ detailRow.shippingDate || '--' }}</el-descriptions-item>
          <el-descriptions-item label="审核状态">{{ getApprovalStatusText(detailRow.status) }}</el-descriptions-item>
@@ -284,6 +288,8 @@
    id: null,
    salesContractNo: "",
    customerName: "",
    specificationModel: "",
    productName: "",
    type: "货车", // è´§è½¦, å¿«é€’
    shippingDate: "",
    shippingCarNumber: "",