Merge remote-tracking branch 'origin/dev_New' into dev_中兴实强
# Conflicts:
# multiple/assets/favicon/favicon.ico
# multiple/assets/screen/PCDZView.png
# multiple/assets/screen/ZXSQView.png
# multiple/config.json
| | |
| | | />
|
| | | <link rel="icon" href="/favicon.ico" />
|
| | | <title>%VITE_APP_TITLE%</title>
|
| | | <!-- é«å¾·å°å¾API -->
|
| | | <script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
|
| | | <!-- é«å¾·å°å¾æç´¢æä»¶ -->
|
| | | <script type="text/javascript" src="https://webapi.amap.com/loca?v=2.0.0&key=6af5d2639adbbabf95eddfbf2bae5739"></script>
|
| | | <!--[if lt IE 11
|
| | | ]><script>
|
| | | window.location.href = "/html/ie.html";
|
| | |
| | | '@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 |
| | |
| | | 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 |
| | |
| | | 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) |
| | |
| | | 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==} |
| | |
| | | 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==} |
| | | |
| | |
| | | |
| | | '@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==} |
| | |
| | | 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'} |
| | |
| | | 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'} |
| | |
| | | |
| | | 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==} |
| | |
| | | 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'} |
| | |
| | | |
| | | 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==} |
| | |
| | | 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'} |
| | |
| | | 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==} |
| | |
| | | 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==} |
| | | |
| | |
| | | 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==} |
| | | |
| | |
| | | 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==} |
| | |
| | | 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'} |
| | |
| | | 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==} |
| | |
| | | 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==} |
| | |
| | | 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==} |
| | |
| | | '@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==} |
| | |
| | | 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'} |
| | |
| | | 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'} |
| | |
| | | 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==} |
| | |
| | | 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 |
| | |
| | | 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: |
| | |
| | | |
| | | atob@2.1.2: {} |
| | | |
| | | autofit.js@3.2.8: {} |
| | | |
| | | available-typed-arrays@1.0.7: |
| | | dependencies: |
| | | possible-typed-array-names: 1.1.0 |
| | |
| | | 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 |
| | |
| | | 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: {} |
| | | |
| | |
| | | dependencies: |
| | | ms: 2.1.3 |
| | | |
| | | decamelize@1.2.0: {} |
| | | |
| | | decode-uri-component@0.2.2: {} |
| | | |
| | | deep-equal@1.1.2: |
| | |
| | | delayed-stream@1.0.0: {} |
| | | |
| | | delegate@3.2.0: {} |
| | | |
| | | dijkstrajs@1.0.3: {} |
| | | |
| | | dom-serializer@0.2.2: |
| | | dependencies: |
| | |
| | | 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: |
| | |
| | | functions-have-names@1.2.3: {} |
| | | |
| | | fuse.js@6.6.2: {} |
| | | |
| | | get-caller-file@2.0.5: {} |
| | | |
| | | get-intrinsic@1.3.0: |
| | | dependencies: |
| | |
| | | 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): |
| | |
| | | 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: {} |
| | | |
| | |
| | | confbox: 0.2.2 |
| | | exsolve: 1.0.5 |
| | | pathe: 2.0.3 |
| | | |
| | | pngjs@5.0.0: {} |
| | | |
| | | posix-character-classes@0.1.1: {} |
| | | |
| | |
| | | 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: {} |
| | | |
| | |
| | | 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: {} |
| | | |
| | |
| | | select@1.1.2: {} |
| | | |
| | | semver@7.7.2: {} |
| | | |
| | | set-blocking@2.0.0: {} |
| | | |
| | | set-function-length@1.2.2: |
| | | dependencies: |
| | |
| | | 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: |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | }) |
| | | } |
| | | |
| | | |
| | | // æ°å¢å®¢æ·è·è¿ |
| | | export function addCustomerFollow(data) { |
| | | return request({ |
| | | url: '/basic/customer-follow/add', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // ä¿®æ¹å®¢æ·è·è¿ |
| | | export function updateCustomerFollow(data) { |
| | | return request({ |
| | | url: '/basic/customer-follow/edit', |
| | | method: 'put', |
| | | data: data, |
| | | }) |
| | | } |
| | | // å é¤å®¢æ·è·è¿ |
| | | export function delCustomerFollow(id) { |
| | | return request({ |
| | | url: '/basic/customer-follow/'+id, |
| | | method: 'delete', |
| | | }) |
| | | } |
| | | |
| | | // å访æé-æ°å¢/æ´æ° |
| | | export function addReturnVisit(data) { |
| | | return request({ |
| | | url: '/basic/customer-follow/return-visit', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | // è·åå访æé详æ
|
| | | export function getReturnVisit(id) { |
| | | return request({ |
| | | url: '/basic/customer-follow/return-visit/' + id, |
| | | method: 'get' |
| | | }) |
| | | } |
| | |
| | | params: query |
| | | }) |
| | | } |
| | | |
| | | // ä¸è½½äº§å导å
¥æ¨¡æ¿ |
| | | export function downloadProductModelImportTemplate() { |
| | | return request({ |
| | | url: '/basic/product/export', |
| | | method: 'get', |
| | | responseType: 'blob' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | // è·åæè®¿è®°å½å表 |
| | | export function getVisitRecords(query) { |
| | | return request({ |
| | | url: '/customerVisits/listPage', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | |
| | | }) |
| | | } |
| | | // å®åå¤ç-éä»¶å é¤ |
| | | export function afterSalesServiceFileDel(ids) { |
| | | export function afterSalesServiceFileDel(id) { |
| | | return request({ |
| | | url: '/afterSalesService/file/del', |
| | | url: `/afterSalesService/file/del/${id}`, |
| | | method: 'delete', |
| | | data: ids, |
| | | }) |
| | | } |
| | | |
| | | // å®åå¤ç-维修记å½å表 |
| | | export function afterSalesServiceRepairListPage(query) { |
| | | return request({ |
| | | url: '/afterSalesService/repair/listPage', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | |
| | | // 临æå®å管ç-å页æ¥è¯¢ |
| | | export function expiryAfterSalesListPage(query) { |
| | | return request({ |
| | | url: '/expiryAfterSales/listPage', |
| | | url: '/afterSalesNearExpiryService/listPage', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | |
| | | // 临æå®å管ç-æ°å¢ |
| | | export function expiryAfterSalesAdd(query) { |
| | | return request({ |
| | | url: '/expiryAfterSales/add', |
| | | url: '/afterSalesNearExpiryService/add', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | |
| | | // 临æå®å管ç-æ´æ° |
| | | export function expiryAfterSalesUpdate(query) { |
| | | return request({ |
| | | url: '/expiryAfterSales/update', |
| | | url: '/afterSalesNearExpiryService/update', |
| | | method: 'post', |
| | | data: query, |
| | | }) |
| | | } |
| | | |
| | | // 临æå®å管ç-å é¤ |
| | | export function expiryAfterSalesDelete(query) { |
| | | export function expiryAfterSalesDelete(ids) { |
| | | return request({ |
| | | url: '/expiryAfterSales/delete', |
| | | url: '/afterSalesNearExpiryService/delete?ids=' + ids, |
| | | method: 'delete', |
| | | data: query, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // æ¥è¯¢ææå®¢æ·ä¿¡æ¯ |
| | | // /basic/customer/list |
| | | export function getAllCustomerList(query) { |
| | | return request({ |
| | | url: '/basic/customer/list', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | |
| | | // æ ¹æ®å®¢æ·æ¥è¯¢éå®è®¢åå· |
| | | // afterSalesService/listSalesLedger |
| | | export function getSalesLedger(query) { |
| | | return request({ |
| | | url: '/afterSalesService/listSalesLedger', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | |
| | | // æ ¹æ®éå®è®¢åå·æ¥è¯¢éå®è®¢å详æ
|
| | | // afterSalesService/count |
| | | export function getSalesLedgerDetail(query) { |
| | | return request({ |
| | | url: '/afterSalesService/count', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // æ¥è¯¢å表 |
| | | // /salesRefundAmountOrder/page |
| | | export const listPage = (params) => { |
| | | return request({ |
| | | url: "/salesRefundAmountOrder/page", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | }; |
| | | |
| | | // æ°å¢ |
| | | // /salesRefundAmountOrder/add |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/salesRefundAmountOrder/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹ |
| | | // /salesRefundAmountOrder/update |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/salesRefundAmountOrder/update", |
| | | method: "put", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ |
| | | // /salesRefundAmountOrder/deleteByIds |
| | | export function del(data) { |
| | | return request({ |
| | | url: "/salesRefundAmountOrder/deleteByIds", |
| | | method: "delete", |
| | | data: data, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // è·åçæ¬¡å表 |
| | | export function getAttendanceRules(query) { |
| | | return request({ |
| | | url: "/personalAttendanceLocationConfig/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ç次 |
| | | export function addAttendanceRule(data) { |
| | | return request({ |
| | | url: "/personalAttendanceLocationConfig/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // æ´æ°ç次 |
| | | export function updateAttendanceRule(data) { |
| | | return request({ |
| | | url: "/attendanceRules/update", |
| | | method: "put", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ç次 |
| | | export function deleteAttendanceRule(ids) { |
| | | return request({ |
| | | url: `/personalAttendanceLocationConfig/del`, |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // è·ååä¸ªçæ¬¡è¯¦æ
|
| | | export function getAttendanceRuleDetail(id) { |
| | | return request({ |
| | | url: `/attendanceRules/detail/${id}`, |
| | | method: "get", |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // é¶è¡ç®¡ç |
| | | export function bankList() { |
| | | return request({ |
| | | url: "/bank/list", |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | export function bankAdd(data) { |
| | | return request({ |
| | | url: "/bank/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function bankUpdate(data) { |
| | | return request({ |
| | | url: "/bank/update", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function bankDelete(ids) { |
| | | return request({ |
| | | url: "/bank/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // çæ¬¡ç¸å
³æ¥å£ |
| | | |
| | | import request from "@/utils/request"; |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-å页æ¥è¯¢ |
| | | export function page(query) { |
| | | return request({ |
| | | url: "/personalShift/page", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-年份å页æ¥è¯¢ |
| | | export function pageYear(query) { |
| | | return request({ |
| | | url: "/personalShift/pageYear", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-æç |
| | | export function add(data) { |
| | | return request({ |
| | | url: "/personalShift/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-æ¶é´é
ç½®-æ¥è¯¢æ¶é´é
ç½®ä¿¡æ¯ |
| | | export function list(query) { |
| | | return request({ |
| | | url: "/shiftTime/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-æ¶é´é
ç½®-æ°å¢ |
| | | export function shiftAdd(data) { |
| | | return request({ |
| | | url: "/shiftTime/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-æ¶é´é
ç½®-ä¿®æ¹ |
| | | export function shiftUpdate(data) { |
| | | return request({ |
| | | url: "/shiftTime/update", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-æ¶é´é
ç½®-å é¤ |
| | | export function shiftRemove(query) { |
| | | return request({ |
| | | url: "/shiftTime/remove", |
| | | method: "delete", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-å¯¼åº |
| | | export function exportFile(query) { |
| | | return request({ |
| | | url: "/personalShift/export", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-å¯¼åº |
| | | export function obtainItemParameterList(query) { |
| | | return request({ |
| | | url: "/laboratoryScope/obtainItemParameterList", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 绩æç®¡ç-çæ¬¡-çæ¬¡ç¶æä¿®æ¹ |
| | | export function update(data) { |
| | | return request({ |
| | | url: "/personalShift/update", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // è·åç¨æ·å表 |
| | | // export function selectUserCondition(query) { |
| | | // return request({ |
| | | // url: "/system/newUser/selectUserCondition", |
| | | // method: "get", |
| | | // params: query, |
| | | // }); |
| | | // } |
| | | export function selectUserCondition() { |
| | | return request({ |
| | | url: '/system/user/userListNoPage', |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // æ¥è¯¢å¨èåå·¥å°è´¦ |
| | | export function staffOnJobListPage(query) { |
| | | return request({ |
| | | url: '/staff/staffOnJob/listPage', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // 人åèªèµå°è´¦å表 |
| | | export function monthlyStatisticsListPage(query) { |
| | | return request({ |
| | | url: "/compensationPerformance/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 人åèªèµå°è´¦è¯¦æ
|
| | | export function monthlyStatisticsGet(id) { |
| | | return request({ |
| | | url: "/monthlyStatistics/get", |
| | | method: "get", |
| | | params: { id }, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢äººåèªèµå°è´¦ |
| | | export function monthlyStatisticsAdd(data) { |
| | | return request({ |
| | | url: "/compensationPerformance/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // ç¼è¾äººåèªèµå°è´¦ |
| | | export function monthlyStatisticsUpdate(data) { |
| | | return request({ |
| | | url: "/compensationPerformance/update", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤äººåèªèµå°è´¦ |
| | | export function monthlyStatisticsDelete(ids) { |
| | | return request({ |
| | | url: "/compensationPerformance/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | // 导åºäººåèªèµå°è´¦ |
| | | export function monthlyStatisticsExport(query) { |
| | | return request({ |
| | | url: "/compensationPerformance/export", |
| | | method: "get", |
| | | params: query, |
| | | responseType: "blob", |
| | | }); |
| | | } |
| | | |
| | | // 人åå表 |
| | | export function staffOnJobList(query) { |
| | | return request({ |
| | | url: "/staff/staffOnJob/list", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request.js"; |
| | | |
| | | export function createPersonalAttendanceRecord(params) { |
| | | return request({ |
| | | url: "/personalAttendanceRecords", |
| | | method: "post", |
| | | data: params, |
| | | }); |
| | | } |
| | | |
| | | export function findPersonalAttendanceRecords(query) { |
| | | return request({ |
| | | url: "/personalAttendanceRecords/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | export function findTodayPersonalAttendanceRecord(query) { |
| | | return request({ |
| | | url: "/personalAttendanceRecords/today", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // 社ä¼ä¿é©è®¾ç½® |
| | | import request from "@/utils/request"; |
| | | |
| | | // å页æ¥è¯¢å表 |
| | | export function socialSecurityListPage(query) { |
| | | return request({ |
| | | url: "/schemeApplicableStaff/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢è¯¦æ
|
| | | export function socialSecurityInfo(id) { |
| | | return request({ |
| | | url: "/schemeApplicableStaff/" + id, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ |
| | | export function socialSecurityAdd(data) { |
| | | return request({ |
| | | url: "/schemeApplicableStaff/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹ |
| | | export function socialSecurityUpdate(data) { |
| | | return request({ |
| | | url: "/schemeApplicableStaff/updateSchemeApplicableStaff", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // å é¤ |
| | | export function socialSecurityDelete(ids) { |
| | | return request({ |
| | | url: "/schemeApplicableStaff/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | |
| | | }) |
| | | } |
| | | |
| | | // æ¥è¯¢åå·¥å
¥èä¿¡æ¯ |
| | | export function getStaffOnJobInfoByUserName(query) { |
| | | return request({ |
| | | url: '/staff/staffOnJob/byUserName', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | |
| | | // æ°å¢åå·¥ |
| | | export function createStaffOnJob(params) { |
| | | return request({ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | // å工工èµä¸»è¡¨ |
| | | export function staffSalaryMainListPage(params) { |
| | | return request({ |
| | | url: "/staffSalaryMain/listPage", |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | export function staffSalaryMainCalculateSalary(ids) { |
| | | return request({ |
| | | url: "/staffSalaryMain/calculateSalary", |
| | | method: "post", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| | | export function staffSalaryMainAdd(data) { |
| | | return request({ |
| | | url: "/staffSalaryMain/add", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function staffSalaryMainUpdate(data) { |
| | | return request({ |
| | | url: "/staffSalaryMain/update", |
| | | method: "post", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export function staffSalaryMainDelete(ids) { |
| | | return request({ |
| | | url: "/staffSalaryMain/delete", |
| | | method: "delete", |
| | | data: ids, |
| | | }); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | // éè´éè´§å |
| | | |
| | | // å页æ¥è¯¢ |
| | | export function findPurchaseReturnOrderListPage(query) { |
| | | return request({ |
| | | url: "/purchaseReturnOrders/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢ |
| | | export function createPurchaseReturnOrder(data) { |
| | | return request({ |
| | | url: "/purchaseReturnOrders/add", |
| | | method: "post", |
| | | data |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | export function listProject(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/listPage', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export function getProject(id) { |
| | | return request({ |
| | | url: `/projectManagement/info/${id}`, |
| | | method: 'post' |
| | | }) |
| | | } |
| | | |
| | | export function addProject(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/save', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export function updateProject(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/save', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export function delProject(ids) { |
| | | return request({ |
| | | url: '/projectManagement/info/remove', |
| | | method: 'delete', |
| | | data: ids |
| | | }) |
| | | } |
| | | |
| | | export function updateStatus(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/updateStatus', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export function submitProject(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/updateStatus', |
| | | method: 'post', |
| | | data: { ...data, reviewStatus: 0 } |
| | | }) |
| | | } |
| | | |
| | | export function auditProject(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/updateStatus', |
| | | method: 'post', |
| | | data: { ...data, reviewStatus: 1 } |
| | | }) |
| | | } |
| | | |
| | | export function reverseAuditProject(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/updateStatus', |
| | | method: 'post', |
| | | data: { ...data, reviewStatus: 0 } |
| | | }) |
| | | } |
| | | |
| | | // é¶æ®µ |
| | | export function saveStage(data) { |
| | | return request({ |
| | | url: '/projectManagement/info/saveStage', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export function listStage(projectId) { |
| | | return request({ |
| | | url: `/projectManagement/info/listStage/${projectId}`, |
| | | method: 'post' |
| | | }) |
| | | } |
| | | |
| | | export function deleteStage(stageId) { |
| | | return request({ |
| | | url: `/projectManagement/info/deleteStage/${stageId}`, |
| | | method: 'post' |
| | | }) |
| | | } |
| | | |
| | | export function listPlan(data) { |
| | | return request({ |
| | | url: '/projectManagement/plan/listPage', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export function addPlan(data) { |
| | | return request({ |
| | | url: '/projectManagement/plan/save', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | export function delPlan(id) { |
| | | return request({ |
| | | url: `/projectManagement/plan/delete/${id}`, |
| | | method: 'post' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | // æ¥è¯¢é¡¹ç®ç±»åå表 |
| | | export function listPlan(data) { |
| | | return request({ |
| | | url: '/projectManagement/plan/listPage', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // ä¿å项ç®ç±»åï¼æ°å¢/ä¿®æ¹ï¼ |
| | | export function savePlan(data) { |
| | | return request({ |
| | | url: '/projectManagement/plan/save', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // å é¤é¡¹ç®ç±»å |
| | | export function deletePlan(id) { |
| | | return request({ |
| | | url: `/projectManagement/plan/delete/${id}`, |
| | | method: 'post' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | |
| | | |
| | | // éå®éè´§-æ¥è¯¢ |
| | | // /returnManagement/listPage |
| | | export function returnManagementList(query) { |
| | | return request({ |
| | | url: "/returnManagement/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // éå®éè´§-æ·»å |
| | | // /returnManagement/add |
| | | export function returnManagementAdd(data) { |
| | | return request({ |
| | | url: "/returnManagement/add", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // éå®éè´§-ä¿®æ¹ |
| | | // /returnManagement/update |
| | | export function returnManagementUpdate(data) { |
| | | return request({ |
| | | url: "/returnManagement/update", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | |
| | | // éå®éè´§-å é¤ |
| | | // /returnManagement/del |
| | | export function returnManagementDel(data) { |
| | | return request({ |
| | | url: "/returnManagement/del", |
| | | method: "delete", |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | // éå®éè´§-æ¥è¯¢ |
| | | // /returnManagement/getById |
| | | export function returnManagementGetById(query) { |
| | | return request({ |
| | | url: "/returnManagement/getById", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // éå®éè´§-æ ¹æ®åºåºåæ¥è¯¢éå®è®¢å以å产åä¿¡æ¯ |
| | | // /returnManagement/getByShippingId |
| | | export function returnManagementGetByShippingId(query) { |
| | | return request({ |
| | | url: "/returnManagement/getByShippingId", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // éè¿å®¢æ·åç§°æ¥è¯¢ |
| | | // /shippingInfo/getByCustomerName |
| | | export function getSalesLedger(query) { |
| | | return request({ |
| | | url: '/shippingInfo/getByCustomerName', |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | |
| | | // å¤ç |
| | | // /returnManagement/handle |
| | | export function returnManagementHandle(data) { |
| | | return request({ |
| | | url: "/returnManagement/handle", |
| | | method: "get", |
| | | params: data, |
| | | }); |
| | | } |
| | |
| | | <slot></slot> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="handleConfirm">确认</el-button> |
| | | <el-button @click="handleCancel">åæ¶</el-button> |
| | | <!-- èªå®ä¹æé®ææ§½ --> |
| | | <slot name="footer"> |
| | | <!-- é»è®¤æé® --> |
| | | <el-button |
| | | v-if="showConfirm" |
| | | type="primary" |
| | | @click="handleConfirm" |
| | | > |
| | | 确认 |
| | | </el-button> |
| | | <el-button @click="handleCancel">åæ¶</el-button> |
| | | </slot> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | |
| | | // 详æ
模å¼ä¸å±ç¤ºâ确认âæé®ï¼å
¶å®ç±»åæ£å¸¸æ¾ç¤º |
| | | const showConfirm = computed(() => props.operationType !== 'detail') |
| | | |
| | | const computedTitle = computed(() => { |
| | | if (typeof props.title === 'function') { |
| | | return props.title(props.operationType) |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" title="æ´½è°è¿åº¦" width="700px" top="10vh" append-to-body destroy-on-close @close="handleClose"> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px"> |
| | | <el-form-item label="项ç®é¶æ®µ" prop="planNodeId"> |
| | | <el-select v-model="form.planNodeId" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input v-model="form.remark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请è¾å
¥" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="éä»¶" prop="attachmentIds"> |
| | | <el-upload |
| | | v-model:file-list="fileList" |
| | | :action="upload.url" |
| | | :headers="upload.headers" |
| | | multiple |
| | | name="files" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | :on-remove="handleRemove" |
| | | > |
| | | <el-button type="primary">ä¸ä¼ æä»¶</el-button> |
| | | </el-upload> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="visible = false">åæ¶</el-button> |
| | | <el-button type="danger" @click="submit">æäº¤</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup name="DiscussProgressDialog"> |
| | | import { computed, reactive, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getToken } from '@/utils/auth' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | projectId: { type: [Number, String], default: undefined }, |
| | | planNodes: { type: Array, default: () => [] }, |
| | | defaultPlanNodeId: { type: [Number, String], default: undefined } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'submitted']) |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: v => emit('update:modelValue', v) |
| | | }) |
| | | |
| | | const upload = reactive({ |
| | | url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload', |
| | | headers: { Authorization: 'Bearer ' + getToken() } |
| | | }) |
| | | |
| | | const formRef = ref() |
| | | const fileList = ref([]) |
| | | const form = ref({ |
| | | planNodeId: undefined, |
| | | remark: '', |
| | | attachmentIds: [] |
| | | }) |
| | | |
| | | const rules = { |
| | | planNodeId: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], |
| | | remark: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }] |
| | | } |
| | | |
| | | const stageOptions = computed(() => { |
| | | const list = Array.isArray(props.planNodes) ? props.planNodes : [] |
| | | const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) |
| | | return sorted |
| | | .map(n => ({ |
| | | label: n.name || n.workContent || n.title || String(n.id ?? ''), |
| | | value: n.id |
| | | })) |
| | | .filter(i => i.value !== undefined && i.value !== null && i.value !== '') |
| | | }) |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | v => { |
| | | if (v) { |
| | | form.value = { planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value, remark: '', attachmentIds: [] } |
| | | fileList.value = [] |
| | | } |
| | | } |
| | | ) |
| | | |
| | | function handleClose() { |
| | | formRef.value?.resetFields?.() |
| | | } |
| | | |
| | | function handleUploadError() { |
| | | ElMessage.error('ä¸ä¼ æä»¶å¤±è´¥') |
| | | } |
| | | |
| | | function handleUploadSuccess(res, file) { |
| | | if (res?.code !== 200) { |
| | | ElMessage.error(res?.msg || 'ä¸ä¼ 失败') |
| | | return |
| | | } |
| | | const attachmentId = res?.data?.id ?? res?.data?.tempId ?? '' |
| | | if (!attachmentId) return |
| | | form.value.attachmentIds.push(attachmentId) |
| | | try { |
| | | file.attachmentId = attachmentId |
| | | } catch (e) {} |
| | | ElMessage.success('ä¸ä¼ æå') |
| | | } |
| | | |
| | | function handleRemove(file) { |
| | | const attachmentId = file?.attachmentId |
| | | if (!attachmentId) return |
| | | form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId) |
| | | } |
| | | |
| | | async function submit() { |
| | | await formRef.value?.validate?.() |
| | | emit('submitted', { |
| | | projectId: props.projectId, |
| | | ...form.value |
| | | }) |
| | | visible.value = false |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" title="è¿åº¦æ±æ¥" width="900px" top="8vh" append-to-body destroy-on-close @close="handleClose"> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px"> |
| | | <el-form-item label="项ç®é¶æ®µ" prop="planNodeId"> |
| | | <el-select v-model="form.planNodeId" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="计åå¼å§æ¶é´" prop="planStartTime"> |
| | | <el-date-picker |
| | | v-model="form.planStartTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="计åå®å·¥æ¶é´" prop="planEndTime"> |
| | | <el-date-picker |
| | | v-model="form.planEndTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="å®é
å¼å·¥æ¥æ" prop="actualStartTime"> |
| | | <el-date-picker |
| | | v-model="form.actualStartTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="æ¬æ¬¡è¿åº¦æ¥æ" prop="reportDate"> |
| | | <el-date-picker |
| | | v-model="form.reportDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="䏿¬¡è¿åº¦(%)" prop="lastProgress"> |
| | | <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="宿è¿åº¦(%)" prop="completionProgress"> |
| | | <div style="display: flex; gap: 8px; width: 100%;"> |
| | | <el-input-number v-model="form.completionProgress" :min="0" :max="100" controls-position="right" style="flex: 1" /> |
| | | <el-button type="danger" @click="markDone">宿</el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="累计è¿åº¦(%)" prop="totalProgress"> |
| | | <el-input-number v-model="form.totalProgress" :min="0" :max="100" controls-position="right" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="å®é
å®å·¥æ¥æ" prop="actualEndTime"> |
| | | <el-date-picker |
| | | v-model="form.actualEndTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="è´è´£äºº" prop="managerName"> |
| | | <el-input v-model="form.managerName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="é¨é¨" prop="departmentName"> |
| | | <el-input v-model="form.departmentName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="16"> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input v-model="form.remark" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-form-item label="éä»¶" prop="attachmentIds"> |
| | | <el-upload |
| | | v-model:file-list="fileList" |
| | | :action="upload.url" |
| | | :headers="upload.headers" |
| | | multiple |
| | | name="files" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | :on-remove="handleRemove" |
| | | > |
| | | <el-button type="primary">ä¸ä¼ æä»¶</el-button> |
| | | </el-upload> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="visible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submit">ç¡®å®</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup name="ProgressReportDialog"> |
| | | import { computed, reactive, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getToken } from '@/utils/auth' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | projectId: { type: [Number, String], default: undefined }, |
| | | projectInfo: { type: Object, default: () => ({}) }, |
| | | planNodes: { type: Array, default: () => [] }, |
| | | defaultPlanNodeId: { type: [Number, String], default: undefined } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'submitted']) |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: v => emit('update:modelValue', v) |
| | | }) |
| | | |
| | | const formRef = ref() |
| | | const fileList = ref([]) |
| | | const upload = reactive({ |
| | | url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload', |
| | | headers: { Authorization: 'Bearer ' + getToken() } |
| | | }) |
| | | |
| | | const form = ref({ |
| | | planNodeId: undefined, |
| | | planStartTime: '', |
| | | planEndTime: '', |
| | | actualStartTime: '', |
| | | actualEndTime: '', |
| | | reportDate: '', |
| | | lastProgress: 0, |
| | | completionProgress: 0, |
| | | totalProgress: 0, |
| | | managerName: '', |
| | | departmentName: '', |
| | | remark: '', |
| | | attachmentIds: [] |
| | | }) |
| | | |
| | | const rules = { |
| | | planNodeId: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], |
| | | planStartTime: [{ required: true, message: 'è¯·éæ©è®¡åå¼å§æ¶é´', trigger: 'change' }], |
| | | planEndTime: [{ required: true, message: 'è¯·éæ©è®¡åå®å·¥æ¶é´', trigger: 'change' }], |
| | | reportDate: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], |
| | | completionProgress: [{ required: true, message: '请è¾å
¥', trigger: 'change' }] |
| | | } |
| | | |
| | | const stageOptions = computed(() => { |
| | | const list = Array.isArray(props.planNodes) ? props.planNodes : [] |
| | | const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) |
| | | return sorted |
| | | .map(n => ({ |
| | | label: n.name || n.workContent || n.title || String(n.id ?? ''), |
| | | value: n.id |
| | | })) |
| | | .filter(i => i.value !== undefined && i.value !== null && i.value !== '') |
| | | }) |
| | | |
| | | function resetFromProject() { |
| | | const info = props.projectInfo || {} |
| | | form.value = { |
| | | planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value, |
| | | planStartTime: info.planStartTime || '', |
| | | planEndTime: info.planEndTime || '', |
| | | actualStartTime: info.actualStartTime || '', |
| | | actualEndTime: info.actualEndTime || '', |
| | | reportDate: '', |
| | | lastProgress: Number(info.lastProgress ?? 0) || 0, |
| | | completionProgress: 0, |
| | | totalProgress: Number(info.totalProgress ?? info.progress ?? 0) || 0, |
| | | managerName: info.managerName || '', |
| | | departmentName: info.departmentName || '', |
| | | remark: '', |
| | | attachmentIds: [] |
| | | } |
| | | fileList.value = [] |
| | | } |
| | | |
| | | watch( |
| | | () => props.modelValue, |
| | | v => { |
| | | if (v) resetFromProject() |
| | | } |
| | | ) |
| | | |
| | | function handleClose() { |
| | | formRef.value?.resetFields?.() |
| | | } |
| | | |
| | | function markDone() { |
| | | form.value.completionProgress = 100 |
| | | form.value.totalProgress = 100 |
| | | if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || '' |
| | | } |
| | | |
| | | function handleUploadError() { |
| | | ElMessage.error('ä¸ä¼ æä»¶å¤±è´¥') |
| | | } |
| | | |
| | | function handleUploadSuccess(res, file) { |
| | | if (res?.code !== 200) { |
| | | ElMessage.error(res?.msg || 'ä¸ä¼ 失败') |
| | | return |
| | | } |
| | | const attachmentId = res?.data?.id ?? res?.data?.tempId ?? '' |
| | | if (!attachmentId) return |
| | | form.value.attachmentIds.push(attachmentId) |
| | | try { |
| | | file.attachmentId = attachmentId |
| | | } catch (e) {} |
| | | ElMessage.success('ä¸ä¼ æå') |
| | | } |
| | | |
| | | function handleRemove(file) { |
| | | const attachmentId = file?.attachmentId |
| | | if (!attachmentId) return |
| | | form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId) |
| | | } |
| | | |
| | | async function submit() { |
| | | await formRef.value?.validate?.() |
| | | emit('submitted', { |
| | | projectId: props.projectId, |
| | | ...form.value |
| | | }) |
| | | visible.value = false |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="search-panel-container"> |
| | | <el-form |
| | | ref="formRef" |
| | | :model="modelValue" |
| | | class="search-form" |
| | | label-width="0" |
| | | > |
| | | <el-row :gutter="10" class="form-row"> |
| | | <!-- 渲æè¡¨å项 --> |
| | | <el-col |
| | | v-for="(item, index) in visibleSchema" |
| | | :key="item.prop || index" |
| | | :xs="24" |
| | | :sm="12" |
| | | :md="8" |
| | | :lg="4" |
| | | :xl="4" |
| | | class="search-col" |
| | | > |
| | | <el-form-item :prop="item.prop" :rules="item.rules" class="search-form-item"> |
| | | <!-- èªå®ä¹ææ§½ --> |
| | | <slot v-if="item.slot" :name="item.slot" :item="item"></slot> |
| | | <!-- é»è®¤æ¸²æç±»å --> |
| | | <template v-else> |
| | | <!-- è¾å
¥æ¡ --> |
| | | <el-input |
| | | v-if="item.type === 'input'" |
| | | v-model="modelValue[item.prop]" |
| | | :placeholder="item.placeholder || '请è¾å
¥'" |
| | | clearable |
| | | class="full-width" |
| | | v-bind="item.props" |
| | | @keyup.enter="handleSearch" |
| | | /> |
| | | |
| | | <!-- ä¸ææ¡ --> |
| | | <el-select |
| | | v-else-if="item.type === 'select'" |
| | | v-model="modelValue[item.prop]" |
| | | :placeholder="item.placeholder || 'è¯·éæ©'" |
| | | clearable |
| | | class="full-width" |
| | | v-bind="item.props" |
| | | > |
| | | {{ item || 'è¯·éæ©' }} |
| | | <!-- <el-option |
| | | v-for="(opt,idx) in getOptions(item)" |
| | | :key="idx" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> --> |
| | | </el-select> |
| | | |
| | | <!-- æ¥æéæ©å¨ --> |
| | | <el-date-picker |
| | | v-else-if="item.type === 'date'" |
| | | v-model="modelValue[item.prop]" |
| | | type="date" |
| | | :placeholder="item.placeholder || 'éæ©æ¥æ'" |
| | | style="width: 100%" |
| | | value-format="YYYY-MM-DD" |
| | | class="full-width" |
| | | v-bind="item.props" |
| | | /> |
| | | |
| | | <!-- æ¥æèå´éæ©å¨ --> |
| | | <el-date-picker |
| | | v-else-if="item.type === 'daterange'" |
| | | v-model="modelValue[item.prop]" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | class="full-width" |
| | | v-bind="item.props" |
| | | /> |
| | | </template> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | <!-- æé®åºå --> |
| | | <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" class="search-actions-col"> |
| | | <el-form-item class="search-actions"> |
| | | <el-button style="background: #002FA7; color: white;" icon="Search" @click="handleSearch">æç´¢</el-button> |
| | | <el-button icon="Refresh" @click="handleReset">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- å±å¼/æ¶èµ·æé® --> |
| | | <div v-if="schema.length > 5" class="expand-toggle" @click="toggleExpand"> |
| | | <span>{{ isExpanded ? 'æ¶èµ·' : 'å±å¼' }}</span> |
| | | <el-icon :class="{ 'is-reverse': isExpanded }"> |
| | | <ArrowDown /> |
| | | </el-icon> |
| | | </div> |
| | | </el-form> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="SearchPanel"> |
| | | import { ref, reactive, computed, getCurrentInstance, onMounted } from 'vue'; |
| | | import { ArrowDown, Search, Refresh } from '@element-plus/icons-vue'; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const props = defineProps({ |
| | | // è¡¨åæ°æ®å¯¹è±¡ |
| | | modelValue: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | // 表åé
置项 |
| | | schema: { |
| | | type: Array, |
| | | default: () => [] |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'search', 'reset']); |
| | | |
| | | // æ¯å¦å±å¼ |
| | | const isExpanded = ref(false); |
| | | const formRef = ref(null); |
| | | const dictMap = reactive({}); |
| | | |
| | | // 计ç®å¯è§ç schema 项 |
| | | const visibleSchema = computed(() => { |
| | | if (isExpanded.value || props.schema.length <= 5) { |
| | | return props.schema; |
| | | } |
| | | return props.schema.slice(0, 5); |
| | | }); |
| | | |
| | | // åå§ååå
¸æ°æ® |
| | | onMounted(() => { |
| | | const dicts = props.schema.filter(item => item.dict).map(item => item.dict); |
| | | if (dicts.length > 0 && proxy.useDict) { |
| | | const dictData = proxy.useDict(...dicts); |
| | | Object.keys(dictData).forEach(key => { |
| | | dictMap[key] = dictData[key]; |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | // è·å䏿é项 (æ¯æéæ options å åå
¸ dict) |
| | | function getOptions(item) { |
| | | if (item.options) return item.options; |
| | | if (item.dict && dictMap[item.dict]) { |
| | | return dictMap[item.dict].value || []; |
| | | } |
| | | return []; |
| | | } |
| | | |
| | | // æç´¢ |
| | | function handleSearch() { |
| | | emit('search', props.modelValue); |
| | | } |
| | | |
| | | // éç½® |
| | | function handleReset() { |
| | | if (formRef.value) { |
| | | formRef.value.resetFields(); |
| | | } |
| | | const keys = props.schema.map(item => item.prop).filter(Boolean); |
| | | keys.forEach(key => { |
| | | props.modelValue[key] = undefined; |
| | | }); |
| | | emit('update:modelValue', props.modelValue); |
| | | emit('reset'); |
| | | } |
| | | |
| | | // 忢å±å¼/æ¶èµ· |
| | | function toggleExpand() { |
| | | isExpanded.value = !isExpanded.value; |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .search-panel-container { |
| | | background: #fff; |
| | | padding: 15px 15px 5px; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
| | | margin-bottom: 15px; |
| | | |
| | | .search-form { |
| | | .form-row { |
| | | width: 100%; |
| | | } |
| | | |
| | | .search-col { |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .search-form-item { |
| | | margin-right: 0; |
| | | margin-bottom: 0; |
| | | width: 100%; |
| | | |
| | | :deep(.el-form-item__content) { |
| | | width: 100%; |
| | | } |
| | | } |
| | | |
| | | .full-width { |
| | | width: 100% !important; |
| | | } |
| | | |
| | | .search-actions-col { |
| | | margin-left: auto; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .search-actions { |
| | | margin-bottom: 0; |
| | | margin-right: 0; |
| | | |
| | | :deep(.el-button--primary) { |
| | | background-color: #409eff; |
| | | border-color: #409eff; |
| | | } |
| | | } |
| | | |
| | | .expand-toggle { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 4px; |
| | | font-size: 13px; |
| | | color: #909399; |
| | | cursor: pointer; |
| | | padding: 5px 0; |
| | | user-select: none; |
| | | width: 100%; |
| | | border-top: 1px solid #f0f2f5; |
| | | margin-top: 5px; |
| | | |
| | | &:hover { |
| | | color: #409eff; |
| | | } |
| | | |
| | | .el-icon { |
| | | transition: transform 0.3s; |
| | | &.is-reverse { |
| | | transform: rotate(180deg); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | |
| | |
| | | emits("remove", file); |
| | | }; |
| | | |
| | | const clearFiles = () => { |
| | | fileList.value = []; |
| | | }; |
| | | |
| | | defineExpose({ |
| | | fileList, |
| | | uploadApi, |
| | | clearFiles, |
| | | }); |
| | | </script> |
| | | |
| | |
| | | <div class="notification-popover-content"> |
| | | <div class="popover-header"> |
| | | <span class="popover-title">æ¶æ¯éç¥</span> |
| | | <el-button type="primary" size="small" @click="handleMarkAllAsRead" :disabled="unreadCount === 0"> |
| | | <el-button type="primary" |
| | | size="small" |
| | | @click="handleMarkAllAsRead" |
| | | :disabled="unreadCount === 0"> |
| | | ä¸é®å·²è¯» |
| | | </el-button> |
| | | </div> |
| | | |
| | | <div class="notification-content"> |
| | | <el-tabs v-model="activeTab" @tab-change="handleTabChange"> |
| | | <el-tab-pane :label="`æªè¯»(${unreadCount})`" name="unread"> |
| | | <div v-if="unreadList.length === 0" class="empty-state"> |
| | | <el-tabs v-model="activeTab" |
| | | @tab-change="handleTabChange"> |
| | | <el-tab-pane :label="`æªè¯»(${unreadCount})`" |
| | | name="unread"> |
| | | <div v-if="unreadList.length === 0" |
| | | class="empty-state"> |
| | | <el-empty description="ææ æªè¯»æ¶æ¯" /> |
| | | </div> |
| | | <div v-else class="notification-list"> |
| | | <div |
| | | v-for="item in unreadList" |
| | | :key="item.id" |
| | | class="notification-item" |
| | | > |
| | | <div v-else |
| | | class="notification-list"> |
| | | <div v-for="item in unreadList" |
| | | :key="item.id" |
| | | class="notification-item"> |
| | | <div class="notification-icon"> |
| | | <el-icon :size="24" color="#67C23A"> |
| | | <el-icon :size="24" |
| | | color="#67C23A"> |
| | | <Bell /> |
| | | </el-icon> |
| | | </div> |
| | |
| | | <div class="notification-time">{{ item.createTime }}</div> |
| | | </div> |
| | | <div class="notification-action"> |
| | | <el-button type="primary" size="small" @click="handleConfirm(item)"> |
| | | <el-button type="primary" |
| | | size="small" |
| | | @click="handleConfirm(item)"> |
| | | 确认 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="已读" name="read"> |
| | | <div v-if="readList.length === 0" class="empty-state"> |
| | | <el-tab-pane label="已读" |
| | | name="read"> |
| | | <div v-if="readList.length === 0" |
| | | class="empty-state"> |
| | | <el-empty description="ææ å·²è¯»æ¶æ¯" /> |
| | | </div> |
| | | <div v-else class="notification-list"> |
| | | <div |
| | | v-for="item in readList" |
| | | :key="item.id" |
| | | class="notification-item read" |
| | | > |
| | | <div v-else |
| | | class="notification-list"> |
| | | <div v-for="item in readList" |
| | | :key="item.id" |
| | | class="notification-item read"> |
| | | <div class="notification-icon"> |
| | | <el-icon :size="24" color="#909399"> |
| | | <el-icon :size="24" |
| | | color="#909399"> |
| | | <Bell /> |
| | | </el-icon> |
| | | </div> |
| | |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | |
| | | <!-- å页 --> |
| | | <div class="pagination-wrapper" v-if="total > 0"> |
| | | <el-pagination |
| | | v-model:current-page="pageNum" |
| | | v-model:page-size="pageSize" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="total" |
| | | layout="prev, pager, next, sizes" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handlePageChange" |
| | | /> |
| | | <div class="pagination-wrapper" |
| | | v-if="total > 0"> |
| | | <el-pagination v-model:current-page="pageNum" |
| | | v-model:page-size="pageSize" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="total" |
| | | layout="prev, pager, next, sizes" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handlePageChange" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Bell } from '@element-plus/icons-vue' |
| | | import { listMessage, markAsRead, markAllAsRead, confirmMessage, getUnreadCount } from '@/api/system/message' |
| | | import { ElMessage } from 'element-plus' |
| | | import useUserStore from '@/store/modules/user' |
| | | import { useRouter } from 'vue-router' |
| | | import { Bell } from "@element-plus/icons-vue"; |
| | | import { |
| | | listMessage, |
| | | markAsRead, |
| | | markAllAsRead, |
| | | confirmMessage, |
| | | getUnreadCount, |
| | | } from "@/api/system/message"; |
| | | import { ElMessage } from "element-plus"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { useRouter } from "vue-router"; |
| | | |
| | | const userStore = useUserStore() |
| | | const router = useRouter() |
| | | const emit = defineEmits(['unreadCountChange']) |
| | | const userStore = useUserStore(); |
| | | const router = useRouter(); |
| | | const emit = defineEmits(["unreadCountChange"]); |
| | | |
| | | const activeTab = ref('unread') |
| | | const unreadList = ref([]) |
| | | const readList = ref([]) |
| | | const unreadCount = ref(0) |
| | | const total = ref(0) |
| | | const pageNum = ref(1) |
| | | const pageSize = ref(10) |
| | | const activeTab = ref("unread"); |
| | | const unreadList = ref([]); |
| | | const readList = ref([]); |
| | | const unreadCount = ref(0); |
| | | const total = ref(0); |
| | | const pageNum = ref(1); |
| | | const pageSize = ref(10); |
| | | |
| | | // å è½½æ¶æ¯å表 |
| | | const loadMessages = async () => { |
| | | try { |
| | | const consigneeId = userStore.id |
| | | if (!consigneeId) { |
| | | console.warn('æªè·åå°å½åç»å½ç¨æ·ID') |
| | | return |
| | | } |
| | | const params = { |
| | | consigneeId: consigneeId, |
| | | current: pageNum.value, |
| | | size: pageSize.value, |
| | | status: activeTab.value === 'read' ? 1 : 0 |
| | | } |
| | | const res = await listMessage(params) |
| | | if (res.code === 200) { |
| | | if (activeTab.value === 'unread') { |
| | | unreadList.value = res.data.records || [] |
| | | } else { |
| | | readList.value = res.data.records || [] |
| | | // å è½½æ¶æ¯å表 |
| | | const loadMessages = async () => { |
| | | try { |
| | | const consigneeId = userStore.id; |
| | | if (!consigneeId) { |
| | | console.warn("æªè·åå°å½åç»å½ç¨æ·ID"); |
| | | return; |
| | | } |
| | | total.value = res.data.total || 0 |
| | | const params = { |
| | | consigneeId: consigneeId, |
| | | current: pageNum.value, |
| | | size: pageSize.value, |
| | | status: activeTab.value === "read" ? 1 : 0, |
| | | }; |
| | | const res = await listMessage(params); |
| | | if (res.code === 200) { |
| | | if (activeTab.value === "unread") { |
| | | unreadList.value = res.data.records || []; |
| | | } else { |
| | | readList.value = res.data.records || []; |
| | | } |
| | | total.value = res.data.total || 0; |
| | | } |
| | | } catch (error) { |
| | | console.error("å è½½æ¶æ¯å表失败:", error); |
| | | } |
| | | } catch (error) { |
| | | console.error('å è½½æ¶æ¯å表失败:', error) |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // å è½½æªè¯»æ°é |
| | | const loadUnreadCount = async () => { |
| | | try { |
| | | const consigneeId = userStore.id |
| | | if (!consigneeId) { |
| | | console.warn('æªè·åå°å½åç»å½ç¨æ·ID') |
| | | return |
| | | // å è½½æªè¯»æ°é |
| | | const loadUnreadCount = async () => { |
| | | try { |
| | | const consigneeId = userStore.id; |
| | | if (!consigneeId) { |
| | | console.warn("æªè·åå°å½åç»å½ç¨æ·ID"); |
| | | return; |
| | | } |
| | | const res = await getUnreadCount(consigneeId); |
| | | if (res.code === 200) { |
| | | unreadCount.value = res.data || 0; |
| | | emit("unreadCountChange", unreadCount.value); |
| | | } |
| | | } catch (error) { |
| | | console.error("å è½½æªè¯»æ°é失败:", error); |
| | | } |
| | | const res = await getUnreadCount(consigneeId) |
| | | if (res.code === 200) { |
| | | unreadCount.value = res.data || 0 |
| | | emit('unreadCountChange', unreadCount.value) |
| | | } |
| | | } catch (error) { |
| | | console.error('å è½½æªè¯»æ°é失败:', error) |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // æ ç¾é¡µåæ¢ |
| | | const handleTabChange = (tab) => { |
| | | pageNum.value = 1 |
| | | loadMessages() |
| | | } |
| | | // æ ç¾é¡µåæ¢ |
| | | const handleTabChange = tab => { |
| | | pageNum.value = 1; |
| | | loadMessages(); |
| | | }; |
| | | |
| | | // ç¡®è®¤æ¶æ¯ |
| | | const handleConfirm = async (item) => { |
| | | try { |
| | | console.log('item', item) |
| | | const res = await confirmMessage(item.noticeId, 1) |
| | | if (res.code === 200) { |
| | | ElMessage.success('确认æå') |
| | | // éæ°å è½½æ°æ® |
| | | loadMessages() |
| | | loadUnreadCount() |
| | | |
| | | // æ ¹æ® jumpPath è¿è¡é¡µé¢è·³è½¬ |
| | | if (item.jumpPath) { |
| | | try { |
| | | // è§£æ jumpPathï¼å离路å¾åæ¥è¯¢åæ° |
| | | const [path, queryString] = item.jumpPath.split('?') |
| | | let query = {} |
| | | |
| | | if (queryString) { |
| | | // è§£ææ¥è¯¢åæ° |
| | | queryString.split('&').forEach(param => { |
| | | const [key, value] = param.split('=') |
| | | if (key && value) { |
| | | query[key] = decodeURIComponent(value) |
| | | } |
| | | }) |
| | | // ç¡®è®¤æ¶æ¯ |
| | | const handleConfirm = async item => { |
| | | try { |
| | | console.log("item", item); |
| | | const res = await confirmMessage(item.noticeId, 1); |
| | | if (res.code === 200) { |
| | | ElMessage.success("确认æå"); |
| | | // éæ°å è½½æ°æ® |
| | | loadMessages(); |
| | | loadUnreadCount(); |
| | | |
| | | // æ ¹æ® jumpPath è¿è¡é¡µé¢è·³è½¬ |
| | | if (item.jumpPath) { |
| | | try { |
| | | // è§£æ jumpPathï¼å离路å¾åæ¥è¯¢åæ° |
| | | const [path, queryString] = item.jumpPath.split("?"); |
| | | let query = {}; |
| | | |
| | | if (queryString) { |
| | | // è§£ææ¥è¯¢åæ° |
| | | queryString.split("&").forEach(param => { |
| | | const [key, value] = param.split("="); |
| | | if (key && value) { |
| | | query[key] = decodeURIComponent(value); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // è·³è½¬å°æå®é¡µé¢ |
| | | router.push({ |
| | | path: path, |
| | | query: query, |
| | | }); |
| | | } catch (error) { |
| | | console.error("页é¢è·³è½¬å¤±è´¥:", error); |
| | | } |
| | | |
| | | // è·³è½¬å°æå®é¡µé¢ |
| | | router.push({ |
| | | path: path, |
| | | query: query |
| | | }) |
| | | } catch (error) { |
| | | console.error('页é¢è·³è½¬å¤±è´¥:', error) |
| | | } |
| | | } |
| | | } catch (error) { |
| | | console.error("ç¡®è®¤æ¶æ¯å¤±è´¥:", error); |
| | | ElMessage.error("确认失败"); |
| | | } |
| | | } catch (error) { |
| | | console.error('ç¡®è®¤æ¶æ¯å¤±è´¥:', error) |
| | | ElMessage.error('确认失败') |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // ä¸é®å·²è¯» |
| | | const handleMarkAllAsRead = async () => { |
| | | try { |
| | | const res = await markAllAsRead() |
| | | if (res.code === 200) { |
| | | ElMessage.success('å·²å
¨é¨æ 记为已读') |
| | | loadMessages() |
| | | loadUnreadCount() |
| | | // ä¸é®å·²è¯» |
| | | const handleMarkAllAsRead = async () => { |
| | | try { |
| | | const res = await markAllAsRead(); |
| | | if (res.code === 200) { |
| | | ElMessage.success("å·²å
¨é¨æ 记为已读"); |
| | | loadMessages(); |
| | | loadUnreadCount(); |
| | | } |
| | | } catch (error) { |
| | | console.error("ä¸é®å·²è¯»å¤±è´¥:", error); |
| | | ElMessage.error("æä½å¤±è´¥"); |
| | | } |
| | | } catch (error) { |
| | | console.error('ä¸é®å·²è¯»å¤±è´¥:', error) |
| | | ElMessage.error('æä½å¤±è´¥') |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // å页大尿¹å |
| | | const handleSizeChange = (size) => { |
| | | pageSize.value = size |
| | | pageNum.value = 1 |
| | | loadMessages() |
| | | } |
| | | // å页大尿¹å |
| | | const handleSizeChange = size => { |
| | | pageSize.value = size; |
| | | pageNum.value = 1; |
| | | loadMessages(); |
| | | }; |
| | | |
| | | // é¡µç æ¹å |
| | | const handlePageChange = (page) => { |
| | | pageNum.value = page |
| | | loadMessages() |
| | | } |
| | | // é¡µç æ¹å |
| | | const handlePageChange = page => { |
| | | pageNum.value = page; |
| | | loadMessages(); |
| | | }; |
| | | |
| | | // ç»ä»¶æè½½æ¶å è½½æªè¯»æ°é |
| | | onMounted(() => { |
| | | loadUnreadCount() |
| | | }) |
| | | // ç»ä»¶æè½½æ¶å è½½æªè¯»æ°é |
| | | onMounted(() => { |
| | | loadUnreadCount(); |
| | | }); |
| | | |
| | | // çå¬ç¶ç»ä»¶ä¼ éç visible ç¶æï¼éè¿ watch å¨ Navbar ä¸å¤çï¼ |
| | | // è¿éåªè´è´£æ°æ®å è½½ï¼ä¸æ§å¶æ¾ç¤º |
| | | // çå¬ç¶ç»ä»¶ä¼ éç visible ç¶æï¼éè¿ watch å¨ Navbar ä¸å¤çï¼ |
| | | // è¿éåªè´è´£æ°æ®å è½½ï¼ä¸æ§å¶æ¾ç¤º |
| | | |
| | | // æ´é²æ¹æ³ä¾å¤é¨è°ç¨ |
| | | defineExpose({ |
| | | loadUnreadCount, |
| | | loadMessages |
| | | }) |
| | | // æ´é²æ¹æ³ä¾å¤é¨è°ç¨ |
| | | defineExpose({ |
| | | loadUnreadCount, |
| | | loadMessages, |
| | | }); |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .notification-popover-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 500px; |
| | | padding: 16px; |
| | | } |
| | | |
| | | .popover-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | width: 100%; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | |
| | | .popover-title { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | } |
| | | |
| | | .notification-content { |
| | | max-height: 60vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.el-tabs) { |
| | | flex: 1; |
| | | .notification-popover-content { |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 0; |
| | | |
| | | .el-tabs__header { |
| | | margin-bottom: 0; |
| | | flex-shrink: 0; |
| | | padding: 0; |
| | | } |
| | | |
| | | .el-tabs__content { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | min-height: 0; |
| | | padding-top: 16px; |
| | | } |
| | | |
| | | .el-tab-pane { |
| | | height: 100%; |
| | | } |
| | | width: 500px; |
| | | padding: 16px; |
| | | } |
| | | } |
| | | |
| | | .empty-state { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | min-height: 300px; |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .notification-list { |
| | | .notification-item { |
| | | .popover-header { |
| | | display: flex; |
| | | padding: 12px 0; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | width: 100%; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | transition: background-color 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | &.read { |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | .notification-icon { |
| | | flex-shrink: 0; |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background-color: #f0f9ff; |
| | | border-radius: 50%; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | | .notification-content-wrapper { |
| | | flex: 1; |
| | | min-width: 0; |
| | | |
| | | .notification-title { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .notification-detail { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | line-height: 1.5; |
| | | margin-bottom: 8px; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .notification-time { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .notification-action { |
| | | flex-shrink: 0; |
| | | margin-left: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | .popover-title { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .pagination-wrapper { |
| | | margin-top: 16px; |
| | | padding-top: 16px; |
| | | border-top: 1px solid #f0f0f0; |
| | | display: flex; |
| | | justify-content: center; |
| | | padding-left: 0; |
| | | padding-right: 0; |
| | | } |
| | | .notification-content { |
| | | max-height: 60vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | :deep(.el-tabs) { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 0; |
| | | |
| | | .el-tabs__header { |
| | | margin-bottom: 0; |
| | | flex-shrink: 0; |
| | | padding: 0; |
| | | } |
| | | |
| | | .el-tabs__content { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | min-height: 0; |
| | | padding-top: 16px; |
| | | } |
| | | |
| | | .el-tab-pane { |
| | | height: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .empty-state { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | min-height: 300px; |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .notification-list { |
| | | .notification-item { |
| | | display: flex; |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | transition: background-color 0.3s; |
| | | |
| | | &:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | &.read { |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | .notification-icon { |
| | | flex-shrink: 0; |
| | | width: 40px; |
| | | height: 40px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | background-color: #f0f9ff; |
| | | border-radius: 50%; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | | .notification-content-wrapper { |
| | | flex: 1; |
| | | min-width: 0; |
| | | |
| | | .notification-title { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .notification-detail { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | line-height: 1.5; |
| | | margin-bottom: 8px; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .notification-time { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .notification-action { |
| | | flex-shrink: 0; |
| | | margin-left: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .pagination-wrapper { |
| | | margin-top: 16px; |
| | | padding-top: 16px; |
| | | border-top: 1px solid #f0f0f0; |
| | | display: flex; |
| | | justify-content: center; |
| | | padding-left: 0; |
| | | padding-right: 0; |
| | | } |
| | | </style> |
| | | |
| | |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | path: "/projectManagement/Management/detail", |
| | | component: Layout, |
| | | hidden: true, |
| | | children: [ |
| | | { |
| | | path: ":id", |
| | | component: () => import("@/views/projectManagement/Management/projectDetail.vue"), |
| | | name: "ProjectManagementDetail", |
| | | meta: { title: "项ç®è¯¦æ
", activeMenu: "/projectManagement/Management" }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | // å¨æè·¯ç±ï¼åºäºç¨æ·æé卿å»å è½½ |
| | |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">客æ·åç§°ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.customerName" |
| | | style="width: 240px;margin-right: 10px" |
| | | placeholder="请è¾å
¥" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span class="search_title">客æ·åç±»ï¼</span> |
| | | <el-select |
| | | v-model="searchForm.customerType" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 240px" |
| | | clearable |
| | | @change="handleQuery" |
| | | > |
| | | <el-option label="é¶å®å®¢æ·" value="é¶å®å®¢æ·" /> |
| | | <el-option label="è¿éå客æ·" value="è¿éå客æ·" /> |
| | | <el-input v-model="searchForm.customerName" |
| | | style="width: 240px;margin-right: 10px" |
| | | placeholder="请è¾å
¥" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" /> |
| | | <span class="search_title">客æ·åç±»ï¼</span> |
| | | <el-select v-model="searchForm.customerType" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 240px" |
| | | clearable |
| | | @change="handleQuery"> |
| | | <el-option label="é¶å®å®¢æ·" |
| | | value="é¶å®å®¢æ·" /> |
| | | <el-option label="è¿éå客æ·" |
| | | value="è¿éå客æ·" /> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | <el-button type="primary" |
| | | @click="handleQuery" |
| | | style="margin-left: 10px">æç´¢</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢å®¢æ·</el-button> |
| | | <el-button type="primary" |
| | | @click="openForm('add')">æ°å¢å®¢æ·</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <el-button type="info" plain icon="Upload" @click="handleImport" |
| | | >导å
¥</el-button |
| | | > |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | <el-button type="info" |
| | | plain |
| | | icon="Upload" |
| | | @click="handleImport">导å
¥</el-button> |
| | | <el-button type="danger" |
| | | plain |
| | | @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | ></PIMTable> |
| | | <PIMTable rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination"></PIMTable> |
| | | </div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢å®¢æ·ä¿¡æ¯' : 'ç¼è¾å®¢æ·ä¿¡æ¯'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <el-form |
| | | :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef" |
| | | > |
| | | <el-dialog v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢å®¢æ·ä¿¡æ¯' : 'ç¼è¾å®¢æ·ä¿¡æ¯'" |
| | | width="70%" |
| | | @close="closeDia"> |
| | | <el-form :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客æ·åç§°ï¼" prop="customerName"> |
| | | <el-input |
| | | v-model="form.customerName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="客æ·åç§°ï¼" |
| | | prop="customerName"> |
| | | <el-input v-model="form.customerName" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item |
| | | label="纳ç¨äººè¯å«å·ï¼" |
| | | prop="taxpayerIdentificationNumber" |
| | | > |
| | | <el-input |
| | | v-model="form.taxpayerIdentificationNumber" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="纳ç¨äººè¯å«å·ï¼" |
| | | prop="taxpayerIdentificationNumber"> |
| | | <el-input v-model="form.taxpayerIdentificationNumber" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å
¬å¸å°åï¼" prop="companyAddress"> |
| | | <el-input |
| | | v-model="form.companyAddress" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="å
¬å¸å°åï¼" |
| | | prop="companyAddress"> |
| | | <el-input v-model="form.companyAddress" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å
¬å¸çµè¯ï¼" prop="companyPhone"> |
| | | <el-input |
| | | v-model="form.companyPhone" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="å
¬å¸çµè¯ï¼" |
| | | prop="companyPhone"> |
| | | <el-input v-model="form.companyPhone" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¶è¡åºæ¬æ·ï¼" prop="basicBankAccount"> |
| | | <el-input |
| | | v-model="form.basicBankAccount" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="é¶è¡åºæ¬æ·ï¼" |
| | | prop="basicBankAccount"> |
| | | <el-input v-model="form.basicBankAccount" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¶è¡è´¦å·ï¼" prop="bankAccount"> |
| | | <el-input |
| | | v-model="form.bankAccount" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="é¶è¡è´¦å·ï¼" |
| | | prop="bankAccount"> |
| | | <el-input v-model="form.bankAccount" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="弿·è¡å·ï¼" prop="bankCode"> |
| | | <el-input |
| | | v-model="form.bankCode" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | <el-form-item label="弿·è¡å·ï¼" |
| | | prop="bankCode"> |
| | | <el-input v-model="form.bankCode" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客æ·åç±»ï¼" prop="customerType"> |
| | | <el-select v-model="form.customerType" placeholder="è¯·éæ©" clearable> |
| | | <el-option label="é¶å®å®¢æ·" value="é¶å®å®¢æ·" /> |
| | | <el-option label="è¿éå客æ·" value="è¿éå客æ·" /> |
| | | <el-form-item label="客æ·åç±»ï¼" |
| | | prop="customerType"> |
| | | <el-select v-model="form.customerType" |
| | | placeholder="è¯·éæ©" |
| | | clearable> |
| | | <el-option label="é¶å®å®¢æ·" |
| | | value="é¶å®å®¢æ·" /> |
| | | <el-option label="è¿éå客æ·" |
| | | value="è¿éå客æ·" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30" v-for="(contact, index) in formYYs.contactList" :key="index"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è系人ï¼" prop="contactPerson"> |
| | | <el-input v-model="contact.contactPerson" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="èç³»çµè¯ï¼" prop="contactPhone"> |
| | | <div style="display: flex; align-items: center;width: 100%;"> |
| | | <el-input v-model="contact.contactPhone" placeholder="请è¾å
¥" clearable /> |
| | | <el-button @click="removeContact(index)" type="danger" circle style="margin-left: 5px;"> |
| | | <el-icon><Close /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-button @click="addNewContact" style="margin-bottom: 10px;">+ æ°å¢è系人</el-button> |
| | | <el-row :gutter="30" |
| | | v-for="(contact, index) in formYYs.contactList" |
| | | :key="index"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è系人ï¼" |
| | | prop="contactPerson"> |
| | | <el-input v-model="contact.contactPerson" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="èç³»çµè¯ï¼" |
| | | prop="contactPhone"> |
| | | <div style="display: flex; align-items: center;width: 100%;"> |
| | | <el-input v-model="contact.contactPhone" |
| | | placeholder="请è¾å
¥" |
| | | clearable /> |
| | | <el-button @click="removeContact(index)" |
| | | type="danger" |
| | | circle |
| | | style="margin-left: 5px;"> |
| | | <el-icon> |
| | | <Close /> |
| | | </el-icon> |
| | | </el-button> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-button @click="addNewContact" |
| | | style="margin-bottom: 10px;">+ æ°å¢è系人</el-button> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»´æ¤äººï¼" prop="maintainer"> |
| | | <el-select |
| | | v-model="form.maintainer" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | disabled |
| | | > |
| | | <el-option |
| | | v-for="item in userList" |
| | | :key="item.nickName" |
| | | :label="item.nickName" |
| | | :value="item.nickName" |
| | | /> |
| | | <el-form-item label="ç»´æ¤äººï¼" |
| | | prop="maintainer"> |
| | | <el-select v-model="form.maintainer" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | disabled> |
| | | <el-option v-for="item in userList" |
| | | :key="item.nickName" |
| | | :label="item.nickName" |
| | | :value="item.nickName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»´æ¤æ¶é´ï¼" prop="maintenanceTime"> |
| | | <el-date-picker |
| | | style="width: 100%" |
| | | v-model="form.maintenanceTime" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | /> |
| | | <el-form-item label="ç»´æ¤æ¶é´ï¼" |
| | | prop="maintenanceTime"> |
| | | <el-date-picker style="width: 100%" |
| | | v-model="form.maintenanceTime" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="è¯·éæ©" |
| | | clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- ç¨æ·å¯¼å
¥å¯¹è¯æ¡ --> |
| | | <el-dialog |
| | | :title="upload.title" |
| | | v-model="upload.open" |
| | | width="400px" |
| | | append-to-body |
| | | > |
| | | <el-upload |
| | | ref="uploadRef" |
| | | :limit="1" |
| | | accept=".xlsx, .xls" |
| | | :headers="upload.headers" |
| | | :action="upload.url + '?updateSupport=' + upload.updateSupport" |
| | | :disabled="upload.isUploading" |
| | | :before-upload="upload.beforeUpload" |
| | | :on-progress="upload.onProgress" |
| | | :on-success="upload.onSuccess" |
| | | :on-error="upload.onError" |
| | | :on-change="upload.onChange" |
| | | :auto-upload="false" |
| | | drag |
| | | > |
| | | <el-dialog :title="upload.title" |
| | | v-model="upload.open" |
| | | width="400px" |
| | | append-to-body> |
| | | <el-upload ref="uploadRef" |
| | | :limit="1" |
| | | accept=".xlsx, .xls" |
| | | :headers="upload.headers" |
| | | :action="upload.url + '?updateSupport=' + upload.updateSupport" |
| | | :disabled="upload.isUploading" |
| | | :before-upload="upload.beforeUpload" |
| | | :on-progress="upload.onProgress" |
| | | :on-success="upload.onSuccess" |
| | | :on-error="upload.onError" |
| | | :on-change="upload.onChange" |
| | | :auto-upload="false" |
| | | drag> |
| | | <el-icon class="el-icon--upload"><upload-filled /></el-icon> |
| | | <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ<em>ç¹å»ä¸ä¼ </em></div> |
| | | <template #tip> |
| | | <div class="el-upload__tip text-center"> |
| | | <span>ä»
å
许导å
¥xlsãxlsxæ ¼å¼æä»¶ã</span> |
| | | <el-link |
| | | type="primary" |
| | | :underline="false" |
| | | style="font-size: 12px; vertical-align: baseline" |
| | | @click="importTemplate" |
| | | >ä¸è½½æ¨¡æ¿</el-link |
| | | > |
| | | <el-link type="primary" |
| | | :underline="false" |
| | | style="font-size: 12px; vertical-align: baseline" |
| | | @click="importTemplate">ä¸è½½æ¨¡æ¿</el-link> |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button type="primary" |
| | | @click="submitFileForm">ç¡® å®</el-button> |
| | | <el-button @click="upload.open = false">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- å访æéå¯¹è¯æ¡ --> |
| | | <el-dialog title="å访æé" |
| | | v-model="reminderDialogVisible" |
| | | width="500px" |
| | | @close="closeReminderDialog"> |
| | | <el-form :model="reminderForm" |
| | | label-width="100px" |
| | | :rules="reminderRules" |
| | | ref="reminderFormRef"> |
| | | <el-form-item label="客æ·åç§°ï¼"> |
| | | <el-input v-model="reminderForm.customerName" |
| | | disabled /> |
| | | </el-form-item> |
| | | <el-form-item label="æéå¼å
³ï¼"> |
| | | <el-switch v-model="reminderForm.reminderSwitch" /> |
| | | </el-form-item> |
| | | <el-form-item label="æéå
容ï¼" |
| | | prop="reminderContent"> |
| | | <el-input v-model="reminderForm.reminderContent" |
| | | type="textarea" |
| | | :maxlength="100" |
| | | show-word-limit |
| | | placeholder="请è¾å
¥æéå
容" /> |
| | | </el-form-item> |
| | | <el-form-item label="æéæ¶é´ï¼" |
| | | prop="reminderTime"> |
| | | <el-date-picker v-model="reminderForm.reminderTime" |
| | | type="datetime" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | placeholder="è¯·éæ©æéæ¶é´" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeReminderDialog">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="submitReminderForm">æäº¤</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- æ·»å /ä¿®æ¹æ´½è°è¿åº¦å¯¹è¯æ¡ --> |
| | | <el-dialog :title="negotiationForm.editIndex !== undefined ? 'ä¿®æ¹è¿åº¦' : 'æ·»å è¿åº¦'" |
| | | v-model="negotiationDialogVisible" |
| | | width="600px" |
| | | @close="closeNegotiationDialog"> |
| | | <el-form :model="negotiationForm" |
| | | label-width="100px" |
| | | :rules="negotiationRules" |
| | | ref="negotiationFormRef"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è·è¿æ¹å¼ï¼" |
| | | prop="followUpMethod"> |
| | | <el-select v-model="negotiationForm.followUpMethod" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%"> |
| | | <el-option label="çµè¯" |
| | | value="çµè¯" /> |
| | | <el-option label="é®ä»¶" |
| | | value="é®ä»¶" /> |
| | | <el-option label="ä¸é¨" |
| | | value="ä¸é¨" /> |
| | | <el-option label="微信" |
| | | value="微信" /> |
| | | <el-option label="å
¶ä»" |
| | | value="å
¶ä»" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è·è¿ç¨åº¦ï¼" |
| | | prop="followUpLevel"> |
| | | <el-select v-model="negotiationForm.followUpLevel" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%"> |
| | | <el-option label="æ½å¨å®¢æ·" |
| | | value="æ½å¨å®¢æ·" /> |
| | | <el-option label="忬¡æè®¿" |
| | | value="忬¡æè®¿" /> |
| | | <el-option label="夿¬¡æè®¿" |
| | | value="夿¬¡æè®¿" /> |
| | | <el-option label="æå客æ·" |
| | | value="æå客æ·" /> |
| | | <el-option label="å·²ç¾çº¦å®¢æ·" |
| | | value="å·²ç¾çº¦å®¢æ·" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è·è¿æ¶é´ï¼" |
| | | prop="followUpTime"> |
| | | <el-date-picker v-model="negotiationForm.followUpTime" |
| | | type="datetime" |
| | | value-format="YYYY-MM-DD HH:mm:ss" |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è·è¿äººï¼"> |
| | | <el-input v-model="negotiationForm.followerUserName" |
| | | disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="å
容ï¼" |
| | | prop="content"> |
| | | <el-input v-model="negotiationForm.content" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请è¾å
¥" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeNegotiationDialog">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="submitNegotiationForm">æäº¤</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 客æ·è¯¦æ
å¯¹è¯æ¡ --> |
| | | <el-dialog title="客æ·è¯¦æ
" |
| | | v-model="detailDialogVisible" |
| | | width="1000px" |
| | | @close="closeDetailDialog"> |
| | | <!-- 客æ·åºæ¬ä¿¡æ¯ --> |
| | | <div class="detail-section"> |
| | | <h3 class="section-title">客æ·åºæ¬ä¿¡æ¯</h3> |
| | | <div class="info-display"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">客æ·åç§°ï¼</span> |
| | | <span class="info-value">{{ detailForm.customerName }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">客æ·åç±»ï¼</span> |
| | | <span class="info-value">{{ detailForm.customerType }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">纳ç¨äººè¯å«å·ï¼</span> |
| | | <span class="info-value">{{ detailForm.taxpayerIdentificationNumber }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">å
¬å¸çµè¯ï¼</span> |
| | | <span class="info-value">{{ detailForm.companyPhone }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">å
¬å¸å°åï¼</span> |
| | | <span class="info-value">{{ detailForm.companyAddress }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">é¶è¡åºæ¬æ·ï¼</span> |
| | | <span class="info-value">{{ detailForm.basicBankAccount }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">é¶è¡è´¦å·ï¼</span> |
| | | <span class="info-value">{{ detailForm.bankAccount }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">弿·è¡å·ï¼</span> |
| | | <span class="info-value">{{ detailForm.bankCode }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">è系人ï¼</span> |
| | | <span class="info-value">{{ detailForm.contactPerson }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">èç³»çµè¯ï¼</span> |
| | | <span class="info-value">{{ detailForm.contactPhone }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">ç»´æ¤äººï¼</span> |
| | | <span class="info-value">{{ detailForm.maintainer }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">ç»´æ¤æ¶é´ï¼</span> |
| | | <span class="info-value">{{ detailForm.maintenanceTime }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | <!-- æ´½è°è¿åº¦è®°å½ --> |
| | | <div class="detail-section"> |
| | | <div class="section-header"> |
| | | <h3 class="section-title">æ´½è°è¿åº¦è®°å½</h3> |
| | | <el-button type="primary" |
| | | size="small" |
| | | @click="openNegotiationDialog(detailForm)"> |
| | | æ·»å è¿åº¦ |
| | | </el-button> |
| | | </div> |
| | | <el-table :data="negotiationRecords" |
| | | border |
| | | style="width: 100%"> |
| | | <el-table-column prop="followUpTime" |
| | | label="è·è¿æ¶é´" |
| | | width="160" /> |
| | | <el-table-column prop="followUpMethod" |
| | | label="è·è¿æ¹å¼" |
| | | width="100" /> |
| | | <el-table-column prop="followUpLevel" |
| | | label="è·è¿ç¨åº¦" /> |
| | | <el-table-column prop="followerUserName" |
| | | label="è·è¿äºº" |
| | | width="100" /> |
| | | <el-table-column prop="content" |
| | | label="å
容" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="éä»¶" |
| | | width="100" |
| | | align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button type="info" |
| | | link |
| | | size="small" |
| | | @click="openAttachmentDialog(row)"> |
| | | <el-icon> |
| | | <Paperclip /> |
| | | </el-icon> |
| | | éä»¶ |
| | | <!-- {{ row.fileList && row.fileList.length > 0 ? row.fileList.length : 'ä¸ä¼ ' }} --> |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | | width="150" |
| | | align="center"> |
| | | <template #default="{ row, $index }"> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="editNegotiationRecord(row, $index)"> |
| | | ä¿®æ¹ |
| | | </el-button> |
| | | <el-button type="danger" |
| | | link |
| | | size="small" |
| | | @click="deleteNegotiationRecord(row, $index)"> |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div v-if="negotiationRecords.length === 0" |
| | | class="no-records"> |
| | | ææ æ´½è°è¿åº¦è®°å½ |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeDetailDialog">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- éä»¶ä¸ä¼ å¼¹çª --> |
| | | <el-dialog title="é件管ç" |
| | | v-model="attachmentDialogVisible" |
| | | width="600px" |
| | | @close="closeAttachmentDialog"> |
| | | <div class="attachment-section"> |
| | | <div class="upload-area"> |
| | | <el-upload ref="attachmentUploadRef" |
| | | :action="getAttachmentUploadUrl()" |
| | | :headers="attachmentUploadHeaders" |
| | | :file-list="currentAttachmentList" |
| | | :on-success="handleAttachmentSuccess" |
| | | :on-error="handleAttachmentError" |
| | | :on-remove="handleAttachmentRemove" |
| | | :before-upload="beforeAttachmentUpload" |
| | | multiple |
| | | :limit="10" |
| | | name="files"> |
| | | <el-button type="primary"> |
| | | <el-icon> |
| | | <Upload /> |
| | | </el-icon> |
| | | ä¸ä¼ éä»¶ |
| | | </el-button> |
| | | <template #tip> |
| | | <div class="el-upload__tip"> |
| | | æ¯æä¸ä¼ å¾çãææ¡£çæä»¶ï¼å个æä»¶ä¸è¶
è¿50MB |
| | | </div> |
| | | </template> |
| | | </el-upload> |
| | | </div> |
| | | <div v-if="currentAttachmentList.length > 0" |
| | | class="attachment-list"> |
| | | <h4>å·²ä¸ä¼ éä»¶ï¼</h4> |
| | | <el-table :data="currentAttachmentList" |
| | | border |
| | | size="small"> |
| | | <el-table-column prop="name" |
| | | label="æä»¶å" |
| | | show-overflow-tooltip /> |
| | | <el-table-column prop="size" |
| | | label="大å°" |
| | | width="100"> |
| | | <template #default="{ row }"> |
| | | {{ formatFileSize(row.size) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="{ row, $index }"> |
| | | <el-button type="primary" |
| | | link |
| | | size="small" |
| | | @click="downloadAttachment(row)"> |
| | | ä¸è½½ |
| | | </el-button> |
| | | <el-button type="danger" |
| | | link |
| | | size="small" |
| | | @click="deleteAttachment(row, $index)"> |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <div v-else |
| | | class="no-attachment"> |
| | | ææ éä»¶ |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeAttachmentDialog">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {onMounted, ref} from "vue"; |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { |
| | | addCustomer, |
| | | delCustomer, |
| | | getCustomer, |
| | | listCustomer, |
| | | updateCustomer, |
| | | } from "@/api/basicData/customerFile.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { getToken } from "@/utils/auth.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | | import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue"; |
| | | import { Search, Paperclip, Upload } from "@element-plus/icons-vue"; |
| | | import { |
| | | addCustomer, |
| | | delCustomer, |
| | | getCustomer, |
| | | listCustomer, |
| | | updateCustomer, |
| | | addCustomerFollow, |
| | | updateCustomerFollow, |
| | | delCustomerFollow, |
| | | addReturnVisit, |
| | | getReturnVisit, |
| | | } from "@/api/basicData/customerFile.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import { getToken } from "@/utils/auth.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "客æ·åç±»", |
| | | prop: "customerType", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "客æ·åç§°", |
| | | prop: "customerName", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "纳ç¨äººè¯å«ç ", |
| | | prop: "taxpayerIdentificationNumber", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "å°ååèç³»æ¹å¼", |
| | | prop: "addressPhone", |
| | | width: 250, |
| | | }, |
| | | { |
| | | label: "è系人", |
| | | prop: "contactPerson", |
| | | }, |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "contactPhone", |
| | | width:150 |
| | | }, |
| | | { |
| | | label: "é¶è¡åºæ¬æ·", |
| | | prop: "basicBankAccount", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "é¶è¡è´¦å·", |
| | | prop: "bankAccount", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "弿·è¡å·", |
| | | prop: "bankCode", |
| | | width:220 |
| | | }, |
| | | { |
| | | label: "ç»´æ¤äºº", |
| | | prop: "maintainer", |
| | | }, |
| | | { |
| | | label: "ç»´æ¤æ¶é´", |
| | | prop: "maintenanceTime", |
| | | width: 100, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | | // å访æéç¸å
³ |
| | | const reminderDialogVisible = ref(false); |
| | | const reminderFormRef = ref(); |
| | | const currentCustomerId = ref(); |
| | | const reminderForm = reactive({ |
| | | customerName: "", |
| | | reminderSwitch: false, |
| | | reminderContent: "", |
| | | reminderTime: "", |
| | | }); |
| | | const reminderRules = { |
| | | reminderContent: [ |
| | | { required: true, message: "请è¾å
¥æéå
容", trigger: "blur" }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const userList = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const total = ref(0); |
| | | reminderTime: [ |
| | | { required: true, message: "è¯·éæ©æéæ¶é´", trigger: "change" }, |
| | | ], |
| | | }; |
| | | |
| | | // ç¨æ·ä¿¡æ¯è¡¨åå¼¹æ¡æ°æ® |
| | | const operationType = ref(""); |
| | | const dialogFormVisible = ref(false); |
| | | const formYYs = ref({ // å
¶ä»å段... |
| | | contactList: [ |
| | | { |
| | | contactPerson: "", |
| | | contactPhone: "" |
| | | } |
| | | ] |
| | | }); |
| | | const data = reactive({ |
| | | searchForm: { |
| | | // æ´½è°è¿åº¦ç¸å
³ |
| | | const negotiationDialogVisible = ref(false); |
| | | const negotiationFormRef = ref(); |
| | | const negotiationForm = reactive({ |
| | | customerName: "", |
| | | customerId: "", |
| | | followUpMethod: "", |
| | | followUpLevel: "", |
| | | followUpTime: "", |
| | | followerUserName: "", |
| | | content: "", |
| | | }); |
| | | const negotiationRules = { |
| | | followUpMethod: [ |
| | | { required: true, message: "è¯·éæ©è·è¿æ¹å¼", trigger: "change" }, |
| | | ], |
| | | followUpLevel: [ |
| | | { required: true, message: "è¯·éæ©è·è¿ç¨åº¦", trigger: "change" }, |
| | | ], |
| | | followUpTime: [ |
| | | { required: true, message: "è¯·éæ©è·è¿æ¶é´", trigger: "change" }, |
| | | ], |
| | | content: [{ required: true, message: "请è¾å
¥å
容", trigger: "blur" }], |
| | | }; |
| | | |
| | | // 详æ
ç¸å
³ |
| | | const detailDialogVisible = ref(false); |
| | | const detailForm = reactive({ |
| | | customerName: "", |
| | | customerType: "", |
| | | }, |
| | | form: { |
| | | customerName: "", |
| | | taxpayerIdentificationNumber: "", |
| | | companyAddress: "", |
| | | companyPhone: "", |
| | | companyAddress: "", |
| | | basicBankAccount: "", |
| | | bankAccount: "", |
| | | bankCode: "", |
| | | contactPerson: "", |
| | | contactPhone: "", |
| | | maintainer: "", |
| | | maintenanceTime: "", |
| | | basicBankAccount: "", |
| | | bankAccount: "", |
| | | bankCode: "", |
| | | customerType: "", |
| | | }, |
| | | rules: { |
| | | customerName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | taxpayerIdentificationNumber: [ |
| | | { required: true, message: "请è¾å
¥", trigger: "blur" }, |
| | | ], |
| | | companyAddress: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | companyPhone: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | // contactPerson: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | // contactPhone: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | maintainer: [{ required: false, message: "è¯·éæ©", trigger: "change" }], |
| | | maintenanceTime: [ |
| | | { required: false, message: "è¯·éæ©", trigger: "change" }, |
| | | ], |
| | | basicBankAccount: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | bankAccount: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | bankCode: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | customerType: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const upload = reactive({ |
| | | // æ¯å¦æ¾ç¤ºå¼¹åºå±ï¼å®¢æ·å¯¼å
¥ï¼ |
| | | open: false, |
| | | // å¼¹åºå±æ é¢ï¼å®¢æ·å¯¼å
¥ï¼ |
| | | title: "", |
| | | // æ¯å¦ç¦ç¨ä¸ä¼ |
| | | isUploading: false, |
| | | // 设置ä¸ä¼ ç请æ±å¤´é¨ |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | // ä¸ä¼ çå°å |
| | | url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData", |
| | | // æä»¶ä¸ä¼ åçåè° |
| | | beforeUpload: (file) => { |
| | | console.log('æä»¶å³å°ä¸ä¼ ', file); |
| | | // å¯ä»¥å¨æ¤å¤åæä»¶ç±»åæå¤§å°æ ¡éª |
| | | const isValid = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.name.endsWith('.xlsx') || file.name.endsWith('.xls'); |
| | | if (!isValid) { |
| | | proxy.$modal.msgError("åªè½ä¸ä¼ Excel æä»¶"); |
| | | } |
| | | return isValid; |
| | | }, |
| | | // æä»¶ç¶ææ¹åæ¶çåè° |
| | | onChange: (file, fileList) => { |
| | | console.log('æä»¶ç¶ææ¹å', file, fileList); |
| | | }, |
| | | // æä»¶ä¸ä¼ æåæ¶çåè° |
| | | onSuccess: (response, file, fileList) => { |
| | | console.log('ä¸ä¼ æå', response, file, fileList); |
| | | upload.isUploading = false; |
| | | if(response.code === 200){ |
| | | proxy.$modal.msgSuccess("æä»¶ä¸ä¼ æå"); |
| | | upload.open = false; |
| | | proxy.$refs["uploadRef"].clearFiles(); |
| | | getList(); |
| | | }else if(response.code === 500){ |
| | | proxy.$modal.msgError(response.msg); |
| | | }else{ |
| | | proxy.$modal.msgWarning(response.msg); |
| | | } |
| | | }, |
| | | // æä»¶ä¸ä¼ 失败æ¶çåè° |
| | | onError: (error, file, fileList) => { |
| | | console.error('ä¸ä¼ 失败', error, file, fileList); |
| | | upload.isUploading = false; |
| | | proxy.$modal.msgError("æä»¶ä¸ä¼ 失败"); |
| | | }, |
| | | // æä»¶ä¸ä¼ è¿åº¦åè° |
| | | onProgress: (event, file, fileList) => { |
| | | console.log('ä¸ä¼ ä¸...', event.percent); |
| | | } |
| | | }); |
| | | const { searchForm, form, rules } = toRefs(data); |
| | | const addNewContact = () => { |
| | | formYYs.value.contactList.push({ |
| | | contactPerson: "", |
| | | contactPhone: "" |
| | | }); |
| | | }; |
| | | const negotiationRecords = ref([]); |
| | | |
| | | const removeContact = (index) => { |
| | | if (formYYs.value.contactList.length > 1) { |
| | | formYYs.value.contactList.splice(index, 1); |
| | | } |
| | | }; |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | listCustomer({ ...searchForm.value, ...page }).then((res) => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.records; |
| | | page.total = res.total; |
| | | }); |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | /** æäº¤ä¸ä¼ æä»¶ */ |
| | | function submitFileForm() { |
| | | upload.isUploading = true; |
| | | proxy.$refs["uploadRef"].submit(); |
| | | } |
| | | /** 导å
¥æé®æä½ */ |
| | | function handleImport() { |
| | | upload.title = "客æ·å¯¼å
¥"; |
| | | upload.open = true; |
| | | } |
| | | /** ä¸è½½æ¨¡æ¿ */ |
| | | function importTemplate() { |
| | | proxy.download("/basic/customer/downloadTemplate", {}, "客æ·å¯¼å
¥æ¨¡æ¿.xlsx"); |
| | | } |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | operationType.value = type; |
| | | form.value = {}; |
| | | form.value.maintainer = userStore.nickName; |
| | | formYYs.value.contactList = [ |
| | | // éä»¶ç¸å
³ |
| | | const attachmentDialogVisible = ref(false); |
| | | const attachmentUploadRef = ref(); |
| | | const currentAttachmentList = ref([]); |
| | | const currentFollowRecord = ref({}); |
| | | const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() }; |
| | | |
| | | // 卿æå»ºä¸ä¼ URL |
| | | const getAttachmentUploadUrl = () => { |
| | | const baseUrl = |
| | | import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload"; |
| | | return currentFollowRecord.value.id |
| | | ? `${baseUrl}/${currentFollowRecord.value.id}` |
| | | : baseUrl; |
| | | }; |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | contactPerson: "", |
| | | contactPhone: "" |
| | | } |
| | | ]; |
| | | form.value.maintenanceTime = getCurrentDate(); |
| | | userListNoPage().then((res) => { |
| | | userList.value = res.data; |
| | | label: "客æ·åç±»", |
| | | prop: "customerType", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "客æ·åç§°", |
| | | prop: "customerName", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "纳ç¨äººè¯å«ç ", |
| | | prop: "taxpayerIdentificationNumber", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "å°ååèç³»æ¹å¼", |
| | | prop: "addressPhone", |
| | | width: 250, |
| | | }, |
| | | { |
| | | label: "è系人", |
| | | prop: "contactPerson", |
| | | }, |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "contactPhone", |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "è·è¿è¿åº¦", |
| | | prop: "followUpLevel", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "è·è¿æ¶é´", |
| | | prop: "followUpTime", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "é¶è¡åºæ¬æ·", |
| | | prop: "basicBankAccount", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "é¶è¡è´¦å·", |
| | | prop: "bankAccount", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "弿·è¡å·", |
| | | prop: "bankCode", |
| | | width: 220, |
| | | }, |
| | | { |
| | | label: "ç»´æ¤äºº", |
| | | prop: "maintainer", |
| | | }, |
| | | { |
| | | label: "ç»´æ¤æ¶é´", |
| | | prop: "maintenanceTime", |
| | | width: 100, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 250, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: row => { |
| | | openForm("edit", row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: row => { |
| | | openDetailDialog(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "å访æé", |
| | | type: "text", |
| | | clickFun: row => { |
| | | openReminderDialog(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "æ·»å æ´½è°è¿åº¦", |
| | | type: "text", |
| | | clickFun: row => { |
| | | openNegotiationDialog(row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const userList = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | if (type === "edit") { |
| | | getCustomer(row.id).then((res) => { |
| | | form.value = { ...res.data }; |
| | | formYYs.value.contactList = res.data.contactPerson.split(",").map((item, index) => { |
| | | return { |
| | | contactPerson: item, |
| | | contactPhone: res.data.contactPhone.split(",")[index] |
| | | } |
| | | }); |
| | | const total = ref(0); |
| | | |
| | | }); |
| | | } |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | // æäº¤è¡¨å |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate((valid) => { |
| | | if (valid) { |
| | | if (operationType.value === "edit") { |
| | | submitEdit(); |
| | | } else { |
| | | submitAdd(); |
| | | // ç¨æ·ä¿¡æ¯è¡¨åå¼¹æ¡æ°æ® |
| | | const operationType = ref(""); |
| | | const dialogFormVisible = ref(false); |
| | | const formYYs = ref({ |
| | | // å
¶ä»å段... |
| | | contactList: [ |
| | | { |
| | | contactPerson: "", |
| | | contactPhone: "", |
| | | }, |
| | | ], |
| | | }); |
| | | const data = reactive({ |
| | | searchForm: { |
| | | customerName: "", |
| | | customerType: "", |
| | | }, |
| | | form: { |
| | | customerName: "", |
| | | taxpayerIdentificationNumber: "", |
| | | companyAddress: "", |
| | | companyPhone: "", |
| | | contactPerson: "", |
| | | contactPhone: "", |
| | | maintainer: "", |
| | | maintenanceTime: "", |
| | | basicBankAccount: "", |
| | | bankAccount: "", |
| | | bankCode: "", |
| | | customerType: "", |
| | | }, |
| | | rules: { |
| | | customerName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | taxpayerIdentificationNumber: [ |
| | | { required: true, message: "请è¾å
¥", trigger: "blur" }, |
| | | ], |
| | | companyAddress: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | companyPhone: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | // contactPerson: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | // contactPhone: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | maintainer: [{ required: false, message: "è¯·éæ©", trigger: "change" }], |
| | | maintenanceTime: [ |
| | | { required: false, message: "è¯·éæ©", trigger: "change" }, |
| | | ], |
| | | basicBankAccount: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | bankAccount: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | bankCode: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | customerType: [{ required: true, message: "è¯·éæ©", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const upload = reactive({ |
| | | // æ¯å¦æ¾ç¤ºå¼¹åºå±ï¼å®¢æ·å¯¼å
¥ï¼ |
| | | open: false, |
| | | // å¼¹åºå±æ é¢ï¼å®¢æ·å¯¼å
¥ï¼ |
| | | title: "", |
| | | // æ¯å¦ç¦ç¨ä¸ä¼ |
| | | isUploading: false, |
| | | // 设置ä¸ä¼ ç请æ±å¤´é¨ |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | // ä¸ä¼ çå°å |
| | | url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData", |
| | | // æä»¶ä¸ä¼ åçåè° |
| | | beforeUpload: file => { |
| | | console.log("æä»¶å³å°ä¸ä¼ ", file); |
| | | // å¯ä»¥å¨æ¤å¤åæä»¶ç±»åæå¤§å°æ ¡éª |
| | | const isValid = |
| | | file.type === |
| | | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || |
| | | file.name.endsWith(".xlsx") || |
| | | file.name.endsWith(".xls"); |
| | | if (!isValid) { |
| | | proxy.$modal.msgError("åªè½ä¸ä¼ Excel æä»¶"); |
| | | } |
| | | } |
| | | return isValid; |
| | | }, |
| | | // æä»¶ç¶ææ¹åæ¶çåè° |
| | | onChange: (file, fileList) => { |
| | | console.log("æä»¶ç¶ææ¹å", file, fileList); |
| | | }, |
| | | // æä»¶ä¸ä¼ æåæ¶çåè° |
| | | onSuccess: (response, file, fileList) => { |
| | | console.log("ä¸ä¼ æå", response, file, fileList); |
| | | upload.isUploading = false; |
| | | if (response.code === 200) { |
| | | proxy.$modal.msgSuccess("æä»¶ä¸ä¼ æå"); |
| | | upload.open = false; |
| | | proxy.$refs["uploadRef"].clearFiles(); |
| | | getList(); |
| | | } else if (response.code === 500) { |
| | | proxy.$modal.msgError(response.msg); |
| | | } else { |
| | | proxy.$modal.msgWarning(response.msg); |
| | | } |
| | | }, |
| | | // æä»¶ä¸ä¼ 失败æ¶çåè° |
| | | onError: (error, file, fileList) => { |
| | | console.error("ä¸ä¼ 失败", error, file, fileList); |
| | | upload.isUploading = false; |
| | | proxy.$modal.msgError("æä»¶ä¸ä¼ 失败"); |
| | | }, |
| | | // æä»¶ä¸ä¼ è¿åº¦åè° |
| | | onProgress: (event, file, fileList) => { |
| | | console.log("ä¸ä¼ ä¸...", event.percent); |
| | | }, |
| | | }); |
| | | }; |
| | | // æäº¤æ°å¢ |
| | | const submitAdd = () => { |
| | | if(formYYs.value.contactList.length < 1){ |
| | | return proxy.$modal.msgWarning("请è³å°æ·»å ä¸ä¸ªè系人"); |
| | | } |
| | | form.value.contactPerson = formYYs.value.contactList.map(item => item.contactPerson).join(","); |
| | | form.value.contactPhone = formYYs.value.contactList.map(item => item.contactPhone).join(","); |
| | | addCustomer(form.value).then((res) => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | getList(); |
| | | }); |
| | | }; |
| | | // æäº¤ä¿®æ¹ |
| | | const submitEdit = () => { |
| | | form.value.contactPerson = formYYs.value.contactList.map(item => item.contactPerson).join(","); |
| | | form.value.contactPhone = formYYs.value.contactList.map(item => item.contactPhone).join(","); |
| | | updateCustomer(form.value).then((res) => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | getList(); |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | }; |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/basic/customer/export", {}, "å®¢æ·æ¡£æ¡.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | const { searchForm, form, rules } = toRefs(data); |
| | | const addNewContact = () => { |
| | | formYYs.value.contactList.push({ |
| | | contactPerson: "", |
| | | contactPhone: "", |
| | | }); |
| | | }; |
| | | // å é¤ |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | // æ£æ¥æ¯å¦æä»äººç»´æ¤çæ°æ® |
| | | const unauthorizedData = selectedRows.value.filter(item => item.maintainer !== userStore.nickName); |
| | | if (unauthorizedData.length > 0) { |
| | | proxy.$modal.msgWarning("ä¸å¯å é¤ä»äººç»´æ¤çæ°æ®"); |
| | | }; |
| | | |
| | | const removeContact = index => { |
| | | if (formYYs.value.contactList.length > 1) { |
| | | formYYs.value.contactList.splice(index, 1); |
| | | } |
| | | }; |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = obj => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | listCustomer({ ...searchForm.value, ...page }).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.records; |
| | | page.total = res.total; |
| | | }); |
| | | }; |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = selection => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | /** æäº¤ä¸ä¼ æä»¶ */ |
| | | function submitFileForm() { |
| | | upload.isUploading = true; |
| | | proxy.$refs["uploadRef"].submit(); |
| | | } |
| | | /** 导å
¥æé®æä½ */ |
| | | function handleImport() { |
| | | upload.title = "客æ·å¯¼å
¥"; |
| | | upload.open = true; |
| | | } |
| | | /** ä¸è½½æ¨¡æ¿ */ |
| | | function importTemplate() { |
| | | proxy.download("/basic/customer/downloadTemplate", {}, "客æ·å¯¼å
¥æ¨¡æ¿.xlsx"); |
| | | } |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | operationType.value = type; |
| | | form.value = {}; |
| | | form.value.maintainer = userStore.nickName; |
| | | formYYs.value.contactList = [ |
| | | { |
| | | contactPerson: "", |
| | | contactPhone: "", |
| | | }, |
| | | ]; |
| | | form.value.maintenanceTime = getCurrentDate(); |
| | | userListNoPage().then(res => { |
| | | userList.value = res.data; |
| | | }); |
| | | if (type === "edit") { |
| | | getCustomer(row.id).then(res => { |
| | | form.value = { ...res.data }; |
| | | formYYs.value.contactList = res.data.contactPerson |
| | | .split(",") |
| | | .map((item, index) => { |
| | | return { |
| | | contactPerson: item, |
| | | contactPhone: res.data.contactPhone.split(",")[index], |
| | | }; |
| | | }); |
| | | }); |
| | | } |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | // æäº¤è¡¨å |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | if (operationType.value === "edit") { |
| | | submitEdit(); |
| | | } else { |
| | | submitAdd(); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | // æäº¤æ°å¢ |
| | | const submitAdd = () => { |
| | | if (formYYs.value.contactList.length < 1) { |
| | | return proxy.$modal.msgWarning("请è³å°æ·»å ä¸ä¸ªè系人"); |
| | | } |
| | | form.value.contactPerson = formYYs.value.contactList |
| | | .map(item => item.contactPerson) |
| | | .join(","); |
| | | form.value.contactPhone = formYYs.value.contactList |
| | | .map(item => item.contactPhone) |
| | | .join(","); |
| | | addCustomer(form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | getList(); |
| | | }); |
| | | }; |
| | | // æäº¤ä¿®æ¹ |
| | | const submitEdit = () => { |
| | | form.value.contactPerson = formYYs.value.contactList |
| | | .map(item => item.contactPerson) |
| | | .join(","); |
| | | form.value.contactPhone = formYYs.value.contactList |
| | | .map(item => item.contactPhone) |
| | | .join(","); |
| | | updateCustomer(form.value).then(res => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | getList(); |
| | | }); |
| | | }; |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | }; |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/basic/customer/export", {}, "å®¢æ·æ¡£æ¡.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | // å é¤ |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | // æ£æ¥æ¯å¦æä»äººç»´æ¤çæ°æ® |
| | | const unauthorizedData = selectedRows.value.filter( |
| | | item => item.maintainer !== userStore.nickName |
| | | ); |
| | | if (unauthorizedData.length > 0) { |
| | | proxy.$modal.msgWarning("ä¸å¯å é¤ä»äººç»´æ¤çæ°æ®"); |
| | | return; |
| | | } |
| | | ids = selectedRows.value.map(item => item.id); |
| | | } else { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | } else { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤æç¤º", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | tableLoading.value = true; |
| | | delCustomer(ids) |
| | | .then((res) => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤æç¤º", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | .then(() => { |
| | | tableLoading.value = true; |
| | | delCustomer(ids) |
| | | .then(res => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // æå¼å访æéå¼¹çª |
| | | const openReminderDialog = row => { |
| | | currentCustomerId.value = row.id; |
| | | reminderForm.customerName = row.customerName; |
| | | reminderForm.reminderSwitch = false; |
| | | reminderForm.reminderContent = ""; |
| | | reminderForm.reminderTime = ""; |
| | | |
| | | // å°è¯è·åå·²æçå访æé |
| | | getReturnVisit(row.id) |
| | | .then(res => { |
| | | if (res.code === 200 && res.data) { |
| | | reminderForm.reminderSwitch = res.data.isEnabled === 1; |
| | | reminderForm.reminderContent = res.data.content; |
| | | reminderForm.reminderTime = res.data.reminderTime; |
| | | reminderForm.id = res.data.id; |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error("è·åå访æé失败:", error); |
| | | }); |
| | | |
| | | reminderDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å
³éå访æéå¼¹çª |
| | | const closeReminderDialog = () => { |
| | | proxy.resetForm("reminderFormRef"); |
| | | reminderDialogVisible.value = false; |
| | | }; |
| | | const submitvalue = ref({}); |
| | | |
| | | // æäº¤å访æé |
| | | const submitReminderForm = () => { |
| | | console.log("æäº¤å访æéæ°æ®:", userStore.id, userStore); |
| | | proxy.$refs.reminderFormRef.validate(valid => { |
| | | if (valid) { |
| | | if (reminderForm.id) { |
| | | submitvalue.value = { |
| | | id: reminderForm.id, |
| | | customerId: currentCustomerId.value, |
| | | isEnabled: reminderForm.reminderSwitch ? 1 : 0, |
| | | content: reminderForm.reminderContent, |
| | | reminderTime: reminderForm.reminderTime, |
| | | remindUserId: userStore.id, |
| | | }; |
| | | } else { |
| | | submitvalue.value = { |
| | | customerId: currentCustomerId.value, |
| | | isEnabled: reminderForm.reminderSwitch ? 1 : 0, |
| | | content: reminderForm.reminderContent, |
| | | reminderTime: reminderForm.reminderTime, |
| | | remindUserId: userStore.id, |
| | | }; |
| | | } |
| | | |
| | | console.log("æäº¤å访æéæ°æ®:", submitvalue.value); |
| | | |
| | | // è°ç¨æ¥å£ |
| | | addReturnVisit(submitvalue.value) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | proxy.$modal.msgSuccess("å访æé设置æå"); |
| | | closeReminderDialog(); |
| | | } else { |
| | | proxy.$modal.msgError(res.msg || "设置失败"); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error("设置å访æé失败:", error); |
| | | proxy.$modal.msgError("设置失败"); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | // è·åå½åæ¥æå¹¶æ ¼å¼å为 YYYY-MM-DD |
| | | function getCurrentDate() { |
| | | const today = new Date(); |
| | | const year = today.getFullYear(); |
| | | const month = String(today.getMonth() + 1).padStart(2, "0"); // æä»½ä»0å¼å§ |
| | | const day = String(today.getDate()).padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | } |
| | | // æå¼æ´½è°è¿åº¦å¼¹çª |
| | | const openNegotiationDialog = row => { |
| | | negotiationForm.customerName = row.customerName; |
| | | negotiationForm.customerId = row.id; |
| | | negotiationForm.followUpMethod = ""; |
| | | negotiationForm.followUpLevel = ""; |
| | | negotiationForm.followUpTime = ""; |
| | | negotiationForm.followerUserName = userStore.nickName; // é»è®¤å½åç»å½äºº |
| | | negotiationForm.content = ""; |
| | | // { |
| | | // "customerId": 152, |
| | | // "followUpMethod": "çµè¯æ²é", |
| | | // "followUpLevel": "没ææå", |
| | | // "followUpTime": "2026-03-04T15:30:00", |
| | | // "followerUserName": "管çåè´¦å·", |
| | | // "content": "111" |
| | | // } |
| | | negotiationDialogVisible.value = true; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | // å
³éæ´½è°è¿åº¦å¼¹çª |
| | | const closeNegotiationDialog = () => { |
| | | proxy.resetForm("negotiationFormRef"); |
| | | // æ¸
é¤ç¼è¾ç¶æ |
| | | delete negotiationForm.editIndex; |
| | | delete negotiationForm.id; |
| | | negotiationDialogVisible.value = false; |
| | | }; |
| | | |
| | | // æäº¤æ´½è°è¿åº¦ |
| | | const submitNegotiationForm = () => { |
| | | proxy.$refs.negotiationFormRef.validate(valid => { |
| | | if (valid) { |
| | | // å¤ææ¯æ°å¢è¿æ¯ä¿®æ¹ |
| | | const isEdit = negotiationForm.editIndex !== undefined; |
| | | |
| | | if (isEdit) { |
| | | // ä¿®æ¹æä½ |
| | | console.log("ä¿®æ¹æ´½è°è¿åº¦æ°æ®:", negotiationForm); |
| | | // è¿éå¯ä»¥è°ç¨æ´æ°æ¥å£ |
| | | // å®é
项ç®ä¸éè¦æ ¹æ®å端æ¥å£è¿è¡è°æ´ |
| | | // 示ä¾ï¼updateCustomerFollow(negotiationForm).then(res => { |
| | | // // æ´æ°æ¬å°æ°æ® |
| | | // const index = negotiationForm.editIndex; |
| | | // negotiationRecords.value[index] = { |
| | | // followUpTime: negotiationForm.followUpTime, |
| | | // followUpMethod: negotiationForm.followUpMethod, |
| | | // followUpLevel: negotiationForm.followUpLevel, |
| | | // followerUserName: negotiationForm.followerUserName, |
| | | // content: negotiationForm.content, |
| | | // id: negotiationForm.id, |
| | | // }; |
| | | // proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | // closeNegotiationDialog(); |
| | | // }); |
| | | updateCustomerFollow(negotiationForm).then(res => { |
| | | // æ´æ°æ¬å°æ°æ® |
| | | getCustomer(negotiationForm.customerId).then(res => { |
| | | // æ´æ°æ¬å°æ°æ® |
| | | negotiationRecords.value = res.data.followUpList || []; |
| | | }); |
| | | }); |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | closeNegotiationDialog(); |
| | | } else { |
| | | // æ°å¢æä½ |
| | | console.log("æäº¤æ´½è°è¿åº¦æ°æ®:", negotiationForm); |
| | | addCustomerFollow(negotiationForm).then(res => { |
| | | // æ·»å æååæ´æ°è¯¦æ
页é¢çè¿åº¦è®°å½ |
| | | const newRecord = { |
| | | followUpTime: negotiationForm.followUpTime, |
| | | followUpMethod: negotiationForm.followUpMethod, |
| | | followUpLevel: negotiationForm.followUpLevel, |
| | | followerUserName: negotiationForm.followerUserName, |
| | | content: negotiationForm.content, |
| | | }; |
| | | negotiationRecords.value.unshift(newRecord); |
| | | |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeNegotiationDialog(); |
| | | getList(); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æå¼è¯¦æ
å¼¹çª |
| | | const openDetailDialog = row => { |
| | | // è°ç¨getCustomeræ¥å£è·å客æ·è¯¦æ
|
| | | getCustomer(row.id).then(res => { |
| | | // å¡«å
客æ·åºæ¬ä¿¡æ¯ |
| | | Object.assign(detailForm, res.data); |
| | | |
| | | // è·åæ´½è°è¿åº¦è®°å½ |
| | | negotiationRecords.value = res.data.followUpList || []; |
| | | |
| | | detailDialogVisible.value = true; |
| | | }); |
| | | }; |
| | | |
| | | // å
³é详æ
å¼¹çª |
| | | const closeDetailDialog = () => { |
| | | detailDialogVisible.value = false; |
| | | }; |
| | | |
| | | // ä¿®æ¹æ´½è°è®°å½ |
| | | const editNegotiationRecord = (row, index) => { |
| | | // å°å½åè®°å½æ°æ®å¡«å
å°è¡¨å |
| | | Object.assign(negotiationForm, { |
| | | customerName: row.customerName, |
| | | customerId: row.customerId, |
| | | followUpMethod: row.followUpMethod, |
| | | followUpLevel: row.followUpLevel, |
| | | followUpTime: row.followUpTime, |
| | | followerUserName: row.followerUserName, |
| | | content: row.content, |
| | | id: row.id, // è®°å½IDç¨äºæ´æ° |
| | | editIndex: index, // è®°å½ç´¢å¼ç¨äºæ¬å°æ´æ° |
| | | }); |
| | | negotiationDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å 餿´½è°è®°å½ |
| | | const deleteNegotiationRecord = (row, index) => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¿æ¡æ´½è°è®°å½åï¼", "å é¤æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | // è¿éå¯ä»¥è°ç¨å 餿¥å£ |
| | | // å®é
项ç®ä¸éè¦æ ¹æ®å端æ¥å£è¿è¡è°æ´ |
| | | // 示ä¾ï¼deleteCustomerFollow(row.id).then(() => { |
| | | // negotiationRecords.value.splice(index, 1); |
| | | // proxy.$modal.msgSuccess("å 餿å"); |
| | | // }); |
| | | delCustomerFollow(row.id).then(() => { |
| | | // å 餿ååæ´æ°æ¬å°æ°æ® |
| | | getCustomer(row.customerId).then(res => { |
| | | // æ´æ°æ¬å°æ°æ® |
| | | negotiationRecords.value = res.data.followUpList || []; |
| | | }); |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | }); |
| | | // æ¬å°å é¤ï¼æ¨¡æï¼ |
| | | negotiationRecords.value.splice(index, 1); |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶å é¤"); |
| | | }); |
| | | }; |
| | | |
| | | // æå¼éä»¶å¼¹çª |
| | | const openAttachmentDialog = row => { |
| | | currentFollowRecord.value = row; |
| | | // 转æ¢ä¸ºç¬¦åElement Plus fileListæ ¼å¼çæ°ç» |
| | | currentAttachmentList.value = (row.fileList || []).map((file, index) => ({ |
| | | name: file.fileName, |
| | | url: file.fileUrl, |
| | | size: file.fileSize, |
| | | id: file.id, |
| | | uid: file.id || index, |
| | | status: "success", |
| | | })); |
| | | |
| | | attachmentDialogVisible.value = true; |
| | | }; |
| | | |
| | | // å
³ééä»¶å¼¹çª |
| | | const closeAttachmentDialog = () => { |
| | | attachmentDialogVisible.value = false; |
| | | currentFollowRecord.value = {}; |
| | | currentAttachmentList.value = []; |
| | | }; |
| | | |
| | | // éä»¶ä¸ä¼ æå |
| | | const handleAttachmentSuccess = (response, file, fileList) => { |
| | | if (response.code === 200) { |
| | | proxy.$modal.msgSuccess("ä¸ä¼ æå"); |
| | | // æ´æ°å½åè®°å½çéä»¶å表 |
| | | currentAttachmentList.value = fileList.map(item => ({ |
| | | name: item.name, |
| | | size: item.size, |
| | | url: item.response?.data?.url || item.url, |
| | | id: item.response?.data?.id, |
| | | uid: item.uid, |
| | | status: "success", |
| | | })); |
| | | // æ´æ°åè®°å½ä¸çfilesåæ®µ |
| | | if (currentFollowRecord.value) { |
| | | currentFollowRecord.value.files = [...currentAttachmentList.value]; |
| | | } |
| | | } else { |
| | | proxy.$modal.msgError(response.msg || "ä¸ä¼ 失败"); |
| | | } |
| | | }; |
| | | |
| | | // éä»¶ä¸ä¼ 失败 |
| | | const handleAttachmentError = (error, file, fileList) => { |
| | | console.error("ä¸ä¼ 失败:", error); |
| | | proxy.$modal.msgError("ä¸ä¼ 失败"); |
| | | }; |
| | | |
| | | // éä»¶ç§»é¤ |
| | | const handleAttachmentRemove = (file, fileList) => { |
| | | currentAttachmentList.value = fileList; |
| | | // æ´æ°åè®°å½ä¸çfilesåæ®µ |
| | | if (currentFollowRecord.value) { |
| | | currentFollowRecord.value.files = [...fileList]; |
| | | } |
| | | }; |
| | | |
| | | // éä»¶ä¸ä¼ åæ ¡éª |
| | | const beforeAttachmentUpload = file => { |
| | | const maxSize = 50 * 1024 * 1024; // 50MB |
| | | if (file.size > maxSize) { |
| | | proxy.$modal.msgError("æä»¶å¤§å°ä¸è½è¶
è¿50MB"); |
| | | return false; |
| | | } |
| | | return true; |
| | | }; |
| | | |
| | | // æ ¼å¼åæä»¶å¤§å° |
| | | const formatFileSize = size => { |
| | | if (size < 1024) { |
| | | return size + " B"; |
| | | } else if (size < 1024 * 1024) { |
| | | return (size / 1024).toFixed(2) + " KB"; |
| | | } else { |
| | | return (size / (1024 * 1024)).toFixed(2) + " MB"; |
| | | } |
| | | }; |
| | | |
| | | // ä¸è½½éä»¶ |
| | | const downloadAttachment = row => { |
| | | if (row.url) { |
| | | // proxy.download(row.url, {}, row.name); |
| | | proxy.$download.name(row.url); |
| | | } else { |
| | | proxy.$modal.msgError("ä¸è½½é¾æ¥ä¸åå¨"); |
| | | } |
| | | }; |
| | | |
| | | // å é¤éä»¶ |
| | | const deleteAttachment = (row, index) => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¿ä¸ªéä»¶åï¼", "å é¤æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | // è°ç¨å端æ¥å£å é¤éä»¶ |
| | | const deleteUrl = |
| | | import.meta.env.VITE_APP_BASE_API + |
| | | "/basic/customer-follow/file/" + |
| | | row.id; |
| | | fetch(deleteUrl, { |
| | | method: "DELETE", |
| | | headers: { |
| | | Authorization: "Bearer " + getToken(), |
| | | "Content-Type": "application/json", |
| | | }, |
| | | }) |
| | | .then(response => response.json()) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | // å 餿ååæ´æ°æ¬å°æä»¶å表 |
| | | currentAttachmentList.value.splice(index, 1); |
| | | // æ´æ°åè®°å½ä¸çfilesåæ®µ |
| | | if (currentFollowRecord.value) { |
| | | currentFollowRecord.value.files = [ |
| | | ...currentAttachmentList.value, |
| | | ]; |
| | | } |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | } else { |
| | | proxy.$modal.msgError(res.msg || "å é¤å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error("å é¤é件失败:", error); |
| | | proxy.$modal.msgError("å é¤å¤±è´¥"); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶å é¤"); |
| | | }); |
| | | }; |
| | | |
| | | // è·åå½åæ¥æå¹¶æ ¼å¼å为 YYYY-MM-DD |
| | | function getCurrentDate() { |
| | | const today = new Date(); |
| | | const year = today.getFullYear(); |
| | | const month = String(today.getMonth() + 1).padStart(2, "0"); // æä»½ä»0å¼å§ |
| | | const day = String(today.getDate()).padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"></style> |
| | | <style scoped lang="scss"> |
| | | .detail-section { |
| | | margin-bottom: 20px; |
| | | padding: 15px; |
| | | background-color: #f9f9f9; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 15px; |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | margin: 0 0 15px 0; |
| | | color: #333; |
| | | } |
| | | |
| | | .info-display { |
| | | background-color: #fff; |
| | | padding: 15px; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .info-item { |
| | | margin-bottom: 12px; |
| | | display: flex; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .info-label { |
| | | width: 120px; |
| | | font-weight: 500; |
| | | color: #606266; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .info-value { |
| | | flex: 1; |
| | | color: #303133; |
| | | word-break: break-word; |
| | | } |
| | | |
| | | .no-records { |
| | | text-align: center; |
| | | padding: 30px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .attachment-section { |
| | | .upload-area { |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background-color: #f9f9f9; |
| | | border-radius: 4px; |
| | | border: 1px dashed #d9d9d9; |
| | | |
| | | .el-upload__tip { |
| | | margin-top: 10px; |
| | | color: #909399; |
| | | } |
| | | } |
| | | |
| | | .attachment-list { |
| | | h4 { |
| | | margin: 0 0 10px 0; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | } |
| | | |
| | | .no-attachment { |
| | | text-align: center; |
| | | padding: 40px; |
| | | color: #909399; |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <el-button type="info" plain icon="Upload" @click="handleImport"> |
| | | 导å
¥ |
| | | </el-button> |
| | | <el-dialog v-model="upload.open" :title="upload.title"> |
| | | <FileUpload |
| | | ref="fileUploadRef" |
| | | accept=".xlsx, .xls" |
| | | :headers="upload.headers" |
| | | :action="upload.url + '?updateSupport=' + upload.updateSupport" |
| | | :disabled="upload.isUploading" |
| | | :showTip="false" |
| | | @success="handleFileSuccess" |
| | | /> |
| | | <el-dialog v-model="upload.open" :title="upload.title" @close="handleDialogClose"> |
| | | <FileUpload ref="fileUploadRef" accept=".xlsx, .xls" :headers="upload.headers" :action="uploadUrl" |
| | | :disabled="upload.isUploading" :showTip="true" @success="handleFileSuccess" |
| | | :downloadTemplate="handleDownloadTemplate" /> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitFileForm">ç¡® å®</el-button> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { reactive } from "vue"; |
| | | import { reactive, computed } from "vue"; |
| | | import { getToken } from "@/utils/auth.js"; |
| | | import { FileUpload } from "@/components/Upload"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { downloadProductModelImportTemplate } from "@/api/basicData/product.js"; |
| | | |
| | | defineOptions({ |
| | | name: "产åç»´æ¤å¯¼å
¥", |
| | | }); |
| | | |
| | | const props = defineProps({ |
| | | productId: { type: [String, Number], default: "" }, |
| | | }); |
| | | const emits = defineEmits(["uploadSuccess"]); |
| | | const fileUploadRef = ref(); |
| | | const upload = reactive({ |
| | |
| | | isUploading: false, |
| | | // 设置ä¸ä¼ ç请æ±å¤´é¨ |
| | | headers: { Authorization: "Bearer " + getToken() }, |
| | | // ä¸ä¼ çå°å |
| | | url: import.meta.env.VITE_APP_BASE_API + "/system/supplier/import", |
| | | }); |
| | | // ä¸ä¼ çå°åï¼æºå¸¦ productId åæ°ï¼ä¼ ç»å端ç importProduct æ¥å£ï¼ |
| | | const uploadUrl = computed( |
| | | () => |
| | | import.meta.env.VITE_APP_BASE_API + |
| | | "/basic/product/import" + |
| | | (props.productId ? `?productId=${props.productId}` : "") |
| | | ); |
| | | // ç¹å»å¯¼å
¥ |
| | | const handleImport = () => { |
| | | if (!props.productId) { |
| | | ElMessage({ message: "请å
éæ©äº§å", type: "warning" }); |
| | | return; |
| | | } |
| | | upload.open = true; |
| | | upload.title = "产å导å
¥"; |
| | | }; |
| | |
| | | fileUploadRef.value.uploadApi(); |
| | | }; |
| | | |
| | | // å
³éå¼¹çªæ¶æ¸
é¤å·²éæä»¶ |
| | | const handleDialogClose = () => { |
| | | fileUploadRef.value?.clearFiles?.(); |
| | | }; |
| | | |
| | | const handleFileSuccess = (response) => { |
| | | const { code, msg } = response; |
| | | if (code == 200) { |
| | | ElMessage({ message: "导å
¥æå", type: "success" }); |
| | | ElMessage({ message: msg || "导å
¥æå", type: "success" }); |
| | | upload.open = false; |
| | | emits("uploadSuccess"); |
| | | } else { |
| | | ElMessage({ message: msg, type: "error" }); |
| | | } |
| | | }; |
| | | |
| | | // ä¸è½½ Excel 导å
¥æ¨¡æ¿ |
| | | const handleDownloadTemplate = () => { |
| | | downloadProductModelImportTemplate() |
| | | .then((blobData) => { |
| | | const blob = |
| | | blobData instanceof Blob |
| | | ? blobData |
| | | : new Blob([blobData], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); |
| | | const url = window.URL.createObjectURL(blob); |
| | | const link = document.createElement("a"); |
| | | link.href = url; |
| | | link.download = "产å导å
¥æ¨¡æ¿.xlsx"; |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | document.body.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | ElMessage({ message: "模æ¿ä¸è½½æå", type: "success" }); |
| | | }) |
| | | .catch(() => { |
| | | ElMessage({ message: "模æ¿ä¸è½½å¤±è´¥", type: "error" }); |
| | | }); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .import-tip { |
| | | margin-top: 12px; |
| | | font-size: 12px; |
| | | color: var(--el-text-color-secondary); |
| | | } |
| | | |
| | | .import-tip .el-button { |
| | | margin-left: 8px; |
| | | } |
| | | </style> |
| | |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <el-button @click="close()">åæ¶</el-button> |
| | | <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm"> |
| | | ç¡®å® |
| | | </el-button> |
| | | <el-button @click="close()">åæ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | |
| | | <el-button type="primary" @click="openModelDia('add')"> |
| | | æ°å¢è§æ ¼åå· |
| | | </el-button> |
| | | <ImportExcel @uploadSuccess="getModelList" /> |
| | | <ImportExcel :product-id="currentId" @uploadSuccess="getModelList" /> |
| | | <el-button |
| | | type="danger" |
| | | @click="handleDelete" |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¾åºåç±»åï¼" prop="supplierType"> |
| | | <el-select v-model="form.supplierType" placeholder="è¯·éæ©" clearable> |
| | | <el-option label="ç²" value="ç²" /> |
| | | <el-option label="ä¹" value="ä¹" /> |
| | | <el-option label="ä¸" value="ä¸" /> |
| | | <el-option label="ä¸" value="ä¸" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¯å¦ç½ååï¼" prop="isWhite"> |
| | | <el-select v-model="form.isWhite" placeholder="è¯·éæ©" clearable> |
| | | <el-option label="æ¯" :value="0" /> |
| | |
| | | label: "ä¾åºååç§°", |
| | | prop: "supplierName", |
| | | width: 250, |
| | | }, |
| | | { |
| | | label: "ä¾åºåç±»å", |
| | | prop: "supplierType", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "纳ç¨äººè¯å«å·", |
| | |
| | | contactUserPhone: "", |
| | | maintainUserId: "", |
| | | maintainTime: "", |
| | | supplierType: "", |
| | | isWhite: "", |
| | | }, |
| | | rules: { |
| | |
| | | contactUserPhone: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | maintainUserId: [{ required: false, message: "è¯·éæ©", trigger: "change" }], |
| | | maintainTime: [{ required: false, message: "è¯·éæ©", trigger: "change" }], |
| | | supplierType: [{ required: true, message: "è¯·éæ©ä¾åºåç±»å", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { searchForm, form, rules } = toRefs(data); |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¾åºåç±»åï¼" prop="supplierType"> |
| | | <el-select v-model="form.supplierType" placeholder="è¯·éæ©" clearable> |
| | | <el-option label="ç²" value="ç²" /> |
| | | <el-option label="ä¹" value="ä¹" /> |
| | | <el-option label="ä¸" value="ä¸" /> |
| | | <el-option label="ä¸" value="ä¸" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ¯å¦ç½ååï¼" prop="isWhite"> |
| | | <el-select v-model="form.isWhite" placeholder="è¯·éæ©" clearable> |
| | | <el-option label="æ¯" :value="0" /> |
| | |
| | | label: "ä¾åºååç§°", |
| | | prop: "supplierName", |
| | | width: 250, |
| | | }, |
| | | { |
| | | label: "ä¾åºåç±»å", |
| | | prop: "supplierType", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "纳ç¨äººè¯å«å·", |
| | |
| | | contactUserPhone: "", |
| | | maintainUserId: "", |
| | | maintainTime: "", |
| | | supplierType: "", |
| | | isWhite: "", |
| | | }, |
| | | rules: { |
| | |
| | | contactUserPhone: [{ required: false, message: "请è¾å
¥", trigger: "blur" }], |
| | | maintainUserId: [{ required: false, message: "è¯·éæ©", trigger: "change" }], |
| | | maintainTime: [{ required: false, message: "è¯·éæ©", trigger: "change" }], |
| | | supplierType: [{ required: true, message: "è¯·éæ©ä¾åºåç±»å", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { searchForm, form, rules } = toRefs(data); |
| | |
| | | { |
| | | label: isQuotationType ? "æ¥ä»·åå·" : isPurchaseType ? "éè´ååå·" : "审æ¹äºç±", |
| | | prop: "approveReason", |
| | | width: 200 |
| | | }, |
| | | { |
| | | label: "ç³è¯·äºº", |
| | |
| | | }); |
| | | |
| | | // æä½å |
| | | const actionOperations = [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => |
| | | currentApproveType.value === 5 || |
| | | currentApproveType.value === 6 || |
| | | currentApproveType.value === 7 || |
| | | row.approveStatus == 2 || |
| | | row.approveStatus == 1 || |
| | | row.approveStatus == 4 |
| | | }, |
| | | { |
| | | name: "å®¡æ ¸", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openApprovalDia("approval", row); |
| | | }, |
| | | disabled: (row) => |
| | | row.approveUserCurrentId == null || |
| | | row.approveStatus == 2 || |
| | | row.approveStatus == 3 || |
| | | row.approveStatus == 4 || |
| | | row.approveUserCurrentId !== userStore.id |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openApprovalDia("view", row); |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | // æ¥ä»·å®¡æ¹ï¼ç±»å 6ï¼ä¸å±ç¤ºâéä»¶âæä½ |
| | | if (!isQuotationType) { |
| | | actionOperations.push({ |
| | | name: "éä»¶", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | downLoadFile(row); |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | baseColumns.push({ |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 230, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => |
| | | currentApproveType.value === 5 || |
| | | currentApproveType.value === 6 || |
| | | currentApproveType.value === 7 || |
| | | row.approveStatus == 2 || |
| | | row.approveStatus == 1 || |
| | | row.approveStatus == 4 |
| | | }, |
| | | { |
| | | name: "å®¡æ ¸", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openApprovalDia("approval", row); |
| | | }, |
| | | disabled: (row) => row.approveUserCurrentId == null || row.approveStatus == 2 || row.approveStatus == 3 || row.approveStatus == 4 || row.approveUserCurrentId !== userStore.id |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openApprovalDia('view', row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "éä»¶", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | downLoadFile(row); |
| | | }, |
| | | }, |
| | | ], |
| | | operation: actionOperations, |
| | | }); |
| | | |
| | | return baseColumns; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <el-form :model="searchForm" :inline="true"> |
| | | <el-form-item label="客æ·åç§°ï¼"> |
| | | <el-input |
| | | v-model="searchForm.customerName" |
| | | placeholder="请è¾å
¥å®¢æ·åç§°" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 200px" |
| | | @change="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="æè®¿äººï¼"> |
| | | <el-input |
| | | v-model="searchForm.visitingPeople" |
| | | placeholder="请è¾å
¥æè®¿äºº" |
| | | clearable |
| | | prefix-icon="Search" |
| | | style="width: 200px" |
| | | @change="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery">æç´¢</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | <div class="table_list"> |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | style="width: 100%" |
| | | height="calc(100vh - 18.5em)" |
| | | > |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="客æ·åç§°" prop="customerName" width="150" show-overflow-tooltip /> |
| | | <el-table-column label="è系人" prop="contact" width="120" show-overflow-tooltip /> |
| | | <el-table-column label="èç³»çµè¯" prop="contactPhone" width="140" show-overflow-tooltip /> |
| | | <el-table-column label="æè®¿ç®ç" prop="purposeVisit" width="150" show-overflow-tooltip /> |
| | | <el-table-column label="æè®¿æ¶é´" prop="purposeDate" width="180" show-overflow-tooltip /> |
| | | <el-table-column label="æè®¿å°ç¹" prop="visitAddress" min-width="200" show-overflow-tooltip /> |
| | | <el-table-column label="æè®¿äºº" prop="visitingPeople" width="120" show-overflow-tooltip /> |
| | | <el-table-column fixed="right" label="æä½" width="100" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="viewDetail(scope.row)">æ¥ç</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- 详æ
å¼¹çª --> |
| | | <el-dialog |
| | | v-model="detailVisible" |
| | | title="å®¢æ·æè®¿è®°å½è¯¦æ
" |
| | | width="600px" |
| | | @close="closeDetail" |
| | | > |
| | | <div class="content-container"> |
| | | <!-- 客æ·ä¿¡æ¯ --> |
| | | <div class="section"> |
| | | <div class="section-title">客æ·ä¿¡æ¯</div> |
| | | <div class="info-item"> |
| | | <span class="info-label">客æ·åç§°</span> |
| | | <span class="info-value">{{ detailForm.customerName || '-' }}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">è系人</span> |
| | | <span class="info-value">{{ detailForm.contact || '-' }}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">èç³»çµè¯</span> |
| | | <span class="info-value">{{ detailForm.contactPhone || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æè®¿ä¿¡æ¯ --> |
| | | <div class="section"> |
| | | <div class="section-title">æè®¿ä¿¡æ¯</div> |
| | | <div class="info-item"> |
| | | <span class="info-label">æè®¿ç®ç</span> |
| | | <span class="info-value">{{ detailForm.purposeVisit || '-' }}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">æè®¿æ¶é´</span> |
| | | <span class="info-value">{{ detailForm.purposeDate || '-' }}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">æè®¿å°ç¹</span> |
| | | <span class="info-value multi-line">{{ detailForm.visitAddress || '-' }}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">æè®¿äºº</span> |
| | | <span class="info-value">{{ detailForm.visitingPeople || '-' }}</span> |
| | | </div> |
| | | <div class="info-item" v-if="detailForm.latitude && detailForm.longitude"> |
| | | <span class="info-label">ç»çº¬åº¦</span> |
| | | <span class="info-value">{{ detailForm.latitude }}, {{ detailForm.longitude }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 夿³¨ä¿¡æ¯ --> |
| | | <div class="section"> |
| | | <div class="section-title">夿³¨ä¿¡æ¯</div> |
| | | <div class="info-item remark-item"> |
| | | <span class="info-label">夿³¨</span> |
| | | <span class="info-value multi-line">{{ detailForm.remark || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeDetail">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, getCurrentInstance } from 'vue' |
| | | import pagination from '@/components/PIMTable/Pagination.vue' |
| | | import { getVisitRecords } from '@/api/collaborativeApproval/customerVisit.js' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const tableData = ref([]) |
| | | const tableLoading = ref(false) |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }) |
| | | const total = ref(0) |
| | | |
| | | // æç´¢è¡¨å |
| | | const searchForm = reactive({ |
| | | customerName: '', |
| | | visitingPeople: '', |
| | | }) |
| | | |
| | | // 详æ
ç¸å
³ |
| | | const detailVisible = ref(false) |
| | | const detailForm = ref({}) |
| | | |
| | | // æ¥è¯¢å表 |
| | | const handleQuery = () => { |
| | | page.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | // å页åå |
| | | const paginationChange = (obj) => { |
| | | page.current = obj.page |
| | | page.size = obj.limit |
| | | getList() |
| | | } |
| | | |
| | | // è·ååè¡¨æ°æ® |
| | | const getList = () => { |
| | | tableLoading.value = true |
| | | getVisitRecords({ ...searchForm, ...page }) |
| | | .then((res) => { |
| | | tableLoading.value = false |
| | | if (res.code === 200) { |
| | | tableData.value = res.data?.records || res.records || [] |
| | | total.value = res.data?.total || res.total || 0 |
| | | } else { |
| | | proxy.$modal.msgError(res.msg || 'è·åæ°æ®å¤±è´¥') |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | tableLoading.value = false |
| | | }) |
| | | } |
| | | |
| | | // æ¥ç详æ
|
| | | const viewDetail = (row) => { |
| | | detailForm.value = { ...row } |
| | | detailVisible.value = true |
| | | } |
| | | |
| | | // å
³é详æ
|
| | | const closeDetail = () => { |
| | | detailVisible.value = false |
| | | detailForm.value = {} |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .table_list { |
| | | margin-top: unset; |
| | | } |
| | | |
| | | .content-container { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .section { |
| | | margin-bottom: 24px; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | margin-bottom: 16px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | } |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | margin-bottom: 12px; |
| | | line-height: 1.6; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | &.remark-item { |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | |
| | | .info-label { |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .info-value { |
| | | width: 100%; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .info-label { |
| | | font-weight: 500; |
| | | color: #606266; |
| | | min-width: 100px; |
| | | margin-right: 12px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .info-value { |
| | | color: #303133; |
| | | flex: 1; |
| | | word-break: break-all; |
| | | |
| | | &.multi-line { |
| | | white-space: pre-wrap; |
| | | word-break: break-word; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="time in timeOptions" |
| | | v-for="time in startTimeOptions" |
| | | :key="time.value" |
| | | :label="time.label" |
| | | :value="time.value" |
| | |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="time in timeOptions" |
| | | v-for="time in endTimeOptions" |
| | | :key="time.value" |
| | | :label="time.label" |
| | | :value="time.value" |
| | |
| | | </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' |
| | |
| | | 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) |
| | | |
| | |
| | | // æ¶é´é项ï¼ä»¥åå°æ¶ä¸ºé´éï¼ |
| | | const timeOptions = ref([]) |
| | | |
| | | const getTimeInMinutes = (time) => { |
| | | if (!time) return -1 |
| | | const [hour, minute] = time.split(':').map(Number) |
| | | return hour * 60 + minute |
| | | } |
| | | |
| | | const isToday = (dateText) => { |
| | | if (!dateText) return false |
| | | const [year, month, day] = dateText.split('-').map(Number) |
| | | const now = new Date() |
| | | return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate() |
| | | } |
| | | |
| | | const validateStartTime = (_rule, value, callback) => { |
| | | if (!value) { |
| | | callback() |
| | | return |
| | | } |
| | | |
| | | if (isToday(meetingForm.meetingDate)) { |
| | | const now = new Date() |
| | | const currentMinutes = now.getHours() * 60 + now.getMinutes() |
| | | if (getTimeInMinutes(value) > currentMinutes) { |
| | | callback(new Error('å½å¤©å¼å§æ¶é´ä¸è½æäºå½åæ¶é´')) |
| | | return |
| | | } |
| | | } |
| | | |
| | | callback() |
| | | } |
| | | |
| | | const validateEndTime = (_rule, value, callback) => { |
| | | if (!value || !meetingForm.startTime) { |
| | | callback() |
| | | return |
| | | } |
| | | |
| | | if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) { |
| | | callback(new Error('ç»ææ¶é´å¿
须大äºå¼å§æ¶é´')) |
| | | return |
| | | } |
| | | |
| | | callback() |
| | | } |
| | | |
| | | // è¡¨åæ ¡éªè§å |
| | | const rules = { |
| | | title: [{required: true, message: '请è¾å
¥ä¼è®®ä¸»é¢', trigger: 'blur'}], |
| | | roomId: [{required: true, message: 'è¯·éæ©ä¼è®®å®¤', trigger: 'change'}], |
| | | host: [{required: true, message: '请è¾å
¥ä¸»æäºº', trigger: 'blur'}], |
| | | meetingDate: [{required: true, message: 'è¯·éæ©ä¼è®®æ¥æ', trigger: 'change'}], |
| | | startTime: [ |
| | | {required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}, |
| | | {validator: validateStartTime, trigger: 'change'} |
| | | ], |
| | | endTime: [ |
| | | {required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}, |
| | | {validator: validateEndTime, trigger: 'change'} |
| | | ], |
| | | participants: [{required: true, message: 'è¯·éæ©åä¼äººå', trigger: 'change'}] |
| | | } |
| | | |
| | | const startTimeOptions = computed(() => { |
| | | if (!isToday(meetingForm.meetingDate)) { |
| | | return timeOptions.value |
| | | } |
| | | const now = new Date() |
| | | const currentMinutes = now.getHours() * 60 + now.getMinutes() |
| | | return timeOptions.value.filter(item => getTimeInMinutes(item.value) <= currentMinutes) |
| | | }) |
| | | |
| | | const endTimeOptions = computed(() => { |
| | | if (!meetingForm.startTime) { |
| | | return timeOptions.value |
| | | } |
| | | const startMinutes = getTimeInMinutes(meetingForm.startTime) |
| | | return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes) |
| | | }) |
| | | |
| | | // åå§åæ¶é´é项 |
| | | const initTimeOptions = () => { |
| | | const options = [] |
| | | const now = new Date() |
| | | const currentHour = now.getHours() |
| | | const currentMinute = now.getMinutes() |
| | | // meetingDate æ¯ "yyyy-MM-dd" |
| | | const meetingDate = new Date(meetingForm.meetingDate) |
| | | |
| | | const isSameDay = |
| | | now.getFullYear() === meetingDate.getFullYear() && |
| | | now.getMonth() === meetingDate.getMonth() && |
| | | now.getDate() === meetingDate.getDate() |
| | | |
| | | console.log('æ¯å¦åä¸å¤©:', isSameDay) |
| | | for (let hour = 8; hour <= 18; hour++) { |
| | | // å¼å§æ¶é´å¿
é¡»æäºå½åæ¶é´ |
| | | if (hour < currentHour && isSameDay) { |
| | | continue |
| | | } |
| | | if (hour === currentHour && currentMinute > 30 && isSameDay) { |
| | | continue |
| | | } |
| | | // æ¯ä¸ªå°æ¶æ·»å 两个éé¡¹ï¼æ´ç¹ååç¹ |
| | | options.push({ |
| | | value: `${hour.toString().padStart(2, '0')}:00`, |
| | |
| | | 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) => { |
| | | // ç¦ç¨ä»å¤©ä¹åçæ¥æ |
| | |
| | | :model="form" |
| | | label-width="140px" |
| | | label-position="top" |
| | | :rules="rules" |
| | | :rules="operationType === 'view' ? {} : rules" |
| | | ref="formRef" |
| | | > |
| | | <el-row :gutter="30"> |
| | |
| | | v-model="form.proDesc" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | disabled |
| | | :disabled="operationType === 'view'" |
| | | type="textarea" |
| | | /> |
| | | </el-form-item> |
| | |
| | | </el-row> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | <el-button v-if="operationType === 'approve'" type="primary" @click="submitForm">确认</el-button> |
| | | <el-button v-if="operationType === 'approve'" @click="closeDia">åæ¶</el-button> |
| | | <el-button v-else type="primary" @click="closeDia">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {...row} |
| | | form.value.disposeUserId = userStore.id; |
| | | form.value.disDate = getCurrentDate(); |
| | | if (type === 'approve') { |
| | | if (!form.value.disposeUserId) { |
| | | form.value.disposeUserId = userStore.id; |
| | | } |
| | | if (!form.value.disDate) { |
| | | form.value.disDate = getCurrentDate(); |
| | | } |
| | | } |
| | | } |
| | | // const setName = (code) => { |
| | | // const index = userList.value.findIndex(item => item.deviceModel === code); |
| | |
| | | // } |
| | | // } |
| | | const submitForm = () => { |
| | | if (operationType.value === 'view') { |
| | | closeDia(); |
| | | return; |
| | | } |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | afterSalesServiceDispose(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå") |
| | | closeDia() |
| | | }) |
| | | } |
| | | if (!valid) return; |
| | | afterSalesServiceDispose(form.value).then(() => { |
| | | proxy.$modal.msgSuccess("å¤çæå") |
| | | closeDia() |
| | | }) |
| | | }) |
| | | } |
| | | // å
³éå¼¹æ¡ |
| | |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| | | </style> |
| | |
| | | <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" |
| | |
| | | :upload-method="handleFileUpload" |
| | | :delete-method="handleFileDelete" |
| | | /> |
| | | <el-dialog |
| | | v-model="repairDialogVisible" |
| | | title="维修记å½" |
| | | width="700px" |
| | | destroy-on-close |
| | | @close="repairRecordList = []" |
| | | > |
| | | <el-table |
| | | :data="repairRecordList" |
| | | border |
| | | v-loading="repairRecordLoading" |
| | | max-height="400" |
| | | > |
| | | <el-table-column type="index" label="åºå·" width="55" align="center" /> |
| | | <el-table-column label="ç»´ä¿®æ¥æ" prop="maintenanceTime" min-width="120" show-overflow-tooltip> |
| | | <template #default="{ row }"> |
| | | {{ row.maintenanceTime || row.repairTime || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="维修人" prop="maintenanceName" min-width="100" show-overflow-tooltip> |
| | | <template #default="{ row }"> |
| | | {{ row.maintenanceName || row.repairName || '-' }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç»´ä¿®ç»æ" prop="maintenanceResult" min-width="180" show-overflow-tooltip /> |
| | | </el-table> |
| | | <template #footer> |
| | | <el-button @click="repairDialogVisible = false">å
³é</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {Search} from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue"; |
| | | import FormDia from "@/views/customerService/afterSalesHandling/components/formDia.vue"; |
| | | import FileListDialog from "@/components/Dialog/FileListDialog.vue"; |
| | |
| | | import request from "@/utils/request"; |
| | | import { getToken } from "@/utils/auth"; |
| | | import { |
| | | afterSalesServiceDelete, |
| | | afterSalesServiceListPage, |
| | | afterSalesServiceFileListPage, |
| | | afterSalesServiceFileAdd, |
| | | afterSalesServiceFileDel, |
| | | afterSalesServiceRepairListPage, |
| | | } from "@/api/customerService/index.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | /* |
| | | post_sale_waiting_list æ°å¢çå®ååç±» |
| | | degree_of_urgency æ°å¢çç´§æ¥ç¨åº¦ |
| | | work_order_status 主页çå·¥åç¶æ |
| | | */ |
| | | const { post_sale_waiting_list, degree_of_urgency, work_order_status } = proxy.useDict( |
| | | "post_sale_waiting_list", |
| | | "degree_of_urgency", |
| | | "work_order_status", |
| | | ); |
| | | |
| | | const classificationOptions = computed(() => post_sale_waiting_list?.value || []); |
| | | const degreeOfUrgencyOptions = computed(() => degree_of_urgency?.value || []); |
| | | const workOrderStatusOptions = computed(() => work_order_status?.value || []); |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "å¤çç¶æ", |
| | | prop: "status", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | if (params == 1) { |
| | | return "å¾
å¤ç"; |
| | | } else if (params == 2) { |
| | | return "å·²å¤ç"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | if (params == 1) { |
| | | return "danger"; |
| | | } else if (params == 2) { |
| | | return "success"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | }, |
| | | label: "å·¥åç¼å·", |
| | | prop:"afterSalesServiceNo", |
| | | width: 150, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "éå®åå·", |
| | | prop:"salesContractNo", |
| | | width: 150, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å¤çç¶æ", |
| | | prop: "status", |
| | | dataType: "tag", |
| | | |
| | | formatData: (params) => { |
| | | if (params === 1) { |
| | | return "å¾
å¤ç"; |
| | | } else if (params === 2) { |
| | | return "å·²å¤ç"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | if (params === 1) { |
| | | return "danger"; |
| | | } else if (params === 2) { |
| | | return "success"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å馿¥æ", |
| | | prop: "feedbackDate", |
| | | width: 150, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "ç»è®°äºº", |
| | | prop: "checkNickName", |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "ç´§æ¥ç¨åº¦", |
| | | prop: "urgency", |
| | | // æ ¹æ®degreeOfUrgencyOptionsåå
¸å»èªå¨å¹é
|
| | | formatData: (params) => { |
| | | if (params) { |
| | | const item = degreeOfUrgencyOptions.value.find(item => item.value === params); |
| | | return item?.label || params; |
| | | } |
| | | return null; |
| | | }, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å®åç±»å", |
| | | prop: "serviceType", |
| | | // æ ¹æ®classificationOptionsåå
¸å»èªå¨å¹é
|
| | | formatData: (params) => { |
| | | if (params) { |
| | | const item = classificationOptions.value.find(item => item.value === params); |
| | | return item?.label || params; |
| | | } |
| | | return null; |
| | | }, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å馿¥æ", |
| | | prop: "feedbackDate", |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "ç»è®°äºº", |
| | | prop: "checkNickName", |
| | | }, |
| | | { |
| | | label: "客æ·åç§°", |
| | | prop: "customerName", |
| | | width: 200, |
| | | }, |
| | | { |
| | | label: "é®é¢æè¿°", |
| | | prop: "proDesc", |
| | | width:300 |
| | | }, |
| | | { |
| | | label: "å
³èé¨é¨", |
| | | prop: "deptName", |
| | | width: 200, |
| | | }, |
| | | { |
| | | label: "å¤ç人", |
| | | prop: "disposeNickName", |
| | | }, |
| | | { |
| | | label: "å¤çç»æ", |
| | | prop: "disRes", |
| | | width: 200, |
| | | }, |
| | | { |
| | | label: "å¤çæ¥æ", |
| | | prop: "disDate", |
| | | width: 150, |
| | | }, |
| | | label: "é®é¢æè¿°", |
| | | prop: "proDesc", |
| | | width:300, |
| | | }, |
| | | { |
| | | label: "å¤çç»æ", |
| | | prop: "disRes", |
| | | width:300, |
| | | }, |
| | | { |
| | | label: "å
³èé¨é¨", |
| | | prop: "deptName", |
| | | width: 200, |
| | | align: "center" |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | |
| | | openFilesFormDia(row); |
| | | }, |
| | | }, |
| | | // TODO ä¸ºåæ¥åæ·»å ç |
| | | { |
| | | name: "维修记å½", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openRepairDialog(row); |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | |
| | | const fileListRef = ref(null) |
| | | const fileListDialogVisible = ref(false) |
| | | const currentFileRow = ref(null) |
| | | const repairDialogVisible = ref(false) |
| | | const repairRecordList = ref([]) |
| | | const repairRecordLoading = ref(false) |
| | | |
| | | // æå¼ç»´ä¿®è®°å½å¼¹æ¡ |
| | | const openRepairDialog = async (row) => { |
| | | repairDialogVisible.value = true |
| | | repairRecordLoading.value = true |
| | | repairRecordList.value = [] |
| | | try { |
| | | const res = await afterSalesServiceRepairListPage({ |
| | | afterSalesServiceId: row.id, |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | if (res.code === 200 && res.data?.records) { |
| | | repairRecordList.value = res.data.records |
| | | } |
| | | } catch (error) { |
| | | proxy.$modal.msgError("è·å维修记å½å¤±è´¥") |
| | | } finally { |
| | | repairRecordLoading.value = false |
| | | } |
| | | // éç½® |
| | | const handleReset = () => { |
| | | Object.keys(searchForm.value).forEach(key => { |
| | | searchForm.value[key] = "" |
| | | }) |
| | | } |
| | | |
| | | // æå¼éä»¶å¼¹æ¡----- TODOï¼æ¥å£æ¯æ²¡æå¯¹æ¥çï¼éè¦æ°å¢æ¥å£ï¼ä¸ºåæ¥åæ·»å ç |
| | | // æå¼éä»¶å¼¹æ¡ |
| | | const openFilesFormDia = async (row) => { |
| | | currentFileRow.value = row |
| | | try { |
| | |
| | | try { |
| | | const formData = new FormData() |
| | | formData.append("file", file) |
| | | formData.append("id", currentFileRow.value.id) |
| | | const uploadRes = await request({ |
| | | url: "/file/upload", |
| | | url: "/afterSalesService/file/upload", |
| | | method: "post", |
| | | data: formData, |
| | | headers: { |
| | |
| | | }, |
| | | }) |
| | | if (uploadRes.code === 200) { |
| | | const fileData = { |
| | | proxy.$modal.msgSuccess("æä»¶ä¸ä¼ æå") |
| | | // éæ°è·åæä»¶å表 |
| | | const listRes = await afterSalesServiceFileListPage({ |
| | | afterSalesServiceId: currentFileRow.value.id, |
| | | name: uploadRes.data?.originalName || file.name, |
| | | url: uploadRes.data?.tempPath || uploadRes.data?.url, |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | if (listRes.code === 200 && fileListRef.value) { |
| | | const fileList = (listRes.data?.records || []).map((item) => ({ |
| | | name: item.fileName, |
| | | url: item.fileUrl, |
| | | id: item.id, |
| | | ...item, |
| | | })) |
| | | fileListRef.value.setList(fileList) |
| | | } |
| | | const saveRes = await afterSalesServiceFileAdd(fileData) |
| | | if (saveRes.code === 200) { |
| | | proxy.$modal.msgSuccess("æä»¶ä¸ä¼ æå") |
| | | const listRes = await afterSalesServiceFileListPage({ |
| | | afterSalesServiceId: currentFileRow.value.id, |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | if (listRes.code === 200 && fileListRef.value) { |
| | | const fileList = (listRes.data?.records || []).map((item) => ({ |
| | | name: item.name || item.fileName, |
| | | url: item.url || item.fileUrl, |
| | | id: item.id, |
| | | ...item, |
| | | })) |
| | | fileListRef.value.setList(fileList) |
| | | } |
| | | resolve({ name: fileData.name, url: fileData.url, id: saveRes.data?.id }) |
| | | } else { |
| | | proxy.$modal.msgError(saveRes.msg || "æä»¶ä¿å失败") |
| | | resolve(null) |
| | | } |
| | | resolve({ name: file.name, url: "", id: null }) |
| | | } else { |
| | | proxy.$modal.msgError(uploadRes.msg || "æä»¶ä¸ä¼ 失败") |
| | | resolve(null) |
| | |
| | | // å é¤éä»¶ |
| | | const handleFileDelete = async (row) => { |
| | | try { |
| | | const res = await afterSalesServiceFileDel([row.id]) |
| | | if (res.code === 200) { |
| | | proxy.$modal.msgSuccess("å 餿å") |
| | | if (currentFileRow.value && fileListRef.value) { |
| | | const listRes = await afterSalesServiceFileListPage({ |
| | | afterSalesServiceId: currentFileRow.value.id, |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | if (listRes.code === 200) { |
| | | const fileList = (listRes.data?.records || []).map((item) => ({ |
| | | name: item.name || item.fileName, |
| | | url: item.url || item.fileUrl, |
| | | id: item.id, |
| | | ...item, |
| | | })) |
| | | fileListRef.value.setList(fileList) |
| | | } |
| | | // æ·»å ç¡®è®¤å¯¹è¯æ¡ |
| | | const confirmResult = await ElMessageBox.confirm( |
| | | 'ç¡®å®è¦å é¤è¿ä¸ªéä»¶åï¼', |
| | | 'å é¤ç¡®è®¤', |
| | | { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | } |
| | | } else { |
| | | proxy.$modal.msgError(res.msg || "å é¤å¤±è´¥") |
| | | return false |
| | | ) |
| | | |
| | | if (confirmResult === 'confirm') { |
| | | const res = await afterSalesServiceFileDel(row.id) |
| | | if (res.code === 200) { |
| | | proxy.$modal.msgSuccess("å 餿å") |
| | | if (currentFileRow.value && fileListRef.value) { |
| | | const listRes = await afterSalesServiceFileListPage({ |
| | | afterSalesServiceId: currentFileRow.value.id, |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | if (listRes.code === 200) { |
| | | const fileList = (listRes.data?.records || []).map((item) => ({ |
| | | name: item.fileName, |
| | | url: item.fileUrl, |
| | | id: item.id, |
| | | ...item, |
| | | })) |
| | | fileListRef.value.setList(fileList) |
| | | } |
| | | } |
| | | } else { |
| | | proxy.$modal.msgError(res.msg || "å é¤å¤±è´¥") |
| | | return false |
| | | } |
| | | } |
| | | } catch (error) { |
| | | proxy.$modal.msgError("å é¤å¤±è´¥") |
| | | // å¦æç¨æ·åæ¶å é¤ï¼ä¸æ¾ç¤ºéè¯¯ä¿¡æ¯ |
| | | if (error !== 'cancel') { |
| | | proxy.$modal.msgError("å é¤å¤±è´¥") |
| | | } |
| | | return false |
| | | } |
| | | } |
| | |
| | | }) |
| | | }; |
| | | |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | } else { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤æç¤º", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | tableLoading.value = true; |
| | | afterSalesServiceDelete(ids) |
| | | .then((res) => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | .search-wrapper { |
| | | background: white; |
| | | padding: 1rem 1rem 0 1rem; |
| | | border: 8px; |
| | | border-radius: 16px; |
| | | } |
| | | </style> |
| | |
| | | import {ref, computed} from "vue"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | // import {userListNoPageByTenantId} from "@/api/system/user.js"; // ææ¶æ³¨éæï¼ä½¿ç¨åæ°æ® |
| | | // import {expiryAfterSalesAdd, expiryAfterSalesUpdate} from "@/api/customerService/index.js"; // ææ¶æ³¨éæï¼ä½¿ç¨åæ°æ® |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import {expiryAfterSalesAdd, expiryAfterSalesUpdate} from "@/api/customerService/index.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | const dialogFormVisible = ref(false); |
| | |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | |
| | | // 模æè·åç¨æ·å表 |
| | | userList.value = [ |
| | | { userId: 1, nickName: "å¼ ä¸" }, |
| | | { userId: 2, nickName: "æå" }, |
| | | { userId: 3, nickName: "çäº" }, |
| | | { userId: 4, nickName: "èµµå
" }, |
| | | { userId: 5, nickName: "åå
«" } |
| | | ]; |
| | | // è·åç¨æ·å表 |
| | | userListNoPageByTenantId().then(res => { |
| | | userList.value = res.data; |
| | | }); |
| | | |
| | | if (type === 'add') { |
| | | // æ°å¢æ¶é置表å |
| | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // 模ææäº¤æä½ |
| | | setTimeout(() => { |
| | | console.log("模ææäº¤çæ°æ®:", form.value); |
| | | const submitData = { |
| | | id: form.value.id, |
| | | productName: form.value.productName, |
| | | batchNumber: form.value.batchNumber, |
| | | expireDate: form.value.expiryDate, |
| | | stockQuantity: form.value.stockQuantity, |
| | | customerName: form.value.customerName, |
| | | contactPhone: form.value.contactPhone, |
| | | disRes: form.value.problemDesc, |
| | | status: form.value.status, |
| | | disposeUserId: form.value.handlerId, |
| | | disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName, |
| | | disposeResult: form.value.handleResult, |
| | | disDate: form.value.handleDate |
| | | }; |
| | | |
| | | const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate; |
| | | apiCall(submitData).then(() => { |
| | | proxy.$modal.msgSuccess(operationType.value === 'add' ? "æ°å¢æå" : "æ´æ°æå"); |
| | | closeDia(); |
| | | }, 300); |
| | | }).catch(error => { |
| | | console.error('æäº¤æ°æ®å¤±è´¥:', error); |
| | | proxy.$modal.msgError('æäº¤æ°æ®å¤±è´¥ï¼è¯·ç¨åéè¯'); |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | |
| | | import {onMounted, ref} from "vue"; |
| | | import FormDia from "@/views/customerService/expiryAfterSales/components/formDia.vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | // import {expiryAfterSalesDelete, expiryAfterSalesListPage} from "@/api/customerService/index.js"; // ææ¶æ³¨éæï¼ä½¿ç¨åæ°æ® |
| | | import {expiryAfterSalesDelete, expiryAfterSalesListPage} from "@/api/customerService/index.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore() |
| | |
| | | label: "å¤çç¶æ", |
| | | prop: "status", |
| | | width: "", |
| | | slot: true, |
| | | dataType: "slot", |
| | | slot: "status", |
| | | }, |
| | | { |
| | | label: "å¤ç人", |
| | |
| | | { |
| | | label: "æä½", |
| | | prop: "operation", |
| | | slot: true, |
| | | dataType: "slot", |
| | | slot: "operation", |
| | | width: "200", |
| | | }, |
| | | ], |
| | |
| | | // è·ååè¡¨æ°æ® |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | // åæ¶æ³¨é并使ç¨çå®API |
| | | // expiryAfterSalesListPage({ |
| | | // ...searchForm.value, |
| | | // current: page.value.current, |
| | | // size: page.value.size |
| | | // }).then(res => { |
| | | // tableData.value = res.data.records; |
| | | // page.value.total = res.data.total; |
| | | // tableLoading.value = false; |
| | | // }); |
| | | // æé æ¥è¯¢åæ°ï¼æ å°åç«¯åæ®µå°åç«¯åæ®µ |
| | | const queryParams = { |
| | | expireDate: searchForm.value.expiryDate, |
| | | disDate: searchForm.value.handleDate, |
| | | status: searchForm.value.status, |
| | | current: page.value.current, |
| | | size: page.value.size |
| | | }; |
| | | |
| | | // ææ¶è¿åç©ºæ°æ® |
| | | tableData.value = []; |
| | | page.value.total = 0; |
| | | tableLoading.value = false; |
| | | expiryAfterSalesListPage(queryParams).then(res => { |
| | | // æ å°å端è¿åæ°æ®å°åç«¯è¡¨æ ¼ |
| | | tableData.value = res.data.records.map(item => ({ |
| | | id: item.id, |
| | | productName: item.productName, |
| | | batchNumber: item.batchNumber, |
| | | expiryDate: item.expireDate, |
| | | stockQuantity: item.stockQuantity, |
| | | customerName: item.customerName, |
| | | contactPhone: item.contactPhone, |
| | | problemDesc: item.disRes, |
| | | status: item.status, |
| | | handlerId: item.disposeUserId, |
| | | handlerName: item.disposeNickName, |
| | | handleResult: item.disposeResult, |
| | | handleDate: item.disDate |
| | | })); |
| | | page.value.total = res.data.total; |
| | | tableLoading.value = false; |
| | | }).catch(error => { |
| | | console.error('è·ååè¡¨æ°æ®å¤±è´¥:', error); |
| | | tableLoading.value = false; |
| | | proxy.$modal.msgError('è·åæ°æ®å¤±è´¥ï¼è¯·ç¨åéè¯'); |
| | | }); |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | |
| | | }) |
| | | .then(() => { |
| | | tableLoading.value = true; |
| | | // åæ¶æ³¨é并使ç¨çå®API |
| | | // expiryAfterSalesDelete(ids).then(() => { |
| | | // proxy.$modal.msgSuccess("å 餿å"); |
| | | // getList(); |
| | | // }).finally(() => { |
| | | // tableLoading.value = false; |
| | | // }); |
| | | |
| | | // ææ¶æ¨¡æå 餿å |
| | | tableLoading.value = false; |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | expiryAfterSalesDelete(ids).then(() => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }).finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" title="éæ©äº§å" width="900px" destroy-on-close :close-on-click-modal="false"> |
| | | <el-form :inline="true" :model="query" class="mb-2"> |
| | | <el-form-item label="产ååç±»"> |
| | | <el-input v-model="query.productCategory" placeholder="è¾å
¥äº§ååç±»" clearable @keyup.enter="onSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="åºæ¬åä½"> |
| | | <el-input v-model="query.unit" placeholder="è¾å
¥åºæ¬åä½" clearable @keyup.enter="onSearch" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item> |
| | | <el-button type="primary" @click="onSearch">æç´¢</el-button> |
| | | <el-button @click="columnsDialogVisible = true">åè¡¨åæ®µ</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- å表 --> |
| | | <el-table ref="tableRef" v-loading="loading" :data="tableData" height="420" highlight-current-row :row-key="getRowKey" |
| | | @selection-change="handleSelectionChange"> |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column type="index" label="åºå·" width="60" /> |
| | | <template v-for="column in visibleColumns" :key="column.prop"> |
| | | <el-table-column :prop="column.prop" :label="column.label" :min-width="column.minWidth" show-overflow-tooltip align="center" /> |
| | | </template> |
| | | </el-table> |
| | | |
| | | <div class="mt-3" style="margin-top: 10px;display: flex; justify-content: flex-end;"> |
| | | |
| | | |
| | | <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total" |
| | | v-model:page-size="page.pageSize" v-model:current-page="page.pageNum" :page-sizes="[10, 20, 50, 100]" |
| | | @size-change="onPageChange" @current-change="onPageChange" /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm"> |
| | | ç¡®å® |
| | | </el-button> |
| | | <el-button @click="close()">åæ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="columnsDialogVisible" title="èªå®ä¹æ¾ç¤ºå项" width="600px"> |
| | | <el-checkbox-group v-model="selectedColumns"> |
| | | <el-checkbox v-for="column in allColumns" :key="column.prop" :label="column.prop" :disabled="column.disabled"> |
| | | {{ column.label }} |
| | | </el-checkbox> |
| | | </el-checkbox-group> |
| | | <template #footer> |
| | | <el-button @click="resetColumns">æ¢å¤é»è®¤</el-button> |
| | | <el-button type="primary" @click="saveColumns">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, reactive, ref, watch, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: Boolean, |
| | | single: Boolean, // æ¯å¦åªè½éæ©ä¸ä¸ªï¼é»è®¤falseï¼å¯éæ©å¤ä¸ªï¼ |
| | | products: { |
| | | type: Array, |
| | | default: () => [] |
| | | }, |
| | | selectedIds: { |
| | | type: Array, |
| | | default: () => [] |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'confirm']); |
| | | |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (v) => emit("update:modelValue", v), |
| | | }); |
| | | |
| | | const query = reactive({ |
| | | productCategory: "", |
| | | unit: "", |
| | | }); |
| | | |
| | | const page = reactive({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | }); |
| | | |
| | | const loading = ref(false); |
| | | const tableData = ref([]); |
| | | const total = ref(0); |
| | | const multipleSelection = ref([]); |
| | | const selectedRowMap = ref(new Map()); |
| | | const tableRef = ref(); |
| | | |
| | | const columnsDialogVisible = ref(false); |
| | | |
| | | const allColumns = ref([ |
| | | { prop: 'productCategory', label: '产ååç±»', selected: true, disabled: false }, |
| | | { prop: 'unit', label: 'åºæ¬åä½', selected: true, disabled: false }, |
| | | ]); |
| | | |
| | | const selectedColumns = ref(allColumns.value.filter(c => c.selected).map(c => c.prop)); |
| | | |
| | | const visibleColumns = computed(() => { |
| | | return allColumns.value.filter(c => selectedColumns.value.includes(c.prop)); |
| | | }); |
| | | |
| | | const getRowKey = (row) => { |
| | | return row?.id ?? row?.productModelId ?? `${row?.productCategory || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}`; |
| | | }; |
| | | |
| | | const syncMultipleSelection = () => { |
| | | multipleSelection.value = Array.from(selectedRowMap.value.values()); |
| | | }; |
| | | |
| | | const initSelectionFromProps = () => { |
| | | const selectedIdSet = new Set((props.selectedIds || []).map((id) => String(id))); |
| | | selectedRowMap.value = new Map(); |
| | | if (!selectedIdSet.size) { |
| | | syncMultipleSelection(); |
| | | return; |
| | | } |
| | | (props.products || []).forEach((row) => { |
| | | if (selectedIdSet.has(String(row?.id))) { |
| | | selectedRowMap.value.set(getRowKey(row), row); |
| | | } |
| | | }); |
| | | syncMultipleSelection(); |
| | | }; |
| | | |
| | | const resetColumns = () => { |
| | | selectedColumns.value = allColumns.value.filter(c => c.selected).map(c => c.prop); |
| | | }; |
| | | |
| | | const saveColumns = () => { |
| | | if (selectedColumns.value.length < 1) { |
| | | ElMessage.warning("å表项æ¾ç¤ºä¸å¾å°äº1项"); |
| | | return; |
| | | } |
| | | columnsDialogVisible.value = false; |
| | | }; |
| | | |
| | | function close() { |
| | | visible.value = false; |
| | | } |
| | | |
| | | const handleSelectionChange = (val) => { |
| | | const currentPageKeys = new Set(tableData.value.map((item) => getRowKey(item))); |
| | | currentPageKeys.forEach((key) => selectedRowMap.value.delete(key)); |
| | | |
| | | if (props.single && val.length > 1) { |
| | | const lastSelected = val[val.length - 1]; |
| | | selectedRowMap.value = new Map(); |
| | | if (lastSelected) { |
| | | selectedRowMap.value.set(getRowKey(lastSelected), lastSelected); |
| | | } |
| | | syncMultipleSelection(); |
| | | nextTick(() => { |
| | | if (tableRef.value) { |
| | | tableRef.value.clearSelection(); |
| | | tableRef.value.toggleRowSelection(lastSelected, true); |
| | | } |
| | | }); |
| | | } else if (props.single) { |
| | | selectedRowMap.value = new Map(); |
| | | if (val[0]) { |
| | | selectedRowMap.value.set(getRowKey(val[0]), val[0]); |
| | | } |
| | | syncMultipleSelection(); |
| | | } else { |
| | | val.forEach((row) => { |
| | | selectedRowMap.value.set(getRowKey(row), row); |
| | | }); |
| | | syncMultipleSelection(); |
| | | } |
| | | } |
| | | |
| | | function onSearch() { |
| | | page.pageNum = 1; |
| | | loadData(); |
| | | } |
| | | |
| | | function onReset() { |
| | | query.productCategory = ""; |
| | | query.unit = ""; |
| | | page.pageNum = 1; |
| | | loadData(); |
| | | } |
| | | |
| | | function onPageChange() { |
| | | loadData(); |
| | | } |
| | | |
| | | function onConfirm() { |
| | | if (multipleSelection.value.length === 0) { |
| | | ElMessage.warning("è¯·éæ©ä¸æ¡äº§å"); |
| | | return; |
| | | } |
| | | if (props.single && multipleSelection.value.length > 1) { |
| | | ElMessage.warning("åªè½éæ©ä¸ä¸ªäº§å"); |
| | | return; |
| | | } |
| | | emit("confirm", props.single ? [multipleSelection.value[0]] : multipleSelection.value); |
| | | close(); |
| | | } |
| | | |
| | | async function loadData() { |
| | | loading.value = true; |
| | | try { |
| | | let filtered = props.products || []; |
| | | if (query.productCategory) { |
| | | filtered = filtered.filter(item => item.productCategory && item.productCategory.includes(query.productCategory)); |
| | | } |
| | | if (query.unit) { |
| | | filtered = filtered.filter(item => item.unit && item.unit.includes(query.unit)); |
| | | } |
| | | |
| | | total.value = filtered.length; |
| | | const start = (page.pageNum - 1) * page.pageSize; |
| | | const end = start + page.pageSize; |
| | | tableData.value = filtered.slice(start, end); |
| | | |
| | | nextTick(() => { |
| | | if (tableRef.value) { |
| | | tableRef.value.clearSelection(); |
| | | tableData.value.forEach(row => { |
| | | if (selectedRowMap.value.has(getRowKey(row))) { |
| | | tableRef.value.toggleRowSelection(row, true); |
| | | } |
| | | }); |
| | | } |
| | | syncMultipleSelection(); |
| | | }); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | // çå¬å¼¹çªæå¼ï¼éç½®éæ© |
| | | watch(() => props.modelValue, (visible) => { |
| | | if (visible) { |
| | | initSelectionFromProps(); |
| | | page.pageNum = 1; |
| | | loadData(); |
| | | } |
| | | }); |
| | | |
| | | watch(() => props.products, () => { |
| | | const latestMap = new Map(); |
| | | const currentKeys = new Set(selectedRowMap.value.keys()); |
| | | (props.products || []).forEach((row) => { |
| | | const key = getRowKey(row); |
| | | if (currentKeys.has(key)) { |
| | | latestMap.set(key, row); |
| | | } |
| | | }); |
| | | selectedRowMap.value.forEach((row, key) => { |
| | | if (!latestMap.has(key)) { |
| | | latestMap.set(key, row); |
| | | } |
| | | }); |
| | | selectedRowMap.value = latestMap; |
| | | syncMultipleSelection(); |
| | | if (props.modelValue) { |
| | | loadData(); |
| | | } |
| | | }, { deep: true }); |
| | | |
| | | onMounted(() => { |
| | | loadData() |
| | | }) |
| | | </script> |
| | |
| | | <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> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- éæ©äº§åå¼¹çª --> |
| | | <ProductSelectDialog |
| | | v-model="isShowProductSelectDialog" |
| | | :products="currentSalesOrderProducts" |
| | | :selected-ids="currentSelectedProductIds" |
| | | @confirm="handleSelectProducts" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref} from "vue"; |
| | | import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue"; |
| | | import ProductSelectDialog from "./ProductSelectDialog.vue"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import {afterSalesServiceAdd, afterSalesServiceUpdate} from "@/api/customerService/index.js"; |
| | | import {afterSalesServiceAdd, afterSalesServiceUpdate, getAllCustomerList, getSalesLedger } from "@/api/customerService/index.js"; |
| | | import { getCurrentDate } from "@/utils/index.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const formRef = ref(null) |
| | | const customerNameOptions = ref([]) |
| | | const userStore = useUserStore(); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | feedbackDate: "", |
| | | checkUserId: "", |
| | | customerName: "", |
| | | proDesc: "", |
| | | topic: "", |
| | | serviceType: "", |
| | | urgency: "", |
| | | salesLedgerId: null, |
| | | productModelIds: "", |
| | | customerId: null, |
| | | salesContractNo: "", |
| | | disRes: "", |
| | | customerName: "" |
| | | }, |
| | | rules: { |
| | | customerName: [{required: true, message: "è¯·éæ©å®¢æ·åç§°", trigger: "change"}], |
| | | serviceType: [{required: true, message: "è¯·éæ©å®åç±»å", trigger: "change"}], |
| | | urgency: [{required: true, message: "è¯·éæ©ç´§æ¥ç¨åº¦", trigger: "change"}], |
| | | feedbackDate: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | | checkUserId: [{required: true, message: "è¯·éæ©", trigger: "change"}], |
| | | customerName: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | proDesc: [{required: true, message: "请è¾å
¥", trigger: "blur"}], |
| | | } |
| | | }) |
| | | |
| | | // èªå®ä¹æ ¡éªå½æ°ï¼å¤ææ¯å¦éè¦æ ¡éªå®åç¼å· |
| | | |
| | | const { form, rules } = toRefs(data); |
| | | const userList = ref([]) |
| | | |
| | | const formatCurrency = (val) => { |
| | | if (val === null || val === undefined || val === '') return '-' |
| | | const num = Number(val) |
| | | return Number.isFinite(num) ? num.toFixed(2) : '-' |
| | | } |
| | | |
| | | const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict( |
| | | "post_sale_waiting_list", |
| | | "degree_of_urgency" |
| | | ); |
| | | |
| | | const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []); |
| | | const urgencyOptions = computed(() => degree_of_urgency?.value || []); |
| | | |
| | | const getProductRowId = (row) => { |
| | | return row?.id ?? row?.productModelId ?? row?.modelId ?? `${row?.productCategory || row?.productName || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}` |
| | | } |
| | | |
| | | const normalizeProductRow = (row) => { |
| | | return { |
| | | ...row, |
| | | id: getProductRowId(row), |
| | | productCategory: row?.productCategory ?? row?.productName ?? '', |
| | | specificationModel: row?.specificationModel ?? row?.model ?? '', |
| | | unit: row?.unit ?? '', |
| | | approveStatus: row?.approveStatus ?? null, |
| | | shippingStatus: row?.shippingStatus ?? '', |
| | | expressCompany: row?.expressCompany ?? '', |
| | | expressNumber: row?.expressNumber ?? '', |
| | | shippingCarNumber: row?.shippingCarNumber ?? '', |
| | | shippingDate: row?.shippingDate ?? '', |
| | | quantity: row?.quantity ?? 0, |
| | | taxRate: row?.taxRate ?? 0, |
| | | taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0, |
| | | taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0, |
| | | taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0, |
| | | } |
| | | } |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "产å大类", prop: "productCategory" }, |
| | | { label: "è§æ ¼åå·", prop: "specificationModel" }, |
| | | { label: "åä½", prop: "unit" }, |
| | | { |
| | | label: "产åç¶æ", |
| | | prop: "approveStatus", |
| | | width: 100, |
| | | align: "center", |
| | | dataType: "tag", |
| | | formatData: (v) => (v === 1 ? "å
è¶³" : "ä¸è¶³"), |
| | | formatType: (v) => (v === 1 ? "success" : "danger"), |
| | | }, |
| | | { |
| | | label: "åè´§ç¶æ", |
| | | align: "center", |
| | | width: 140, |
| | | dataType: "slot", |
| | | slot: "shippingStatus", |
| | | }, |
| | | { label: "å¿«éå
¬å¸", prop: "expressCompany", width: 140 }, |
| | | { label: "å¿«éåå·", prop: "expressNumber", width: 160 }, |
| | | { label: "å货车ç", prop: "shippingCarNumber", minWidth: 100, align: "center" }, |
| | | { label: "åè´§æ¥æ", prop: "shippingDate", minWidth: 100, align: "center" }, |
| | | { label: "æ°é", prop: "quantity", width: 100 }, |
| | | { label: "ç¨ç(%)", prop: "taxRate", width: 100 }, |
| | | { |
| | | label: "å«ç¨åä»·(å
)", |
| | | prop: "taxInclusiveUnitPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "å«ç¨æ»ä»·(å
)", |
| | | prop: "taxInclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | label: "ä¸å«ç¨æ»ä»·(å
)", |
| | | prop: "taxExclusiveTotalPrice", |
| | | width: 160, |
| | | formatData: formatCurrency, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "å é¤", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | tableData.value = tableData.value.filter(i => getProductRowId(i) !== getProductRowId(row)) |
| | | }, |
| | | |
| | | }, |
| | | ], |
| | | }, |
| | | ]) |
| | | const tableData = ref([]) |
| | | // éæ©äº§åå¼¹çª |
| | | const isShowProductSelectDialog = ref(false) |
| | | const handleSelectProducts = (rows) => { |
| | | if (!Array.isArray(rows)) return |
| | | const existingIds = new Set(tableData.value.map(i => String(getProductRowId(i)))) |
| | | const mapped = rows |
| | | .map(normalizeProductRow) |
| | | .filter(r => !existingIds.has(String(getProductRowId(r)))) |
| | | tableData.value = tableData.value.concat(mapped) |
| | | } |
| | | const currentSelectedProductIds = computed(() => { |
| | | return tableData.value.map(item => getProductRowId(item)).filter(item => item !== undefined && item !== null && item !== '') |
| | | }) |
| | | |
| | | const associatedSalesOrderNumberChange = () => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | (item) => item.value === form.value.salesContractNo |
| | | ) |
| | | tableData.value = (opt?.productData || []).map(normalizeProductRow) |
| | | form.value.salesLedgerId = opt?.id || null |
| | | } |
| | | |
| | | const associatedSalesOrderNumberOptions = ref([]) |
| | | |
| | | const currentSalesOrderProducts = computed(() => { |
| | | const opt = associatedSalesOrderNumberOptions.value.find( |
| | | (item) => item.value === form.value.salesContractNo |
| | | ) |
| | | return (opt?.productData || []).map(normalizeProductRow) |
| | | }) |
| | | |
| | | const customerNameChange = (val) => { |
| | | const opt = customerNameOptions.value.find(item => item.value === val); |
| | | if (opt) { |
| | | form.value.customerId = opt.id; |
| | | } |
| | | getSalesLedger({ |
| | | customerName: form.value.customerName |
| | | }).then(res => { |
| | | if(res.code === 200){ |
| | | associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({ |
| | | label: item.salesContractNo, |
| | | value: item.salesContractNo, |
| | | productData:item.productData, |
| | | id: item.id |
| | | })) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | const getShippingStatusText = (row) => { |
| | | if (!row) return 'å¾
åè´§' |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return 'å·²åè´§' |
| | | } |
| | | const status = row.shippingStatus |
| | | if (status === null || status === undefined || status === '') { |
| | | return 'å¾
åè´§' |
| | | } |
| | | const map = { |
| | | 'å¾
åè´§': 'å¾
åè´§', |
| | | 'å¾
å®¡æ ¸': 'å¾
å®¡æ ¸', |
| | | 'å®¡æ ¸ä¸': 'å®¡æ ¸ä¸', |
| | | 'å®¡æ ¸æç»': 'å®¡æ ¸æç»', |
| | | 'å®¡æ ¸éè¿': 'å®¡æ ¸éè¿', |
| | | 'å·²åè´§': 'å·²åè´§' |
| | | } |
| | | return map[String(status).trim()] || 'å¾
åè´§' |
| | | } |
| | | |
| | | const getShippingStatusType = (row) => { |
| | | if (!row) return 'info' |
| | | if (row.shippingDate || row.shippingCarNumber) { |
| | | return 'success' |
| | | } |
| | | const status = row.shippingStatus |
| | | if (status === null || status === undefined || status === '') { |
| | | return 'info' |
| | | } |
| | | const map = { |
| | | 'å¾
åè´§': 'info', |
| | | 'å¾
å®¡æ ¸': 'warning', |
| | | 'å®¡æ ¸ä¸': 'warning', |
| | | 'å®¡æ ¸æç»': 'danger', |
| | | 'å®¡æ ¸éè¿': 'success', |
| | | 'å·²åè´§': 'success' |
| | | } |
| | | return map[String(status).trim()] || 'info' |
| | | } |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openDialog = (type, row) => { |
| | | const openDialog =async (type, row) => { |
| | | // 请æ±å¤ä¸ªæ¥å£ï¼è·åæ°æ® |
| | | let res = await getAllCustomerList(); |
| | | if(res.records){ |
| | | customerNameOptions.value = res.records.map(item => ({ |
| | | label: item.customerName, |
| | | value: item.customerName, |
| | | id: item.id |
| | | })); |
| | | } |
| | | |
| | | |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | form.value = {} |
| | | proxy.resetForm("formRef"); |
| | | form.value.checkUserId = userStore.id; |
| | | form.value.feedbackDate = getCurrentDate(); |
| | | // æ°å¢æ¶æ¸
空已éå
³è产å |
| | | if (type === "add") { |
| | | tableData.value = [] |
| | | } |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | if (type === "edit") { |
| | | form.value = {...row} |
| | | if (form.value.customerName) { |
| | | const res = await getSalesLedger({ customerName: form.value.customerName }) |
| | | if (res?.code === 200) { |
| | | console.log(res) |
| | | associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(item => ({ |
| | | label: item.salesContractNo, |
| | | value: item.salesContractNo, |
| | | productData: item.productData, |
| | | id: item.id |
| | | })) |
| | | } |
| | | } |
| | | console.log(form.value) |
| | | } |
| | | } |
| | | // const setName = (code) => { |
| | | // const index = userList.value.findIndex(item => item.deviceModel === code); |
| | | // if (index > -1) { |
| | | // console.log(userList) |
| | | // form.value.name = userList.value[index].deviceName; |
| | | // } |
| | | // } |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | // å¹é
产ååå·IDs |
| | | form.value.productModelIds = tableData.value.map(item => item.id).join(",") |
| | | if (operationType.value === "add") { |
| | | afterSalesServiceAdd(form.value).then(response => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå") |
| | |
| | | }); |
| | | </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> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">å馿¥æï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.feedbackDate" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | @change="handleQuery" |
| | | /> |
| | | <span style="margin-left: 10px;" class="search_title">å¤çç¶æï¼</span> |
| | | <el-select v-model="searchForm.status" placeholder="è¯·éæ©ç¶æ" @change="handleQuery" style="width: 140px" clearable> |
| | | <el-option label="å¾
å¤ç" :value="1"></el-option> |
| | | <el-option label="å·²å¤ç" :value="2"></el-option> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px" |
| | | >æç´¢</el-button |
| | | > |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | ></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | </div> |
| | | <div class="app-container"> |
| | | <div class="workorder-stats"> |
| | | <div |
| | | v-for="(item, index) in statsList" |
| | | :key="index" |
| | | class="stat-card" |
| | | > |
| | | <div class="stat-icon" :style="{ backgroundColor: item.bgColor }"> |
| | | <el-icon :color="item.color" :size="20"> |
| | | <component :is="item.icon" /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="stat-info"> |
| | | <div class="stat-number">{{ item.count }}</div> |
| | | <div class="stat-label">{{ item.label }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="search-wrapper"> |
| | | <el-form |
| | | :model="searchForm" |
| | | class="demo-form-inline" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-input |
| | | v-model="searchForm.afterSalesServiceNo" |
| | | placeholder="请è¾å
¥å·¥åç¼å·" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-select |
| | | v-model="searchForm.status" |
| | | placeholder="è¯·éæ©å·¥åç¶æ" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="dict in workOrderStatusOptions" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-select |
| | | v-model="searchForm.urgency" |
| | | placeholder="è¯·éæ©ç´§æ¥ç¨åº¦" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="dict in degreeOfUrgencyOptions" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-select |
| | | v-model="searchForm.serviceType" |
| | | placeholder="è¯·éæ©å®åç±»å" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="dict in classificationOptions" |
| | | :key="dict.value" |
| | | :label="dict.label" |
| | | :value="dict.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-input |
| | | v-model="searchForm.orderNo" |
| | | placeholder="请è¾å
¥éå®åå·" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | |
| | | |
| | | <!-- æé® --> |
| | | <el-col :span="4"> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery"> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="handleReset"> |
| | | éç½® |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </div> |
| | | <div class="table_list"> |
| | | <div class="table_header" style="display: flex; justify-content: space-between; align-items: center;"> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢å®åå</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :height="tableHeight" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | ></PIMTable> |
| | | </div> |
| | | <form-dia ref="formDia" @close="handleQuery"></form-dia> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {Search} from "@element-plus/icons-vue"; |
| | | import {onMounted, ref, getCurrentInstance, nextTick} from "vue"; |
| | | import {onMounted, reactive, ref, toRefs, computed, getCurrentInstance, nextTick} from "vue"; |
| | | import FormDia from "@/views/customerService/feedbackRegistration/components/formDia.vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | | import {afterSalesServiceDelete, afterSalesServiceListPage} from "@/api/customerService/index.js"; |
| | | import {afterSalesServiceDelete, afterSalesServiceListPage, getSalesLedgerDetail} from "@/api/customerService/index.js"; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore() |
| | | import { Document, FolderOpened, UserFilled } from "@element-plus/icons-vue" |
| | | import { markRaw } from 'vue' |
| | | |
| | | const statsList = ref([ |
| | | { |
| | | icon: markRaw(Document), |
| | | count: 0, |
| | | label: 'å
¨é¨å·¥å', |
| | | color: '#4080ff', |
| | | bgColor: '#eaf2ff' |
| | | }, |
| | | { |
| | | icon: markRaw(FolderOpened), |
| | | count: 0, |
| | | label: 'å·²å¤ç', |
| | | color: '#ff9a2e', |
| | | bgColor: '#fff5e6' |
| | | }, |
| | | { |
| | | icon: markRaw(UserFilled), |
| | | count: 0, |
| | | label: '已宿', |
| | | color: '#00b42a', |
| | | bgColor: '#e6f7ed' |
| | | }, |
| | | ]) |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | feedbackDate: "", |
| | | }, |
| | | searchForm : { |
| | | customerName: "", |
| | | status: "", |
| | | urgency: "", |
| | | serviceType: "", |
| | | reviewStatus: "", |
| | | orderNo: "", |
| | | } |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "å¤çç¶æ", |
| | | prop: "status", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | if (params == 1) { |
| | | return "å¾
å¤ç"; |
| | | } else if (params == 2) { |
| | | return "å·²å¤ç"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | formatType: (params) => { |
| | | if (params == 1) { |
| | | return "danger"; |
| | | } else if (params == 2) { |
| | | return "success"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | }, |
| | | { |
| | | label: "å馿¥æ", |
| | | prop: "feedbackDate", |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "ç»è®°äºº", |
| | | prop: "checkNickName", |
| | | }, |
| | | { |
| | | label: "客æ·åç§°", |
| | | prop: "customerName", |
| | | width: 200, |
| | | }, |
| | | { |
| | | label: "é®é¢æè¿°", |
| | | prop: "proDesc", |
| | | width:300 |
| | | }, |
| | | { |
| | | label: "å
³èé¨é¨", |
| | | prop: "deptName", |
| | | width: 200, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => { |
| | | return row.status !== 1 |
| | | } |
| | | }, |
| | | ], |
| | | }, |
| | | { |
| | | label: "å·¥åç¼å·", |
| | | prop:"afterSalesServiceNo", |
| | | width: 150, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "éå®åå·", |
| | | prop:"salesContractNo", |
| | | width: 150, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å¤çç¶æ", |
| | | prop: "status", |
| | | dataType: "tag", |
| | | |
| | | formatData: (params) => { |
| | | if (params) { |
| | | let part = String(params) |
| | | const item = workOrderStatusOptions.value.find(item => item.value === part); |
| | | return item?.label || params; |
| | | } |
| | | return null; |
| | | }, |
| | | formatType: (params) => { |
| | | if (params === 1) { |
| | | return "danger"; |
| | | } else if (params === 2) { |
| | | return "success"; |
| | | } else { |
| | | return null; |
| | | } |
| | | }, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å馿¥æ", |
| | | prop: "feedbackDate", |
| | | width: 150, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "ç»è®°äºº", |
| | | prop: "checkNickName", |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "ç´§æ¥ç¨åº¦", |
| | | prop: "urgency", |
| | | // æ ¹æ®degreeOfUrgencyOptionsåå
¸å»èªå¨å¹é
|
| | | formatData: (params) => { |
| | | if (params) { |
| | | const item = degreeOfUrgencyOptions.value.find(item => item.value === params); |
| | | return item?.label || params; |
| | | } |
| | | return null; |
| | | }, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å®åç±»å", |
| | | prop: "serviceType", |
| | | // æ ¹æ®classificationOptionsåå
¸å»èªå¨å¹é
|
| | | formatData: (params) => { |
| | | if (params) { |
| | | const item = classificationOptions.value.find(item => item.value === params); |
| | | return item?.label || params; |
| | | } |
| | | return null; |
| | | }, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "é®é¢æè¿°", |
| | | prop: "disRes", |
| | | width:300, |
| | | }, |
| | | { |
| | | label: "å
³èé¨é¨", |
| | | prop: "deptName", |
| | | width: 200, |
| | | align: "center" |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | console.log(row) |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => { |
| | | return row.status !== 1 |
| | | } |
| | | }, |
| | | ], |
| | | align: "center" |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | current: 1, |
| | | size: 100, |
| | | total: 0, |
| | | }); |
| | | const selectedRows = ref([]); |
| | | const tableHeight = computed(() => "calc(100% -80px)"); |
| | | |
| | | const handleReset = () => { |
| | | Object.keys(searchForm.value).forEach(key => { |
| | | searchForm.value[key] = "" |
| | | }) |
| | | page.current = 1; |
| | | getList(); |
| | | } |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | selectedRows.value = selection; |
| | | }; |
| | | const formDia = ref() |
| | | |
| | | // åå
¸è·å |
| | | /* |
| | | post_sale_waiting_list æ°å¢çå®ååç±» |
| | | degree_of_urgency æ°å¢çç´§æ¥ç¨åº¦ |
| | | work_order_status 主页çå·¥åç¶æ |
| | | review_status é¦é¡µçå®¡æ ¸ç¶æ |
| | | */ |
| | | const { post_sale_waiting_list, degree_of_urgency, work_order_status, review_status } = proxy.useDict( |
| | | "post_sale_waiting_list", |
| | | "degree_of_urgency", |
| | | "work_order_status", |
| | | "review_status" |
| | | ); |
| | | |
| | | const classificationOptions = computed(() => post_sale_waiting_list?.value || []); |
| | | const degreeOfUrgencyOptions = computed(() => degree_of_urgency?.value || []); |
| | | const workOrderStatusOptions = computed(() => work_order_status?.value || []); |
| | | |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | afterSalesServiceListPage({ ...searchForm.value, ...page }).then((res) => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }); |
| | | tableLoading.value = true; |
| | | getSalesLedgerDetails() |
| | | afterSalesServiceListPage({ ...searchForm.value, ...page }).then((res) => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }); |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row) |
| | | }) |
| | | }; |
| | | |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | // æ£æ¥æ¯å¦æä»äººç»´æ¤çæ°æ® |
| | | const unauthorizedData = selectedRows.value.filter(item => item.checkUserId !== userStore.id); |
| | | if (unauthorizedData.length > 0) { |
| | | proxy.$modal.msgWarning("ä¸å¯å é¤ä»äººç»´æ¤çæ°æ®"); |
| | | return; |
| | | } |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | } else { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤æç¤º", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | tableLoading.value = true; |
| | | afterSalesServiceDelete(ids) |
| | | .then((res) => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | function handleDelete() { |
| | | let ids = []; |
| | | if (selectedRows.value.length > 0) { |
| | | // æ£æ¥æ¯å¦æä»äººç»´æ¤çæ°æ® |
| | | const unauthorizedData = selectedRows.value.filter(item => item.checkUserId !== userStore.id); |
| | | if (unauthorizedData.length > 0) { |
| | | proxy.$modal.msgWarning("ä¸å¯å é¤ä»äººç»´æ¤çæ°æ®"); |
| | | return; |
| | | } |
| | | ids = selectedRows.value.map((item) => item.id); |
| | | } else { |
| | | proxy.$modal.msgWarning("è¯·éæ©æ°æ®"); |
| | | return; |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤æç¤º", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | tableLoading.value = true; |
| | | afterSalesServiceDelete(ids) |
| | | .then(() => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleOut = () => { |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/afterSalesService/export", {}, "åé¦ç»è®°.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/afterSalesService/export", {}, "åé¦ç»è®°.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // è·åç»è®¡æ°æ®å¹¶å·æ°é¡¶é¨å¡ç |
| | | const getSalesLedgerDetails = () => { |
| | | getSalesLedgerDetail({}).then((res) => { |
| | | if (res.code === 200) { |
| | | statsList.value[0].count = res.data.filter((item) => item.status === 3)[0].count; |
| | | statsList.value[1].count = res.data.filter((item) => item.status === 2)[0].count; |
| | | statsList.value[2].count = res.data.filter((item) => item.status === 1)[0].count; |
| | | |
| | | // }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | getList(); |
| | | |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | <style scoped lang="scss"> |
| | | .search-wrapper { |
| | | background: white; |
| | | padding: 1rem 1rem 0 1rem; |
| | | border: 8px; |
| | | border-radius: 16px; |
| | | } |
| | | |
| | | </style> |
| | | .expand-btn { |
| | | width: 100%; |
| | | padding: 20px; /* ä¸ä¸å·¦å³å20pxï¼ç¹å»è¿ä¸ªèå´é½è½è§¦åäºä»¶ */ |
| | | cursor: pointer; /* é¼ æ æ¬æµ®æ¾ç¤ºæåï¼æåä½éª */ |
| | | text-align: center; |
| | | } |
| | | |
| | | .workorder-stats { |
| | | display: flex; |
| | | gap: 16px; |
| | | padding-bottom:1rem; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .stat-card { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 20px; |
| | | background-color: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06); |
| | | } |
| | | |
| | | .stat-icon { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 48px; |
| | | height: 48px; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .stat-info { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .stat-number { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | line-height: 1; |
| | | } |
| | | .table_header{ |
| | | padding-bottom: 10px; |
| | | } |
| | | |
| | | .table_list { |
| | | height: calc(100vh - 380px); |
| | | min-height: 360px; |
| | | background: #fff; |
| | | margin-top: 20px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | :deep(.table_list .pagination-container) { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | align-items: center; |
| | | margin-top: auto; |
| | | padding: 12px 0 0; |
| | | } |
| | | |
| | | :deep(.table_list .el-pagination) { |
| | | flex-wrap: nowrap; |
| | | justify-content: flex-end; |
| | | width: 100%; |
| | | } |
| | | </style> |
| | |
| | | <el-button type="primary" @click="addCategory" >æ°å¢</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="columns" |
| | | :tableData="renderTableData" |
| | | :tableLoading="loading" |
| | | :page="pagination" |
| | | :isShowPagination="true" |
| | | @pagination="handleSizeChange" |
| | | > |
| | | <template #status="{ row }"> |
| | | <el-tag type="success" size="small">{{ row.status }}</el-tag> |
| | | </template> |
| | | </PIMTable> |
| | | |
| | | <div class="table_list"> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="renderTableData" |
| | | style="width: 100%; margin-top: 10px;" |
| | | border |
| | | row-key="id" |
| | | > |
| | | <el-table-column prop="deviceNameStr" label="设å¤åç§°" width="300"></el-table-column> |
| | | <el-table-column prop="name" label="å¤ä»¶åç§°" width="200"></el-table-column> |
| | | <el-table-column prop="sparePartsNo" label="å¤ä»¶ç¼å·" width="200"></el-table-column> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="{ row }"> |
| | | <el-tag type="success" size="small">{{ row.status }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="price" label="ä»·æ ¼" width="140"></el-table-column> |
| | | <el-table-column prop="quantity" label="æ°é" width="140"></el-table-column> |
| | | <el-table-column prop="description" label="æè¿°"></el-table-column> |
| | | <el-table-column label="æä½" width="150" fixed="right" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | @click="() => editCategory(row)" |
| | | :disabled="loading" |
| | | > |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button |
| | | link |
| | | @click="() => deleteCategory(row.id)" |
| | | style="color: #f56c6c;" |
| | | :disabled="loading" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <!-- å页ç»ä»¶ --> |
| | | <div class="pagination-container"> |
| | | <el-pagination |
| | | v-model:current-page="pagination.current" |
| | | v-model:page-size="pagination.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="pagination.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | /> |
| | | </div> |
| | | </div> |
| | | <el-dialog title="å类管ç" v-model="dialogVisible" width="60%"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> |
| | | <el-form-item label="设å¤" prop="deviceLedgerIds"> |
| | |
| | | size: 10, |
| | | total: 0 |
| | | }); |
| | | const columns = ref([ |
| | | { |
| | | label: "设å¤åç§°", |
| | | prop: "deviceNameStr", |
| | | }, |
| | | { |
| | | label: "å¤ä»¶åç§°", |
| | | prop: "name", |
| | | }, |
| | | { |
| | | label: "å¤ä»¶ç¼å·", |
| | | prop: "sparePartsNo", |
| | | }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "status", |
| | | slot: "status", |
| | | dataType: "slot", |
| | | }, |
| | | { |
| | | label: "ä»·æ ¼", |
| | | prop: "price", |
| | | }, |
| | | { |
| | | label: "æ°é", |
| | | prop: "quantity", |
| | | }, |
| | | { |
| | | label: "æè¿°", |
| | | prop: "description", |
| | | }, |
| | | { |
| | | label: "æä½", |
| | | prop: "operation", |
| | | width: 150, |
| | | fixed: 'right', |
| | | align: "center", |
| | | dataType: "action", |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | clickFun: (row) => { |
| | | editCategory(row) |
| | | }, |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | clickFun: (row) => { |
| | | deleteCategory(row.id) |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | // è¡¨åæ°æ® |
| | | const form = reactive({ |
| | | id:'', |
| | |
| | | form.status = ''; |
| | | form.description = ''; |
| | | form.deviceLedgerIds = []; |
| | | form.quantity = undefined; |
| | | form.price = null; |
| | | operationType.value = 'add' |
| | | dialogVisible.value = true; |
| | |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | total: pagination.total, |
| | | layout: 'total, sizes, prev, pager, next, jumper' |
| | | }" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="handlePagination" |
| | |
| | | |
| | | // æå»ºæ¥è¯¢åæ° |
| | | const query = { |
| | | page: pagination.currentPage, |
| | | current: pagination.currentPage, |
| | | size: pagination.pageSize, |
| | | documentClassificationId:currentId.value |
| | | documentClassificationId: currentId.value |
| | | }; |
| | | |
| | | const res = await getDocumentList(query); |
| | |
| | | }; |
| | | |
| | | // å¤çå页åå |
| | | const handlePagination = (current, size) => { |
| | | pagination.currentPage = current; |
| | | pagination.pageSize = size; |
| | | const handlePagination = (payload) => { |
| | | // PIMTable emit: { page, limit } |
| | | pagination.currentPage = payload?.page || 1; |
| | | pagination.pageSize = payload?.limit || pagination.pageSize; |
| | | loadDocumentList(); |
| | | }; |
| | | |
| | |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | > |
| | | <el-option :label="item.label" :value="item.value" v-for="(item,index) in income_types" :key="index" /> |
| | | <el-option :label="item.label" :value="item.value" v-for="(item,index) in income_types.filter(item => item.value != 3)" :key="index" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="客æ·åç§°" prop="customerName"> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="visible" title="æ¶æ¬¾/鿬¾" width="90%" append-to-body> |
| | | <div class="section"> |
| | | <div class="section-title descriptions">åºç¡èµæ</div> |
| | | <el-form :model="form" label-width="100px"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="åæ®ç¼å·"> |
| | | <el-input v-model="form.billNo" placeholder="使ç¨ç³»ç»ç¼å·" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="客æ·"> |
| | | <el-select v-model="form.customerId" placeholder="è¯·éæ©"> |
| | | <el-option v-for="c in customerOptions" :key="c.value" :label="c.label" :value="c.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å¶å人"> |
| | | <el-select v-model="form.makerId" placeholder="è¯·éæ©"> |
| | | <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å¶åæ¥æ"> |
| | | <el-date-picker v-model="form.makeDate" type="date" value-format="YYYY-MM-DD" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="ç³è¯·é¨é¨"> |
| | | <el-select v-model="form.applyDeptId" placeholder="è¯·éæ©"> |
| | | <el-option v-for="d in deptOptions" :key="d.value" :label="d.label" :value="d.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="夿³¨"> |
| | | <el-input v-model="form.remark" maxlength="100" show-word-limit placeholder="请è¾å
¥" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="éä»¶"> |
| | | <el-upload :action="uploadUrl" :headers="uploadHeaders" name="files" :on-success="onUploadSuccess"> |
| | | <el-button>ä¸ä¼ æä»¶</el-button> |
| | | </el-upload> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <div class="section"> |
| | | <div class="toolbar"> |
| | | <div class="section-title descriptions">仿¬¾å表</div> |
| | | <el-input v-model="form.discountAmount" placeholder="伿 éé¢" style="width:240px" /> |
| | | </div> |
| | | <el-table :data="form.paymentList" border> |
| | | <el-table-column label="仿¬¾è´¦å·" minWidth="160"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.accountNo" placeholder="请è¾å
¥" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="仿¬¾è´¦å·åç§°" minWidth="180"> |
| | | <template #default="scope"> |
| | | <el-select v-model="scope.row.accountName" placeholder="è¯·éæ©"> |
| | | <el-option v-for="a in accountOptions" :key="a.value" :label="a.label" :value="a.label" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="仿¬¾æ¹å¼" minWidth="140"> |
| | | <template #default="scope"> |
| | | <el-select v-model="scope.row.payMethod" placeholder="è¯·éæ©"> |
| | | <el-option v-for="m in payMethodOptions" :key="m.value" :label="m.label" :value="m.value" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å®é
仿¬¾éé¢" minWidth="160"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.amount" placeholder="请è¾å
¥" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æç»è´¹" minWidth="140"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.fee" placeholder="请è¾å
¥" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="交æå·/票æ®å·" minWidth="180"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.txNo" placeholder="请è¾å
¥" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="夿³¨" minWidth="200"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.remark" maxlength="30" show-word-limit placeholder="请è¾å
¥" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" minWidth="120" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="addPayment">æ°å¢ä¸è¡</el-button> |
| | | <el-button link type="danger" @click="removePayment(scope.$index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="summary">å计</div> |
| | | </div> |
| | | |
| | | <div class="section"> |
| | | <div class="section-container"> |
| | | <div class="section-title descriptions">æºåä¿¡æ¯</div> |
| | | <div class="source-toolbar"> |
| | | <el-button @click="clearSource">æ¸
空</el-button> |
| | | <el-button @click="selectSource">éæ©æºå</el-button> |
| | | <el-button type="primary" @click="autoWriteOff">èªå¨æ ¸é</el-button> |
| | | </div> |
| | | </div> |
| | | <el-table :data="form.sourceList" border> |
| | | <el-table-column label="åæ®æ¥æ" minWidth="160" prop="billDate" /> |
| | | <el-table-column label="åæ®ç±»å" minWidth="160" prop="billType" /> |
| | | <el-table-column label="åæ®ç¼å·" minWidth="200" prop="billNo" /> |
| | | <el-table-column label="åæ®éé¢" minWidth="120" prop="billAmount" /> |
| | | <el-table-column label="å·²æ ¸ééé¢" minWidth="120" prop="wroteAmount" /> |
| | | <el-table-column label="æªæ ¸ééé¢" minWidth="120" prop="unWroteAmount" /> |
| | | <el-table-column label="æ¬æ¬¡æ ¸ééé¢" minWidth="160"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.thisWriteOffAmount" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="100" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button link type="danger" @click="removeSource(scope.$index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="summary">å计</div> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <el-button type="primary" @click="submit">确认</el-button> |
| | | <el-button @click="visible=false">åæ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue'; |
| | | import { getToken } from '@/utils/auth'; |
| | | |
| | | const visible = ref(false); |
| | | const form = ref({ |
| | | billNo: '', |
| | | customerId: undefined, |
| | | makerId: undefined, |
| | | makeDate: '', |
| | | applyDeptId: undefined, |
| | | remark: '', |
| | | discountAmount: '', |
| | | paymentList: [{ accountNo: '', accountName: '', payMethod: '', amount: '', fee: '', txNo: '', remark: '' }], |
| | | sourceList: [{ billDate: '', billType: '', billNo: '', billAmount: 0, wroteAmount: 0, unWroteAmount: 0, thisWriteOffAmount: '' }] |
| | | }); |
| | | |
| | | const customerOptions = ref([]); |
| | | const userOptions = ref([]); |
| | | const deptOptions = ref([]); |
| | | const accountOptions = ref([]); |
| | | const payMethodOptions = ref([]); |
| | | |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload'; |
| | | const uploadHeaders = { Authorization: 'Bearer ' + getToken() }; |
| | | |
| | | function addPayment() { |
| | | form.value.paymentList.push({ accountNo: '', accountName: '', payMethod: '', amount: '', fee: '', txNo: '', remark: '' }); |
| | | } |
| | | function removePayment(i) { |
| | | form.value.paymentList.splice(i, 1); |
| | | } |
| | | function removeSource(i) { |
| | | form.value.sourceList.splice(i, 1); |
| | | } |
| | | function clearSource() { |
| | | form.value.sourceList = []; |
| | | } |
| | | function selectSource() {} |
| | | function autoWriteOff() {} |
| | | function onUploadSuccess() {} |
| | | |
| | | function open(payload) { |
| | | visible.value = true; |
| | | } |
| | | function submit() { |
| | | visible.value = false; |
| | | emit('submitted'); |
| | | } |
| | | |
| | | defineExpose({ open }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .section { background: #fff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); padding: 16px; margin-bottom: 16px; } |
| | | .section-title { font-weight: 600; margin-bottom: 12px; } |
| | | .descriptions { |
| | | margin-bottom: 20px; |
| | | display: inline-block; |
| | | font-size: 1rem; |
| | | font-weight: 600; |
| | | padding-left: 12px; |
| | | position: relative; |
| | | } |
| | | .descriptions::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 1rem; |
| | | background-color: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | .toolbar { margin-bottom: 10px; display: flex; justify-content: space-between; |
| | | align-items: center; } |
| | | .source-toolbar { margin-bottom: 10px; display: flex; gap: 8px; } |
| | | .summary { padding: 8px 12px; background: #fff7e6; color: #ad6800; } |
| | | .section-container{display: flex;align-items: center;justify-content: space-between; } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 使ç¨å
Œ
±æç´¢ç»ä»¶ --> |
| | | <SearchPanel |
| | | v-model="queryParams" |
| | | :schema="searchSchema" |
| | | @search="handleQuery" |
| | | @reset="resetQuery" |
| | | /> |
| | | |
| | | <!-- è¡¨æ ¼åºå --> |
| | | <el-card class="table-card"> |
| | | <el-table :data="refundList" v-loading="loading" border> |
| | | <el-table-column label="éè´§åå·" prop="returnManagementNo" align="center" /> |
| | | <el-table-column label="客æ·åç§°" prop="customerName" align="center" /> |
| | | <el-table-column label="éå®åå·" prop="salesContractNo" align="center" /> |
| | | <el-table-column label="åºé款éé¢" prop="refundAmount" align="center" /> |
| | | <el-table-column label="已鿬¾éé¢" prop="refundedAmount" align="center" /> |
| | | <el-table-column label="æªé款éé¢" prop="notRefundedAmount" align="center" /> |
| | | <el-table-column label="ç¶æ" prop="status" align="center"> |
| | | <template #default="scope"> |
| | | <dict-tag :options="dictRef.sales_refund_status.value" :value="scope.row.status" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å建人" prop="createUserName" align="center" /> |
| | | <el-table-column label="å建æ¶é´" prop="createTime" align="center" /> |
| | | <el-table-column label="æä½" align="center" width="150"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="openDetail(scope.row)">详æ
</el-button> |
| | | <el-button link type="primary" @click="openConfirm(scope.row)">确认</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | v-model:page="queryParams.pageNum" |
| | | v-model:limit="queryParams.pageSize" |
| | | @pagination="getList" |
| | | /> |
| | | </el-card> |
| | | <ReceiptandRefundPopupWindow ref="popupRef" @submitted="getList" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="SalesRefund"> |
| | | import { ref, reactive, onMounted, computed, getCurrentInstance } from 'vue'; |
| | | const { proxy } = getCurrentInstance(); |
| | | import { listPage, add, update, del } from '@/api/financialManagement/salesRefund'; |
| | | import SearchPanel from '@/components/SearchPanel/index.vue'; |
| | | import ReceiptandRefundPopupWindow from './components/ReceiptandRefundPopupWindow.vue'; |
| | | |
| | | // æ¥è¯¢åæ° |
| | | const queryParams = reactive({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | returnManagementNo: undefined, |
| | | customerName: undefined, |
| | | salesContractNo: undefined, |
| | | createUserName: undefined, |
| | | status: undefined |
| | | }); |
| | | |
| | | const dictRef = proxy.useDict('sales_refund_status'); |
| | | const salesRefundStatusOptions = computed(() => dictRef.sales_refund_status.value || []); |
| | | |
| | | // æç´¢æ é
ç½® |
| | | const searchSchema = [ |
| | | { type: 'input', prop: 'returnManagementNo', placeholder: 'éè´§åå·' }, |
| | | { type: 'input', prop: 'customerName', placeholder: '客æ·åç§°' }, |
| | | { type: 'input', prop: 'salesContractNo', placeholder: 'éå®åå·' }, |
| | | { type: 'input', prop: 'createUserName', placeholder: 'å建人åç§°' }, |
| | | { type: 'select', prop: 'status', placeholder: 'ç¶æ', options: salesRefundStatusOptions } |
| | | ]; |
| | | |
| | | const loading = ref(false); |
| | | const total = ref(0); |
| | | const refundList = ref([]); |
| | | const popupRef = ref(null); |
| | | |
| | | /** æ¥è¯¢å表 */ |
| | | function getList() { |
| | | loading.value = true; |
| | | const { pageNum, pageSize, ...filters } = queryParams; |
| | | listPlan({ |
| | | current: pageNum, |
| | | size: pageSize, |
| | | ...filters |
| | | }) |
| | | .then(res => { |
| | | refundList.value = res?.data?.records || res?.rows || []; |
| | | total.value = res?.data?.total || res?.total || 0; |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | } |
| | | |
| | | /** æç´¢æé®æä½ */ |
| | | function handleQuery() { |
| | | queryParams.pageNum = 1; |
| | | getList(); |
| | | } |
| | | |
| | | /** éç½®æé®æä½ */ |
| | | function resetQuery() { |
| | | handleQuery(); |
| | | } |
| | | |
| | | function openDetail(row) { |
| | | if (popupRef.value) { |
| | | popupRef.value.open({ mode: 'detail', row }); |
| | | } |
| | | } |
| | | function openConfirm(row) { |
| | | if (popupRef.value) { |
| | | popupRef.value.open({ mode: 'confirm', row }); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .table-card { |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); |
| | | } |
| | | </style> |
| | | |
| | | <!-- keep-alive child --> |
| | |
| | | |
| | | <div class="process-card"> |
| | | <div class="process-card__label">ç´¯è®¡æ»æå
¥</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalInput) }}<span class="unit">å
</span> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalInput) }} |
| | | </div> |
| | | </div> |
| | | <div class="process-card"> |
| | | <div class="process-card__label">ç´¯è®¡æ»æ¥åº</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }}<span class="unit">å
</span> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }} |
| | | </div> |
| | | </div> |
| | | <div class="process-card"> |
| | | <div class="process-card__label">累计æ»äº§åº</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }}<span class="unit">å
</span> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | { |
| | | name: 'å¼ç¥¨', |
| | | type: 'line', |
| | | data: receiptAmount, |
| | | data: invoiceAmount, |
| | | stack: 'Total', |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | |
| | | { |
| | | name: '忬¾', |
| | | type: 'line', |
| | | data: invoiceAmount, |
| | | data: receiptAmount, |
| | | stack: 'Total', |
| | | lineStyle: { |
| | | width: 0 |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | width="700px" |
| | | :close-on-click-modal="false"> |
| | | <el-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="120px" |
| | | class="mt8"> |
| | | <!-- é¨é¨éæ© --> |
| | | <el-form-item label="é¨é¨" |
| | | prop="sysDeptId"> |
| | | <el-tree-select v-model="form.sysDeptId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'id', label: 'label', children: 'children' }" |
| | | value-key="id" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | check-strictly |
| | | style="width: 100%" |
| | | :disabled="['edit', 'view'].includes(operationType)" /> |
| | | </el-form-item> |
| | | <!-- å°ç¹ä¿¡æ¯ --> |
| | | <!-- <el-form-item label="å°ç¹åç§°" |
| | | prop="locationName"> |
| | | <el-input v-model="form.locationName" |
| | | placeholder="请è¾å
¥å°ç¹åç§°" |
| | | :disabled="operationType === 'view'" /> |
| | | </el-form-item> --> |
| | | <!-- æå¡èå´ --> |
| | | <el-form-item label="çæ¬¡" |
| | | prop="shift"> |
| | | <el-select v-model="form.shift" |
| | | placeholder="è¯·éæ©çæ¬¡" |
| | | :disabled="operationType === 'view'" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in shifts_list" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æå¡èå´(m)" |
| | | prop="radius"> |
| | | <el-input-number v-model="form.radius" |
| | | :min="10" |
| | | :max="1000" |
| | | :step="10" |
| | | placeholder="请è¾å
¥æå¡èå´" |
| | | :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | <!-- é«å¾·å°å¾éæ© --> |
| | | <el-form-item label="æå¡ä½ç½®" |
| | | prop="longitude"> |
| | | <div class="map-container"> |
| | | <div class="map-header" |
| | | style="margin-bottom: 10px"> |
| | | <!-- <el-button @click="getCurrentLocation"> |
| | | <el-icon> |
| | | <Position /> |
| | | </el-icon> |
| | | å½åä½ç½® |
| | | </el-button> --> |
| | | <!-- <span style="margin-left: 10px; color: #909399;font-size: 12px;">ç¹å»å°å¾éæ©ä½ç½®</span> --> |
| | | </div> |
| | | <div id="map-container" |
| | | class="map" |
| | | ref="mapContainer"></div> |
| | | <div class="coordinates-info mt10"> |
| | | <el-input v-model="form.longitude" |
| | | readonly |
| | | placeholder="ç»åº¦" |
| | | style="width: 140px; margin-right: 10px" /> |
| | | <el-input v-model="form.latitude" |
| | | readonly |
| | | placeholder="纬度" |
| | | style="width: 140px; margin-right: 10px" /> |
| | | <!-- <el-input v-model="form.locationName" |
| | | placeholder="å°ç¹åç§°" |
| | | style="width: calc(100% - 290px)" /> --> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="å°ç¹åç§°" |
| | | prop="locationName"> |
| | | <el-input v-model="form.locationName" |
| | | :disabled="operationType === 'view'" |
| | | placeholder="请è¾å
¥å°ç¹åç§°" /> |
| | | </el-form-item> |
| | | <!-- ä¸ä¸çæ¶é´ --> |
| | | <el-form-item label="ä¸çæ¶é´" |
| | | prop="startAt"> |
| | | <el-time-picker v-model="form.startAt" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | placeholder="è¯·éæ©ä¸çæ¶é´" |
| | | :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | <el-form-item label="ä¸çæ¶é´" |
| | | prop="endAt"> |
| | | <el-time-picker v-model="form.endAt" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | :picker-options="{ |
| | | minTime: form.startAt |
| | | }" |
| | | placeholder="è¯·éæ©ä¸çæ¶é´" |
| | | :disabled="operationType === 'view'" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="dialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="submitForm" |
| | | v-if="operationType !== 'view'"> |
| | | ç¡®å® |
| | | </el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, watch, onMounted, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { Position } from "@element-plus/icons-vue"; |
| | | import { deptTreeSelect } from "@/api/system/user.js"; |
| | | import { addAttendanceRule } from "@/api/personnelManagement/attendanceRules.js"; |
| | | import { useDict } from "@/utils/dict"; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | operationType: { |
| | | type: String, |
| | | default: "add", |
| | | }, |
| | | row: { |
| | | type: Object, |
| | | default: () => ({}), |
| | | }, |
| | | }); |
| | | // const pickerOptions = ref({ minTime: form.value.startAt }); |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close"]); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: val => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const dialogTitle = computed(() => { |
| | | if (props.operationType === "add") return "æ°å¢ç次"; |
| | | if (props.operationType === "edit") return "ç¼è¾ç次"; |
| | | return "æ¥ççæ¬¡"; |
| | | }); |
| | | |
| | | // è·åçæ¬¡åå
¸å¼ |
| | | const { shifts_list } = useDict("shifts_list"); |
| | | |
| | | // è¡¨åæ°æ® |
| | | const formRef = ref(); |
| | | const form = reactive({ |
| | | id: "", |
| | | sysDeptId: "", |
| | | locationName: "", |
| | | longitude: "", |
| | | latitude: "", |
| | | radius: 100, |
| | | startAt: "09:00", |
| | | endAt: "18:00", |
| | | shift: "", |
| | | }); |
| | | |
| | | // 表åéªè¯è§å |
| | | const rules = { |
| | | sysDeptId: [{ required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }], |
| | | locationName: [ |
| | | { required: true, message: "请è¾å
¥å°ç¹åç§°", trigger: "blur" }, |
| | | ], |
| | | longitude: [{ required: true, message: "è¯·éæ©æå¡ä½ç½®", trigger: "blur" }], |
| | | latitude: [{ required: true, message: "è¯·éæ©æå¡ä½ç½®", trigger: "blur" }], |
| | | shift: [{ required: true, message: "è¯·éæ©çæ¬¡", trigger: "change" }], |
| | | radius: [{ required: true, message: "请è¾å
¥æå¡èå´", trigger: "blur" }], |
| | | startAt: [{ required: true, message: "è¯·éæ©ä¸çæ¶é´", trigger: "change" }], |
| | | endAt: [ |
| | | { required: true, message: "è¯·éæ©ä¸çæ¶é´", trigger: "change" }, |
| | | { |
| | | validator: (rule, value, callback) => { |
| | | if (form.startAt && value) { |
| | | const startParts = form.startAt.split(":"); |
| | | const endParts = value.split(":"); |
| | | const startTime = |
| | | parseInt(startParts[0]) * 60 + parseInt(startParts[1]); |
| | | const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]); |
| | | if (endTime <= startTime) { |
| | | callback(new Error("ä¸çæ¶é´ä¸è½æ©äºä¸çæ¶é´")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | } else { |
| | | callback(); |
| | | } |
| | | }, |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | // é¨é¨é项 |
| | | const deptOptions = ref([]); |
| | | |
| | | // å°å¾ç¸å
³ |
| | | const mapContainer = ref(null); |
| | | let map = null; |
| | | let marker = null; |
| | | let circle = null; |
| | | |
| | | // è·åé¨é¨å表 |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then(response => { |
| | | deptOptions.value = filterDisabledDept( |
| | | JSON.parse(JSON.stringify(response.data)) |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | // è¿æ»¤ç¦ç¨çé¨é¨ |
| | | const filterDisabledDept = deptList => { |
| | | return deptList.filter(dept => { |
| | | if (dept.disabled) { |
| | | return false; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | }; |
| | | |
| | | // åå§åå°å¾ |
| | | const initMap = () => { |
| | | nextTick(() => { |
| | | if (window.AMap && mapContainer.value) { |
| | | // åå§åå°å¾ |
| | | map = new window.AMap.Map(mapContainer.value, { |
| | | zoom: 16, |
| | | center: [116.397428, 39.90923], // é»è®¤å京 |
| | | }); |
| | | |
| | | // æ·»å æ§ä»¶ |
| | | window.AMap.plugin(["AMap.ToolBar", "AMap.Scale"], function () { |
| | | map.addControl(new window.AMap.ToolBar()); |
| | | map.addControl(new window.AMap.Scale()); |
| | | }); |
| | | |
| | | // æ·»å æ è®° |
| | | marker = new window.AMap.Marker({ |
| | | position: [116.397428, 39.90923], |
| | | draggable: true, |
| | | cursor: "move", |
| | | title: "ææ½å®ä½", |
| | | }); |
| | | map.add(marker); |
| | | |
| | | // æ·»å åå½¢èå´ |
| | | circle = new window.AMap.Circle({ |
| | | center: [116.397428, 39.90923], |
| | | radius: form.radius, |
| | | strokeColor: "#3366FF", |
| | | strokeOpacity: 0.8, |
| | | strokeWeight: 2, |
| | | fillColor: "#3366FF", |
| | | fillOpacity: 0.2, |
| | | }); |
| | | map.add(circle); |
| | | |
| | | // ç嬿 è®°ææ½ |
| | | marker.on("dragend", e => { |
| | | const position = e.lnglat; |
| | | const lng = position.getLng(); |
| | | const lat = position.getLat(); |
| | | form.longitude = lng; |
| | | form.latitude = lat; |
| | | updateCircle(position); |
| | | }); |
| | | |
| | | // ç嬿 è®°ææ½å¼å§ |
| | | marker.on("dragstart", () => { |
| | | map.setDefaultCursor("move"); |
| | | }); |
| | | |
| | | // ç嬿 è®°ææ½ç»æ |
| | | marker.on("dragend", () => { |
| | | map.setDefaultCursor("default"); |
| | | }); |
| | | |
| | | // çå¬å°å¾ç¹å» |
| | | map.on("click", e => { |
| | | const position = e.lnglat; |
| | | const lng = position.getLng(); |
| | | const lat = position.getLat(); |
| | | form.longitude = lng; |
| | | form.latitude = lat; |
| | | updateMarker(position); |
| | | updateCircle(position); |
| | | }); |
| | | |
| | | // å°è¯è·åå½åä½ç½®å¹¶è®¾ç½®ä¸ºå°å¾ä¸å¿ |
| | | if (navigator.geolocation && !form.longitude && !form.latitude) { |
| | | navigator.geolocation.getCurrentPosition( |
| | | position => { |
| | | console.log("è·åå°å½åä½ç½®:", position); |
| | | const { longitude, latitude } = position.coords; |
| | | const currentPosition = [longitude, latitude]; |
| | | map.setCenter(currentPosition); |
| | | updateMarker(currentPosition); |
| | | updateCircle(currentPosition); |
| | | form.longitude = longitude; |
| | | form.latitude = latitude; |
| | | }, |
| | | error => { |
| | | console.log("è·åä½ç½®å¤±è´¥ï¼ä½¿ç¨é»è®¤ä½ç½®"); |
| | | } |
| | | ); |
| | | } else if (form.longitude && form.latitude) { |
| | | // å¦æææ°æ®ï¼è®¾ç½®å°å°å¾ |
| | | const position = [form.longitude, form.latitude]; |
| | | map.setCenter(position); |
| | | updateMarker(position); |
| | | updateCircle(position); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æ´æ°æ è®°ä½ç½® |
| | | const updateMarker = position => { |
| | | if (marker) { |
| | | marker.setPosition(position); |
| | | } |
| | | }; |
| | | |
| | | // æ´æ°åå½¢èå´ |
| | | const updateCircle = position => { |
| | | if (circle) { |
| | | circle.setCenter(position); |
| | | circle.setRadius(form.radius); |
| | | } |
| | | }; |
| | | |
| | | // è·åå½åä½ç½® |
| | | const getCurrentLocation = () => { |
| | | if (navigator.geolocation) { |
| | | navigator.geolocation.getCurrentPosition( |
| | | position => { |
| | | const { longitude, latitude } = position.coords; |
| | | form.longitude = longitude; |
| | | form.latitude = latitude; |
| | | if (map) { |
| | | map.setCenter([longitude, latitude]); |
| | | updateMarker([longitude, latitude]); |
| | | updateCircle([longitude, latitude]); |
| | | } |
| | | |
| | | // éå°çç¼ç è·åå°å |
| | | if (window.AMap) { |
| | | // å è½½Geocoderæä»¶ |
| | | window.AMap.plugin("AMap.Geocoder", function () { |
| | | const geocoder = new window.AMap.Geocoder(); |
| | | geocoder.getAddress([longitude, latitude], (status, result) => { |
| | | if (status === "complete" && result.regeocode) { |
| | | form.locationName = result.regeocode.formattedAddress; |
| | | } |
| | | }); |
| | | }); |
| | | } |
| | | }, |
| | | error => { |
| | | ElMessage.error("è·åä½ç½®å¤±è´¥ï¼è¯·æå¨éæ©"); |
| | | } |
| | | ); |
| | | } else { |
| | | ElMessage.error("æµè§å¨ä¸æ¯æå°çå®ä½"); |
| | | } |
| | | }; |
| | | |
| | | // çå¬åå¾åå |
| | | watch( |
| | | () => form.radius, |
| | | newValue => { |
| | | if (circle) { |
| | | circle.setRadius(newValue); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | // çå¬ä¸çæ¶é´ååï¼è§¦åä¸çæ¶é´æ ¡éª |
| | | watch( |
| | | () => form.startAt, |
| | | () => { |
| | | if (formRef.value && form.endAt) { |
| | | formRef.value.validateField("endAt"); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | // çå¬å¼¹çªæ¾ç¤º |
| | | watch( |
| | | () => dialogVisible.value, |
| | | newValue => { |
| | | if (newValue) { |
| | | // é置表å |
| | | Object.assign(form, { |
| | | id: "", |
| | | sysDeptId: "", |
| | | locationName: "", |
| | | longitude: "", |
| | | latitude: "", |
| | | radius: 100, |
| | | startAt: "09:00", |
| | | endAt: "18:00", |
| | | shift: "", |
| | | }); |
| | | |
| | | // 妿æ¯ç¼è¾ææ¥çï¼å¡«å
æ°æ® |
| | | if (props.operationType !== "add" && props.row.id) { |
| | | // å¤çæ¶é´æ ¼å¼ï¼ç¡®ä¿æ¯HH:mmæ ¼å¼ |
| | | const rowData = { ...props.row }; |
| | | if (rowData.startAt && rowData.startAt.includes(":")) { |
| | | rowData.startAt = rowData.startAt.split(":").slice(0, 2).join(":"); |
| | | } |
| | | if (rowData.endAt && rowData.endAt.includes(":")) { |
| | | rowData.endAt = rowData.endAt.split(":").slice(0, 2).join(":"); |
| | | } |
| | | Object.assign(form, rowData); |
| | | } |
| | | |
| | | // åå§åå°å¾ |
| | | setTimeout(() => { |
| | | initMap(); |
| | | }, 100); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | // æäº¤è¡¨å |
| | | const submitForm = () => { |
| | | formRef.value.validate(valid => { |
| | | if (valid) { |
| | | const submitData = { |
| | | ...form, |
| | | // è½¬æ¢æ¶é´æ ¼å¼ï¼ç¡®ä¿åªä¿çæ¶åé¨å |
| | | startAt: form.startAt |
| | | ? `${form.startAt.split(":").slice(0, 2).join(":")}` |
| | | : null, |
| | | endAt: form.endAt |
| | | ? `${form.endAt.split(":").slice(0, 2).join(":")}` |
| | | : null, |
| | | }; |
| | | |
| | | if (props.operationType === "add") { |
| | | addAttendanceRule(submitData).then(() => { |
| | | ElMessage.success("æ°å¢æå"); |
| | | emit("close"); |
| | | }); |
| | | } else if (props.operationType === "edit") { |
| | | addAttendanceRule(submitData).then(() => { |
| | | ElMessage.success("æ´æ°æå"); |
| | | emit("close"); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | fetchDeptOptions(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .map-container { |
| | | width: 100%; |
| | | } |
| | | |
| | | .map { |
| | | width: 100%; |
| | | height: 400px; |
| | | border: 1px solid #e4e7ed; |
| | | } |
| | | |
| | | .coordinates-info { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .coordinates-display { |
| | | padding: 10px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .mt10 { |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .mt8 { |
| | | margin-top: 8px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 页颿 é¢åæä½æé® --> |
| | | <div class="page-header"> |
| | | <div class="title">çæ¬¡é
ç½®</div> |
| | | <div class="actions"> |
| | | <el-button type="primary" |
| | | @click="openForm('add')"> |
| | | <el-icon> |
| | | <Plus /> |
| | | </el-icon> |
| | | æ°å¢ç次 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <!-- æ¥è¯¢æ¡ä»¶ --> |
| | | <!-- <el-form :model="searchForm" |
| | | :inline="true" |
| | | class="search-form mb16"> |
| | | <el-form-item label="é¨é¨ï¼" |
| | | prop="countId"> |
| | | <el-tree-select v-model="searchForm.countId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'id', label: 'label', children: 'children' }" |
| | | value-key="id" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | check-strictly |
| | | style="width: 200px" /> |
| | | </el-form-item> |
| | | <el-form-item label="å°ç¹ï¼" |
| | | prop="locationName"> |
| | | <el-input v-model="searchForm.locationName" |
| | | placeholder="请è¾å
¥å°ç¹åç§°" |
| | | clearable |
| | | style="width: 200px" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" |
| | | @click="fetchData"> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="resetSearch"> |
| | | <el-icon> |
| | | <Refresh /> |
| | | </el-icon> |
| | | éç½® |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> --> |
| | | <!-- çæ¬¡å表 --> |
| | | <el-card shadow="never" |
| | | class="mb16"> |
| | | <el-table :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | height="calc(100vh - 18.5em)" |
| | | style="width: 100%" |
| | | row-key="id"> |
| | | <el-table-column type="index" |
| | | label="åºå·" |
| | | width="60" |
| | | align="center" /> |
| | | <el-table-column label="é¨é¨"> |
| | | <template #default="scope"> |
| | | {{ getDeptNameById(scope.row.sysDeptId) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="çæ¬¡"> |
| | | <template #default="scope"> |
| | | {{ getShiftNameByValue(scope.row.shift) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="locationName" |
| | | label="å°ç¹åç§°" /> |
| | | <el-table-column prop="longitude" |
| | | label="ç»åº¦" /> |
| | | <el-table-column prop="latitude" |
| | | label="纬度" /> |
| | | <el-table-column prop="radius" |
| | | label="æå¡èå´(m)" /> |
| | | <el-table-column prop="startAt" |
| | | label="ä¸çæ¶é´"> |
| | | <template #default="scope"> |
| | | {{ scope.row.startAt }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="endAt" |
| | | label="ä¸çæ¶é´"> |
| | | <template #default="scope"> |
| | | {{ scope.row.endAt }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" |
| | | width="180" |
| | | fixed="right" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-button type="primary" |
| | | size="small" |
| | | link |
| | | @click="openForm('edit', scope.row)">ç¼è¾</el-button> |
| | | <el-button type="danger" |
| | | size="small" |
| | | link |
| | | @click="handleDelete(scope.row.id)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination :total="page.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" |
| | | class="mt10" /> |
| | | </el-card> |
| | | <!-- æ°å¢/ç¼è¾çæ¬¡å¼¹çª --> |
| | | <rule-form ref="ruleFormRef" |
| | | v-model="dialogVisible" |
| | | :operation-type="operationType" |
| | | :row="currentRow" |
| | | @close="dialogVisible = false; fetchData()" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { |
| | | Plus, |
| | | Edit, |
| | | Delete, |
| | | Search, |
| | | Refresh, |
| | | ArrowLeft, |
| | | } from "@element-plus/icons-vue"; |
| | | import Pagination from "@/components/Pagination/index.vue"; |
| | | import RuleForm from "./components/form.vue"; |
| | | import { deptTreeSelect } from "@/api/system/user.js"; |
| | | import { |
| | | getAttendanceRules, |
| | | deleteAttendanceRule, |
| | | } from "@/api/personnelManagement/attendanceRules.js"; |
| | | import { useDict } from "@/utils/dict"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | |
| | | // å页忰 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // æ¥è¯¢è¡¨å |
| | | const searchForm = reactive({ |
| | | countId: "", |
| | | locationName: "", |
| | | }); |
| | | |
| | | // é¨é¨é项 |
| | | const deptOptions = ref([]); |
| | | // è·åçæ¬¡åå
¸å¼ |
| | | const { shifts_list } = useDict("shifts_list"); |
| | | |
| | | // å¼¹çªæ§å¶ |
| | | const dialogVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const currentRow = ref({}); |
| | | const ruleFormRef = ref(); |
| | | |
| | | // æ ¼å¼åæ¶é´ |
| | | const formatTime = timestamp => { |
| | | if (!timestamp) return ""; |
| | | const date = new Date(timestamp); |
| | | return `${String(date.getHours()).padStart(2, "0")}:${String( |
| | | date.getMinutes() |
| | | ).padStart(2, "0")}`; |
| | | }; |
| | | |
| | | // æ ¹æ®ç次å¼è·åçæ¬¡åç§° |
| | | const getShiftNameByValue = value => { |
| | | if (!value) return ""; |
| | | const shift = shifts_list.value.find(item => item.value === value); |
| | | return shift ? shift.label : value; |
| | | }; |
| | | |
| | | // è·åé¨é¨å表 |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then(response => { |
| | | deptOptions.value = filterDisabledDept( |
| | | JSON.parse(JSON.stringify(response.data)) |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | // è¿æ»¤ç¦ç¨çé¨é¨ |
| | | const filterDisabledDept = deptList => { |
| | | return deptList.filter(dept => { |
| | | if (dept.disabled) { |
| | | return false; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | }; |
| | | |
| | | // æ ¹æ®é¨é¨IDæ¥æ¾é¨é¨åç§° |
| | | const getDeptNameById = (deptId, deptList = deptOptions.value) => { |
| | | for (const dept of deptList) { |
| | | if (dept.id === deptId) { |
| | | return dept.label; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | const name = getDeptNameById(deptId, dept.children); |
| | | if (name) { |
| | | return name; |
| | | } |
| | | } |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | // æ¥è¯¢ç次å表 |
| | | const fetchData = () => { |
| | | tableLoading.value = true; |
| | | getAttendanceRules({ ...page, ...searchForm }) |
| | | .then(res => { |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // å页忴 |
| | | const paginationChange = pagination => { |
| | | page.current = pagination.page; |
| | | page.size = pagination.limit; |
| | | fetchData(); |
| | | }; |
| | | |
| | | // éç½®æç´¢ |
| | | const resetSearch = () => { |
| | | searchForm.countId = ""; |
| | | searchForm.locationName = ""; |
| | | fetchData(); |
| | | }; |
| | | |
| | | // æå¼è¡¨å |
| | | const openForm = (type, row = {}) => { |
| | | operationType.value = type; |
| | | currentRow.value = row; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | // å é¤ç次 |
| | | const handleDelete = id => { |
| | | ElMessageBox.confirm("ç¡®å®è¦å é¤è¿æ¡ç次åï¼", "å é¤ç¡®è®¤", { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | deleteAttendanceRule([id]).then(() => { |
| | | ElMessage.success("å 餿å"); |
| | | fetchData(); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | // åæ¶å é¤ |
| | | }); |
| | | }; |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | fetchDeptOptions(); |
| | | fetchData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | |
| | | .title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .mt10 { |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- åå·¥æå¡åº --> |
| | | <el-card shadow="never" class="mb16"> |
| | | <!-- <el-card shadow="never" |
| | | class="mb16"> |
| | | <div class="attendance-header"> |
| | | <div> |
| | | <div class="title">æå¡ç¾å°</div> |
| | | <div class="title">æå¡ç¾å° |
| | | </div> |
| | | <div class="sub-title">æ¯æä¸é®æå¡ï¼èªå¨è®°å½ä¸ä¸çæ¶é´</div> |
| | | </div> |
| | | <div class="attendance-actions"> |
| | |
| | | <div class="label">å½åæ¶é´</div> |
| | | <div class="value">{{ nowTime }}</div> |
| | | </div> |
| | | <el-button type="primary" size="large" @click="handleCheckInOut"> |
| | | <el-button type="primary" |
| | | size="large" |
| | | @click="handleCheckInOut" |
| | | :disabled="todayRecord.workEndAt"> |
| | | {{ checkInOutText }} |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | <el-descriptions border :column="4" class="mt10"> |
| | | <el-descriptions border |
| | | :column="4" |
| | | class="mt10"> |
| | | <el-descriptions-item label="åå·¥å§å"> |
| | | {{ currentUser.name }} |
| | | {{ todayRecord.staffName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å·¥å·"> |
| | | {{ currentUser.no }} |
| | | {{ todayRecord.staffNo }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æå±é¨é¨"> |
| | | {{ currentUser.dept }} |
| | | {{ todayRecord.deptName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="仿¥ç¶æ"> |
| | | <el-tag :type="todayStatusTag" size="small"> |
| | | <el-tag :type="todayStatusTag" |
| | | size="small"> |
| | | {{ todayStatusText }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ä¸çæ¶é´"> |
| | | {{ todayRecord?.checkInTime || '-' }} |
| | | {{ todayRecord?.workStartAt || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ä¸çæ¶é´"> |
| | | {{ todayRecord?.checkOutTime || '-' }} |
| | | {{ todayRecord?.workEndAt || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å·¥æ¶(å°æ¶)"> |
| | | {{ todayRecord?.workHours ?? '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å¼å¸¸æ è®°"> |
| | | <span v-if="todayRecord?.status === 'normal'">-</span> |
| | | <el-tag v-else type="danger" size="small"> |
| | | {{ todayRecord?.statusText }} |
| | | <span v-if="!todayRecord.id || todayRecord?.status === 0">-</span> |
| | | <el-tag v-else |
| | | type="danger" |
| | | size="small"> |
| | | {{ todayRecord?.status ? getStatusText(todayRecord.status) : '-' }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- æ¥è¯¢æ¡ä»¶ï¼ç®¡çåè夿¥æ¥ï¼ --> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">é¨é¨ï¼</span> |
| | | <el-select |
| | | v-model="searchForm.dept" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | style="width: 180px" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="item in deptOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | |
| | | <span class="search_title ml10">æ¥æï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.date" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | clearable |
| | | /> |
| | | |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px"> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </div> |
| | | <div> |
| | | <el-button icon="Download" @click="handleExport"> |
| | | 导åºè夿¥æ¥ |
| | | </el-button> |
| | | </div> |
| | | </el-card> --> |
| | | <div class="attendance-operation"> |
| | | <!-- æ¥è¯¢æ¡ä»¶ï¼ç®¡çåè夿¥æ¥ï¼ --> |
| | | <el-form :model="searchForm" |
| | | :inline="true" |
| | | class="search-form"> |
| | | <el-form-item label="é¨é¨ï¼" |
| | | prop="deptId"> |
| | | <el-tree-select v-model="searchForm.deptId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'id', label: 'label', children: 'children' }" |
| | | value-key="id" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | check-strictly |
| | | style="width: 200px" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æï¼" |
| | | prop="date"> |
| | | <el-date-picker v-model="searchForm.date" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | clearable /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" |
| | | @click="fetchData"> |
| | | <el-icon> |
| | | <Search /> |
| | | </el-icon> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="resetSearch"> |
| | | <el-icon> |
| | | <Refresh /> |
| | | </el-icon> |
| | | éç½® |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | <el-button icon="Download" |
| | | @click="handleExport"> |
| | | 导åºè夿¥æ¥ |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- è夿¥æ¥è¡¨æ ¼ --> |
| | | <div class="table_list"> |
| | | <el-table |
| | | :data="tableData" |
| | | border |
| | | style="width: 100%" |
| | | height="calc(100vh - 24em)" |
| | | :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" |
| | | :row-class-name="rowClassName" |
| | | > |
| | | <el-table-column type="index" label="åºå·" width="60" align="center" /> |
| | | <el-table-column |
| | | prop="date" |
| | | label="æ¥æ" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | prop="dept" |
| | | label="é¨é¨" |
| | | width="140" |
| | | /> |
| | | <el-table-column |
| | | prop="name" |
| | | label="å§å" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | prop="no" |
| | | label="å·¥å·" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | prop="checkInTime" |
| | | label="ä¸çæ¶é´" |
| | | width="140" |
| | | /> |
| | | <el-table-column |
| | | prop="checkOutTime" |
| | | label="ä¸çæ¶é´" |
| | | width="140" |
| | | /> |
| | | <el-table-column |
| | | prop="workHours" |
| | | label="å·¥æ¶(å°æ¶)" |
| | | width="110" |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | prop="statusText" |
| | | label="èå¤ç¶æ" |
| | | width="120" |
| | | align="center" |
| | | > |
| | | <el-table :data="tableData" |
| | | border |
| | | v-loading="tableLoading" |
| | | style="width: 100%" |
| | | height="calc(100vh - 24em)" |
| | | :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" |
| | | :row-class-name="rowClassName"> |
| | | <el-table-column type="index" |
| | | label="åºå·" |
| | | width="60" |
| | | align="center" /> |
| | | <el-table-column prop="date" |
| | | label="æ¥æ" |
| | | width="120" /> |
| | | <el-table-column prop="deptName" |
| | | label="é¨é¨" |
| | | width="140" /> |
| | | <el-table-column prop="staffName" |
| | | label="å§å" |
| | | width="120" /> |
| | | <el-table-column prop="staffNo" |
| | | label="å·¥å·" |
| | | width="120" /> |
| | | <el-table-column prop="workStartAt" |
| | | label="ä¸çæ¶é´" |
| | | width="140" /> |
| | | <el-table-column prop="workEndAt" |
| | | label="ä¸çæ¶é´" |
| | | width="140" /> |
| | | <el-table-column prop="workHours" |
| | | label="å·¥æ¶(å°æ¶)" |
| | | align="center" /> |
| | | <el-table-column prop="status" |
| | | label="èå¤ç¶æ" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag |
| | | v-if="scope.row.status === 'normal'" |
| | | type="success" |
| | | size="small" |
| | | > |
| | | <el-tag v-if="scope.row.status === 0" |
| | | type="success" |
| | | size="small"> |
| | | æ£å¸¸ |
| | | </el-tag> |
| | | <el-tag |
| | | v-else |
| | | type="danger" |
| | | size="small" |
| | | > |
| | | {{ scope.row.statusText }} |
| | | <el-tag v-else |
| | | type="danger" |
| | | size="small"> |
| | | <!-- {{ scope.row.status === 1 ? 'è¿å°' : scope.row.status === 2 ? 'æ©é' : 'è¿å°ãæ©é' }} --> |
| | | {{ getStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | prop="remark" |
| | | label="夿³¨" |
| | | show-overflow-tooltip |
| | | /> |
| | | <el-table-column prop="remark" |
| | | label="夿³¨" |
| | | show-overflow-tooltip /> |
| | | </el-table> |
| | | <pagination :total="page.total" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" |
| | | :limit="page.size" |
| | | @pagination="paginationChange" /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { |
| | | createPersonalAttendanceRecord, |
| | | findPersonalAttendanceRecords, |
| | | findTodayPersonalAttendanceRecord, |
| | | } from "@/api/personnelManagement/personalAttendanceRecords.js"; |
| | | import Pagination from "@/components/Pagination/index.vue"; |
| | | import { deptTreeSelect } from "@/api/system/user.js"; |
| | | import { Refresh, Search, ArrowLeft } from "@element-plus/icons-vue"; |
| | | |
| | | // 模æå½åç»å½åå·¥ |
| | | const currentUser = reactive({ |
| | | id: 1, |
| | | name: "å¼ ä¸", |
| | | no: "E10001", |
| | | dept: "ç产ä¸é¨", |
| | | }); |
| | | |
| | | // é¨é¨é项 |
| | | const deptOptions = [ |
| | | { label: "ç产ä¸é¨", value: "ç产ä¸é¨" }, |
| | | { label: "ç产äºé¨", value: "ç产äºé¨" }, |
| | | { label: "设å¤ç»´æ¤é¨", value: "设å¤ç»´æ¤é¨" }, |
| | | { label: "è´¨æ£é¨", value: "è´¨æ£é¨" }, |
| | | ]; |
| | | |
| | | // 模æèå¤åå§æ°æ® |
| | | const rawAttendance = ref([ |
| | | { |
| | | id: 1, |
| | | date: "2024-12-01", |
| | | userId: 1, |
| | | name: "å¼ ä¸", |
| | | no: "E10001", |
| | | dept: "ç产ä¸é¨", |
| | | checkInTime: "08:58", |
| | | checkOutTime: "18:10", |
| | | workHours: 9.2, |
| | | status: "normal", |
| | | statusText: "æ£å¸¸", |
| | | remark: "", |
| | | }, |
| | | { |
| | | id: 2, |
| | | date: "2024-12-01", |
| | | userId: 2, |
| | | name: "æå", |
| | | no: "E10002", |
| | | dept: "ç产ä¸é¨", |
| | | checkInTime: "09:15", |
| | | checkOutTime: "18:05", |
| | | workHours: 8.8, |
| | | status: "late", |
| | | statusText: "è¿å°", |
| | | remark: "å äº¤éæ¥å µè¿å°", |
| | | }, |
| | | { |
| | | id: 3, |
| | | date: "2024-12-01", |
| | | userId: 3, |
| | | name: "çäº", |
| | | no: "E20001", |
| | | dept: "设å¤ç»´æ¤é¨", |
| | | checkInTime: "08:50", |
| | | checkOutTime: "17:20", |
| | | workHours: 8.5, |
| | | status: "early", |
| | | statusText: "æ©é", |
| | | remark: "å¤åºå¤çç´§æ¥æ
é", |
| | | }, |
| | | { |
| | | id: 4, |
| | | date: "2024-12-02", |
| | | userId: 1, |
| | | name: "å¼ ä¸", |
| | | no: "E10001", |
| | | dept: "ç产ä¸é¨", |
| | | checkInTime: "08:45", |
| | | checkOutTime: "18:30", |
| | | workHours: 9.7, |
| | | status: "normal", |
| | | statusText: "æ£å¸¸", |
| | | remark: "å ç0.5å°æ¶", |
| | | }, |
| | | ]); |
| | | |
| | | // æ¥è¯¢è¡¨å |
| | | const searchForm = reactive({ |
| | | dept: "", |
| | | date: "", |
| | | }); |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([]); |
| | | |
| | | // å½åæ¶é´å±ç¤º |
| | | const nowTime = ref(""); |
| | | let timer = null; |
| | | |
| | | const updateNowTime = () => { |
| | | const now = new Date(); |
| | | const Y = now.getFullYear(); |
| | | const M = String(now.getMonth() + 1).padStart(2, "0"); |
| | | const D = String(now.getDate()).padStart(2, "0"); |
| | | const h = String(now.getHours()).padStart(2, "0"); |
| | | const m = String(now.getMinutes()).padStart(2, "0"); |
| | | const s = String(now.getSeconds()).padStart(2, "0"); |
| | | nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`; |
| | | }; |
| | | |
| | | // 仿¥æ¥æ |
| | | const todayStr = computed(() => nowTime.value.slice(0, 10)); |
| | | |
| | | // 彿¥å½ååå·¥èå¤è®°å½ |
| | | const todayRecord = computed(() => |
| | | rawAttendance.value.find( |
| | | (item) => |
| | | item.userId === currentUser.id && item.date === todayStr.value |
| | | ) |
| | | ); |
| | | |
| | | // æå¡æé®ææ¬ |
| | | const checkInOutText = computed(() => { |
| | | if (!todayRecord.value || !todayRecord.value.checkInTime) { |
| | | return "ä¸çæå¡"; |
| | | } |
| | | if (!todayRecord.value.checkOutTime) { |
| | | return "ä¸çæå¡"; |
| | | } |
| | | return "仿¥å·²æå¡å®æ"; |
| | | }); |
| | | |
| | | // 仿¥ç¶æå±ç¤º |
| | | const todayStatusTag = computed(() => { |
| | | if (!todayRecord.value) return "info"; |
| | | if (todayRecord.value.status === "normal") return "success"; |
| | | return "danger"; |
| | | }); |
| | | |
| | | const todayStatusText = computed(() => { |
| | | if (!todayRecord.value) return "æªæå¡"; |
| | | return todayRecord.value.statusText || "æ£å¸¸"; |
| | | }); |
| | | |
| | | // è¡æ ·å¼ï¼å¼å¸¸é«äº® |
| | | const rowClassName = ({ row }) => { |
| | | if (row.status === "late" || row.status === "early") { |
| | | return "row-abnormal"; |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | // æ¥è¯¢ |
| | | const recomputeTable = () => { |
| | | const list = rawAttendance.value.filter((item) => { |
| | | if (searchForm.dept && item.dept !== searchForm.dept) { |
| | | return false; |
| | | } |
| | | if (searchForm.date && item.date !== searchForm.date) { |
| | | return false; |
| | | } |
| | | return true; |
| | | const { proxy } = getCurrentInstance(); |
| | | const router = useRouter(); |
| | | const tableLoading = ref(false); |
| | | // å页忰 |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | tableData.value = list; |
| | | }; |
| | | // 仿¥æ°æ® |
| | | const todayRecord = ref({}); |
| | | |
| | | const handleQuery = () => { |
| | | recomputeTable(); |
| | | }; |
| | | // é¨é¨é项 |
| | | const deptOptions = ref([]); |
| | | |
| | | const resetSearch = () => { |
| | | searchForm.dept = ""; |
| | | searchForm.date = ""; |
| | | recomputeTable(); |
| | | }; |
| | | // æ¥è¯¢è¡¨å |
| | | const searchForm = reactive({ |
| | | deptId: "", |
| | | date: "", |
| | | }); |
| | | |
| | | // 导åºï¼æ¼ç¤ºï¼ |
| | | const handleExport = () => { |
| | | ElMessage.success("å½å为æ¼ç¤ºé¡µé¢ï¼å¯¼åºåè½æªå¯¹æ¥å®é
æ¥å£"); |
| | | }; |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([]); |
| | | |
| | | // æå¡é»è¾ï¼ä»
å端模æï¼ |
| | | const handleCheckInOut = () => { |
| | | const [dateStr, timeStr] = nowTime.value.split(" "); |
| | | if (!dateStr || !timeStr) return; |
| | | // å½åæ¶é´å±ç¤º |
| | | const nowTime = ref(""); |
| | | let timer = null; |
| | | |
| | | // ä¸çæå¡ |
| | | if (!todayRecord.value) { |
| | | const newId = rawAttendance.value.length |
| | | ? Math.max(...rawAttendance.value.map((i) => i.id)) + 1 |
| | | : 1; |
| | | const status = |
| | | timeStr > "09:00:00" ? "late" : "normal"; |
| | | const statusText = status === "late" ? "è¿å°" : "æ£å¸¸"; |
| | | rawAttendance.value.push({ |
| | | id: newId, |
| | | date: dateStr, |
| | | userId: currentUser.id, |
| | | name: currentUser.name, |
| | | no: currentUser.no, |
| | | dept: currentUser.dept, |
| | | checkInTime: timeStr.slice(0, 5), |
| | | checkOutTime: "", |
| | | workHours: null, |
| | | status, |
| | | statusText, |
| | | remark: "", |
| | | }); |
| | | ElMessage.success("ä¸çæå¡æå"); |
| | | } else if (!todayRecord.value.checkOutTime) { |
| | | // ä¸çæå¡ |
| | | todayRecord.value.checkOutTime = timeStr.slice(0, 5); |
| | | // ç®åæ 9:00-18:00 计ç®å·¥æ¶ |
| | | const start = todayRecord.value.checkInTime || "09:00"; |
| | | const [sh, sm] = start.split(":").map((v) => parseInt(v, 10)); |
| | | const [eh, em] = todayRecord.value.checkOutTime |
| | | .split(":") |
| | | .map((v) => parseInt(v, 10)); |
| | | const diff = (eh * 60 + em - (sh * 60 + sm)) / 60; |
| | | todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1)); |
| | | const updateNowTime = () => { |
| | | const now = new Date(); |
| | | const Y = now.getFullYear(); |
| | | const M = String(now.getMonth() + 1).padStart(2, "0"); |
| | | const D = String(now.getDate()).padStart(2, "0"); |
| | | const h = String(now.getHours()).padStart(2, "0"); |
| | | const m = String(now.getMinutes()).padStart(2, "0"); |
| | | const s = String(now.getSeconds()).padStart(2, "0"); |
| | | nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`; |
| | | }; |
| | | |
| | | // æ©é夿ï¼18:00 å离å¼è§ä¸ºæ©éï¼åªç¤ºæï¼ |
| | | if (timeStr < "18:00:00") { |
| | | todayRecord.value.status = "early"; |
| | | todayRecord.value.statusText = "æ©é"; |
| | | } else if (todayRecord.value.status === "normal") { |
| | | todayRecord.value.statusText = "æ£å¸¸"; |
| | | // æå¡æé®ææ¬ |
| | | const checkInOutText = computed(() => { |
| | | if (!todayRecord.value || !todayRecord.value.workStartAt) { |
| | | return "ä¸çæå¡"; |
| | | } |
| | | ElMessage.success("ä¸çæå¡æå"); |
| | | } else { |
| | | ElMessage.info("仿¥å·²å®æä¸ä¸çæå¡"); |
| | | if (!todayRecord.value.workEndAt) { |
| | | return "ä¸çæå¡"; |
| | | } |
| | | return "仿¥å·²æå¡å®æ"; |
| | | }); |
| | | |
| | | // 仿¥ç¶æå±ç¤º |
| | | const todayStatusTag = computed(() => { |
| | | if (!todayRecord.value.id) return "info"; |
| | | if (todayRecord.value.status === 0) return "success"; |
| | | return "danger"; |
| | | }); |
| | | const getStatusText = status => { |
| | | switch (status) { |
| | | case 0: |
| | | return "æ£å¸¸"; |
| | | case 1: |
| | | return "è¿å°"; |
| | | case 2: |
| | | return "æ©é"; |
| | | case 3: |
| | | return "è¿å°ãæ©é"; |
| | | case 4: |
| | | return "缺å¤"; |
| | | } |
| | | }; |
| | | |
| | | const todayStatusText = computed(() => { |
| | | if (!todayRecord.value.id) return "æªæå¡"; |
| | | switch (todayRecord.value.status) { |
| | | case 0: |
| | | return "æ£å¸¸"; |
| | | case 1: |
| | | return "è¿å°"; |
| | | case 2: |
| | | return "æ©é"; |
| | | case 3: |
| | | return "è¿å°ãæ©é"; |
| | | case 4: |
| | | return "缺å¤"; |
| | | } |
| | | }); |
| | | |
| | | // è¡æ ·å¼ï¼å¼å¸¸é«äº® |
| | | const rowClassName = ({ row }) => { |
| | | if (row.status === 1 || row.status === 2) { |
| | | return "row-abnormal"; |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | // æ¥è¯¢é¨é¨å表 |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then(response => { |
| | | deptOptions.value = filterDisabledDept( |
| | | JSON.parse(JSON.stringify(response.data)) |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | /** è¿æ»¤ç¦ç¨çé¨é¨ */ |
| | | function filterDisabledDept(deptList) { |
| | | return deptList.filter(dept => { |
| | | if (dept.disabled) { |
| | | return false; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | } |
| | | |
| | | recomputeTable(); |
| | | }; |
| | | // æ¥è¯¢ |
| | | const fetchData = () => { |
| | | tableLoading.value = true; |
| | | findPersonalAttendanceRecords({ ...page, ...searchForm }) |
| | | .then(res => { |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | updateNowTime(); |
| | | timer = setInterval(updateNowTime, 1000); |
| | | // é»è®¤å±ç¤ºå½å¤©æ°æ® |
| | | const today = new Date(); |
| | | const Y = today.getFullYear(); |
| | | const M = String(today.getMonth() + 1).padStart(2, "0"); |
| | | const D = String(today.getDate()).padStart(2, "0"); |
| | | searchForm.date = `${Y}-${M}-${D}`; |
| | | recomputeTable(); |
| | | }); |
| | | // æ¥è¯¢ä»æ¥æå¡ä¿¡æ¯ |
| | | const fetchTodayData = () => { |
| | | // findTodayPersonalAttendanceRecord({}).then(res => { |
| | | // todayRecord.value = res.data; |
| | | // }); |
| | | }; |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | } |
| | | }); |
| | | const paginationChange = pagination => { |
| | | page.current = pagination.page; |
| | | page.size = pagination.limit; |
| | | fetchData(); |
| | | }; |
| | | |
| | | const resetSearch = () => { |
| | | searchForm.deptId = ""; |
| | | searchForm.date = ""; |
| | | fetchData(); |
| | | }; |
| | | |
| | | const handleExport = () => { |
| | | ElMessageBox.confirm("æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download("/personalAttendanceRecords/export", {}, "èå¤è®°å½.xlsx"); |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | |
| | | // è·åå½åä½ç½® |
| | | const getCurrentLocation = () => { |
| | | return new Promise((resolve, reject) => { |
| | | if (!navigator.geolocation) { |
| | | reject(new Error("æµè§å¨ä¸æ¯æå°çå®ä½")); |
| | | return; |
| | | } |
| | | |
| | | // æ£æ¥æ¯å¦ä½¿ç¨HTTPS |
| | | const isSecureContext = |
| | | window.isSecureContext || window.location.protocol === "https:"; |
| | | console.log( |
| | | "å½ååè®®:", |
| | | window.location.protocol, |
| | | "æ¯å¦å®å
¨ä¸ä¸æ:", |
| | | isSecureContext |
| | | ); |
| | | |
| | | if (!isSecureContext) { |
| | | console.warn("å½å䏿¯HTTPSåè®®ï¼å°çä½ç½®APIå¯è½åé"); |
| | | } |
| | | |
| | | navigator.geolocation.getCurrentPosition( |
| | | position => { |
| | | const { longitude, latitude } = position.coords; |
| | | console.log("è·åä½ç½®æå:", longitude, latitude); |
| | | resolve({ longitude, latitude }); |
| | | }, |
| | | error => { |
| | | console.log("è·åä½ç½®å¤±è´¥:", error); |
| | | let errorMessage = "è·åä½ç½®å¤±è´¥"; |
| | | |
| | | // æ ¹æ®é误类åæä¾æ´å
·ä½çæç¤º |
| | | switch (error.code) { |
| | | case error.PERMISSION_DENIED: |
| | | errorMessage = |
| | | "ç¨æ·æç»äºä½ç½®æé请æ±ï¼è¯·å¨æµè§å¨è®¾ç½®ä¸å
许ä½ç½®è®¿é®"; |
| | | break; |
| | | case error.POSITION_UNAVAILABLE: |
| | | errorMessage = "ä½ç½®ä¿¡æ¯ä¸å¯ç¨ï¼è¯·æ£æ¥è®¾å¤å®ä½åè½"; |
| | | break; |
| | | case error.TIMEOUT: |
| | | errorMessage = "è·åä½ç½®è¶
æ¶ï¼è¯·éè¯"; |
| | | break; |
| | | case error.UNKNOWN_ERROR: |
| | | errorMessage = "è·åä½ç½®æ¶åçæªç¥é误"; |
| | | break; |
| | | default: |
| | | errorMessage = `è·åä½ç½®å¤±è´¥: ${error.message}`; |
| | | } |
| | | |
| | | // æ£æ¥æ¯å¦æ¯HTTPSé®é¢ |
| | | if (error.code === error.PERMISSION_DENIED && !isSecureContext) { |
| | | errorMessage += "ï¼æ³¨æï¼ç产ç¯å¢éè¦ä½¿ç¨HTTPSåè®®æè½è·åä½ç½®ï¼"; |
| | | } |
| | | |
| | | reject(new Error(errorMessage)); |
| | | }, |
| | | { |
| | | enableHighAccuracy: true, |
| | | timeout: 10000, |
| | | maximumAge: 0, |
| | | } |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | // æå¡ |
| | | const handleCheckInOut = () => { |
| | | getCurrentLocation() |
| | | .then(location => { |
| | | console.log("ä½ç½®æå"); |
| | | createPersonalAttendanceRecord(location).then(res => { |
| | | fetchData(); |
| | | fetchTodayData(); |
| | | ElMessage.success("æå¡æåï¼"); |
| | | }); |
| | | }) |
| | | .catch(error => { |
| | | // è·åä½ç½®å¤±è´¥æ¶ï¼ä»å
许æå¡ |
| | | ElMessage.warning("è·åä½ç½®å¤±è´¥ï¼å°ä½¿ç¨é»è®¤ä½ç½®æå¡"); |
| | | createPersonalAttendanceRecord({}).then(res => { |
| | | fetchData(); |
| | | fetchTodayData(); |
| | | ElMessage.success("æå¡æåï¼"); |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | updateNowTime(); |
| | | timer = setInterval(updateNowTime, 1000); |
| | | // é»è®¤å±ç¤ºå½å¤©æ°æ® |
| | | const today = new Date(); |
| | | const Y = today.getFullYear(); |
| | | const M = String(today.getMonth() + 1).padStart(2, "0"); |
| | | const D = String(today.getDate()).padStart(2, "0"); |
| | | searchForm.date = `${Y}-${M}-${D}`; |
| | | fetchData(); |
| | | fetchTodayData(); |
| | | fetchDeptOptions(); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | .mb16 { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .attendance-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | .attendance-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .attendance-header .title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | margin-bottom: 4px; |
| | | } |
| | | .attendance-header .title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .attendance-header .sub-title { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | .attendance-header .sub-title { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .attendance-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | .attendance-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .time-block { |
| | | text-align: right; |
| | | } |
| | | .time-block { |
| | | text-align: right; |
| | | } |
| | | |
| | | .time-block .label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | .time-block .label { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .time-block .value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | .time-block .value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | ::v-deep(.row-abnormal) { |
| | | background-color: #fff5f5; |
| | | } |
| | | ::v-deep(.row-abnormal) { |
| | | background-color: #fff5f5; |
| | | } |
| | | |
| | | .attendance-operation { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="class-page"> |
| | | <div class="search-container"> |
| | | <div class="search-form"> |
| | | <div class="search-row"> |
| | | <div class="search-item"> |
| | | <label class="search-label">éæ©æ¶é´ï¼</label> |
| | | <div class="search-input-group"> |
| | | <el-date-picker v-model="query.year" |
| | | type="year" |
| | | size="small" |
| | | format="YYYY" |
| | | placeholder="鿩年" |
| | | @change="refreshTable()" |
| | | style="width: 90px" |
| | | :clearable="false" /> |
| | | <el-select v-model="query.month" |
| | | clearable |
| | | placeholder="éæ©æ" |
| | | style="width: 70px; margin-left: 8px" |
| | | size="small" |
| | | @change="refreshTable()"> |
| | | <el-option v-for="item in monthOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <div class="search-item"> |
| | | <el-input v-model="query.userName" |
| | | placeholder="请è¾å
¥äººååç§°" |
| | | size="small" |
| | | style="width: 120px" |
| | | clearable |
| | | @keyup.enter="refreshTable()" /> |
| | | </div> |
| | | <div class="search-item"> |
| | | <el-tree-select v-model="query.sysDeptId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'id', label: 'label', children: 'children' }" |
| | | value-key="id" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | size="small" |
| | | clearable |
| | | @change="refreshTable()" |
| | | style="width: 140px" /> |
| | | </div> |
| | | <div class="search-actions"> |
| | | <el-button size="small" |
| | | type="primary" |
| | | @click="refreshTable()" |
| | | :icon="Search"> |
| | | æ¥è¯¢ |
| | | </el-button> |
| | | <el-button size="small" |
| | | @click="refresh()" |
| | | :icon="Refresh" |
| | | style="margin-left: 8px"> |
| | | éç½® |
| | | </el-button> |
| | | </div> |
| | | <div class="search-buttons"> |
| | | <el-button size="small" |
| | | type="primary" |
| | | @click="configTime" |
| | | :icon="Setting"> |
| | | çæ¬¡é
ç½® |
| | | </el-button> |
| | | <el-button size="small" |
| | | type="success" |
| | | @click="handleDown" |
| | | :loading="downLoading" |
| | | :icon="Download" |
| | | style="margin-left: 8px"> |
| | | å¯¼åº |
| | | </el-button> |
| | | <el-button size="small" |
| | | type="warning" |
| | | @click="schedulingVisible = true" |
| | | :icon="Calendar" |
| | | style="margin-left: 8px"> |
| | | æç |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="scheduling-container" |
| | | v-loading="pageLoading"> |
| | | <!-- æåº¦æç --> |
| | | <div class="scheduling-table" |
| | | v-show="query.month"> |
| | | <div class="scheduling-left"> |
| | | <div class="scheduling-header"> |
| | | 人ååç§° |
| | | </div> |
| | | <div class="scheduling-user" |
| | | :class="{ 'scheduling-user-hover': currentUserIndex == index }" |
| | | v-for="(item, index) in listForm" |
| | | :key="'e' + index" |
| | | @mouseenter="onMouseEnter(index)" |
| | | @mouseleave="currentUserIndex = null"> |
| | | <div class="user-avatar"> |
| | | {{ item.name ? item.name.charAt(0) : "" }} |
| | | </div> |
| | | <div class="user-details"> |
| | | <h4 class="user-name">{{ item.name }}</h4> |
| | | <!-- <div class="user-stats"> |
| | | <span class="stat-item">æ©:{{ item.day0 }}</span> |
| | | <span class="stat-item">ä¸:{{ item.day1 }}</span> |
| | | <span class="stat-item">å¤:{{ item.day2 }}</span> |
| | | <span class="stat-item">ä¼:{{ item.day3 }}</span> |
| | | <span class="stat-item">å:{{ item.day4 }}</span> |
| | | <span class="stat-item">å·®:{{ item.day6 }}</span> |
| | | </div> --> |
| | | <div class="user-total"> |
| | | <span class="total-label">å计åºå¤:</span> |
| | | <span class="total-value">{{ item.monthlyAttendance.totalAttendance }}天</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="scheduling-right"> |
| | | <div class="scheduling-calendar"> |
| | | <div class="calendar-header"> |
| | | <div class="calendar-header-item" |
| | | v-for="(item, index) in weeks" |
| | | :key="'b' + index"> |
| | | <span class="week-number" |
| | | v-if="item.week == '卿¥'">{{ item.weekNum }}å¨</span> |
| | | <div class="day-info"> |
| | | <span class="day-number">{{ item.day }}</span> |
| | | <span class="day-week">{{ item.week.charAt(1) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="calendar-body"> |
| | | <div class="calendar-row" |
| | | v-for="(item, index) in listForm" |
| | | :key="'c' + index" |
| | | :class="{ 'calendar-row-hover': currentUserIndex == index }" |
| | | @mouseenter="onMouseEnter(index)" |
| | | @mouseleave="currentUserIndex = null"> |
| | | <div class="calendar-cell" |
| | | v-for="(m, i) in item.list" |
| | | :key="'d' + i"> |
| | | <el-dropdown trigger="click" |
| | | placement="bottom" |
| | | @command="(e) => handleCommand(e, m)" |
| | | class="shift-dropdown"> |
| | | <div class="shift-box" |
| | | :class="{ |
| | | 'shift-box-early': m.shift === 'æ©ç', |
| | | 'shift-box-mid': m.shift === 'ä¸ç', |
| | | 'shift-box-night': m.shift === 'å¤ç', |
| | | 'shift-box-rest': m.shift === '伿¯', |
| | | 'shift-box-leave': m.shift === '请å', |
| | | 'shift-box-other': m.shift === 'å¤11', |
| | | 'shift-box-business': m.shift === 'å¤12', |
| | | }"> |
| | | <span class="shift-text">{{ getShiftNameByValue(m.shift) || 'â' }}</span> |
| | | </div> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item v-for="(n, j) in classType" |
| | | :key="'h' + j" |
| | | :command="n.id">{{ n.shift || 'â' |
| | | }}</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 年度æç --> |
| | | <div class="yearly-table" |
| | | v-show="!query.month"> |
| | | <div class="scheduling-left"> |
| | | <div class="scheduling-header"> |
| | | 人ååç§° |
| | | </div> |
| | | <div class="scheduling-user" |
| | | :class="{ 'scheduling-user-hover': currentUserIndex == index }" |
| | | v-for="(item, index) in yearList" |
| | | :key="'e' + index" |
| | | @mouseenter="onMouseEnter(index)" |
| | | @mouseleave="currentUserIndex = null"> |
| | | <div class="user-avatar"> |
| | | {{ item.name ? item.name.charAt(0) : "" }} |
| | | </div> |
| | | <div class="user-details"> |
| | | <h4 class="user-name">{{ item.name }}</h4> |
| | | <!-- <div class="user-stats"> |
| | | <span class="stat-item">æ©:{{ item.day0 }}</span> |
| | | <span class="stat-item">ä¸:{{ item.day1 }}</span> |
| | | <span class="stat-item">å¤:{{ item.day2 }}</span> |
| | | <span class="stat-item">ä¼:{{ item.day3 }}</span> |
| | | <span class="stat-item">å:{{ item.day4 }}</span> |
| | | <span class="stat-item">å·®:{{ item.day6 }}</span> |
| | | </div> --> |
| | | <div class="user-total"> |
| | | <span class="total-label">å计åºå¤:</span> |
| | | <span class="total-value">{{ item.work_time }}天</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="scheduling-right"> |
| | | <div class="yearly-calendar"> |
| | | <div class="yearly-header"> |
| | | <div class="yearly-header-item" |
| | | v-for="(item, index) in monthList" |
| | | :key="'b' + index"> |
| | | <span class="month-name">{{ item }}æ</span> |
| | | </div> |
| | | </div> |
| | | <div class="yearly-body"> |
| | | <div class="yearly-row" |
| | | v-for="(item, index) in yearList" |
| | | :key="'c' + index" |
| | | :class="{ 'calendar-row-hover': currentUserIndex == index }" |
| | | @mouseenter="onMouseEnter(index)" |
| | | @mouseleave="currentUserIndex = null"> |
| | | <div class="yearly-cell" |
| | | v-for="(m, i) in item.monthList" |
| | | :key="'d' + i"> |
| | | <div class="monthly-attendance"> |
| | | <span class="attendance-label">å计åºå¤ï¼</span> |
| | | <span class="attendance-value">{{ m.totalMonthAttendance }}</span> |
| | | </div> |
| | | <!-- <div class="monthly-stats"> |
| | | <span class="stat-item">æ©:{{ m.day0 }}</span> |
| | | <span class="stat-item">ä¸:{{ m.day1 }}</span> |
| | | <span class="stat-item">å¤:{{ m.day2 }}</span> |
| | | <span class="stat-item">ä¼:{{ m.day3 }}</span> |
| | | <span class="stat-item">å:{{ m.day4 }}</span> |
| | | <span class="stat-item">å·®:{{ m.day6 }}</span> |
| | | </div> --> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div style="display: flex; justify-content: flex-end; margin-top: 10px; margin-right: 30px"> |
| | | <el-pagination background |
| | | @current-change="currentChange" |
| | | :page-size="pageSize" |
| | | :current-page="currentPage" |
| | | layout="total, prev, pager, next, jumper" |
| | | :total="total"> |
| | | </el-pagination> |
| | | </div> |
| | | <el-dialog title="æç" |
| | | v-model="schedulingVisible" |
| | | width="400px"> |
| | | <div class="search_thing"> |
| | | <div class="search_label" |
| | | style="width: 90px"> |
| | | <span style="color: red; margin-right: 4px">*</span>卿¬¡ï¼ |
| | | </div> |
| | | <div class="search_input"> |
| | | <el-date-picker v-model="schedulingQuery.week" |
| | | type="week" |
| | | format="YYYY 第 ww å¨" |
| | | placeholder="鿩卿¬¡" |
| | | style="width: 100%"> |
| | | </el-date-picker> |
| | | </div> |
| | | </div> |
| | | <div class="search_thing"> |
| | | <div class="search_label" |
| | | style="width: 90px"> |
| | | <span style="color: red; margin-right: 4px">*</span>人ååç§°ï¼ |
| | | </div> |
| | | <div class="search_input"> |
| | | <el-select v-model="schedulingQuery.userId" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | multiple |
| | | clearable |
| | | collapse-tags> |
| | | <el-option v-for="item in personList" |
| | | :key="item.id" |
| | | :label="item.staffName" |
| | | :value="item.id"> |
| | | </el-option> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <div class="search_thing"> |
| | | <div class="search_label" |
| | | style="width: 90px"> |
| | | <span style="color: red; margin-right: 4px">*</span>çæ¬¡ï¼ |
| | | </div> |
| | | <div class="search_input"> |
| | | <el-select v-model="schedulingQuery.shift" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in classType" |
| | | :key="item.id" |
| | | :label="getShiftNameByValue(item.shift)" |
| | | :value="item.id"> |
| | | </el-option> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="schedulingVisible = false">å æ¶</el-button> |
| | | <el-button type="primary" |
| | | @click="confirmScheduling" |
| | | :loading="loading">ç¡® å®</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import { |
| | | page, |
| | | pageYear, |
| | | add, |
| | | exportFile, |
| | | update, |
| | | staffOnJobListPage, |
| | | } from "@/api/personnelManagement/class"; |
| | | import { deptTreeSelect } from "@/api/system/user.js"; |
| | | import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js"; |
| | | import { useDict } from "@/utils/dict"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const router = useRouter(); |
| | | |
| | | // æ¥è¯¢æ¡ä»¶ |
| | | const query = reactive({ |
| | | userName: "", |
| | | sysDeptId: "", |
| | | year: new Date(), |
| | | month: new Date().getMonth() + 1, |
| | | }); |
| | | // è·åçæ¬¡åå
¸å¼ |
| | | const { shifts_list } = useDict("shifts_list"); |
| | | // æä»½é项 |
| | | const monthOptions = [ |
| | | { value: 1, label: "1æ" }, |
| | | { value: 2, label: "2æ" }, |
| | | { value: 3, label: "3æ" }, |
| | | { value: 4, label: "4æ" }, |
| | | { value: 5, label: "5æ" }, |
| | | { value: 6, label: "6æ" }, |
| | | { value: 7, label: "7æ" }, |
| | | { value: 8, label: "8æ" }, |
| | | { value: 9, label: "9æ" }, |
| | | { value: 10, label: "10æ" }, |
| | | { value: 11, label: "11æ" }, |
| | | { value: 12, label: "12æ" }, |
| | | ]; |
| | | |
| | | // é¨é¨å表 |
| | | const deptOptions = ref([]); |
| | | |
| | | // å¨å表 |
| | | const weeks = ref([]); |
| | | |
| | | // çæ¬¡ç±»å |
| | | const classType = ref([]); |
| | | |
| | | // å½åç¨æ·ç´¢å¼ |
| | | const currentUserIndex = ref(null); |
| | | |
| | | // æçå¼¹çªæ¾ç¤ºç¶æ |
| | | const schedulingVisible = ref(false); |
| | | |
| | | // 人åå表 |
| | | const personList = ref([]); |
| | | |
| | | // å è½½ç¶æ |
| | | const loading = ref(false); |
| | | |
| | | // æçæ¥è¯¢æ¡ä»¶ |
| | | const schedulingQuery = reactive({ |
| | | week: "", |
| | | userId: null, |
| | | shift: "", |
| | | }); |
| | | |
| | | // åè¡¨æ°æ® |
| | | const listForm = ref([]); |
| | | |
| | | // å½å页 |
| | | const currentPage = ref(1); |
| | | |
| | | // æ¯é¡µæ¡æ° |
| | | const pageSize = ref(6); |
| | | |
| | | // æ»æ¡æ° |
| | | const total = ref(3); |
| | | |
| | | // 页é¢å è½½ç¶æ |
| | | const pageLoading = ref(false); |
| | | |
| | | // æä»½å表 |
| | | const monthList = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); |
| | | |
| | | // 年度å表 |
| | | const yearList = ref([]); |
| | | |
| | | // 导åºå è½½ç¶æ |
| | | const downLoading = ref(false); |
| | | |
| | | // è·åé¨é¨å表 |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then(response => { |
| | | deptOptions.value = filterDisabledDept( |
| | | JSON.parse(JSON.stringify(response.data)) |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | // æ ¹æ®ç次å¼è·åçæ¬¡åç§° |
| | | const getShiftNameByValue = value => { |
| | | if (!value) return ""; |
| | | const shift = shifts_list.value.find(item => item.value === value); |
| | | return shift ? shift.label : value; |
| | | }; |
| | | |
| | | // è¿æ»¤ç¦ç¨çé¨é¨ |
| | | const filterDisabledDept = deptList => { |
| | | return deptList.filter(dept => { |
| | | if (dept.disabled) { |
| | | return false; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | }; |
| | | |
| | | // å·æ° |
| | | const refresh = () => { |
| | | listForm.value = []; |
| | | yearList.value = []; |
| | | currentPage.value = 1; |
| | | query.userName = ""; |
| | | query.sysDeptId = ""; |
| | | query.year = new Date(); |
| | | query.month = new Date().getMonth() + 1; |
| | | if (query.month) { |
| | | init(); |
| | | } else { |
| | | initYear(); |
| | | } |
| | | }; |
| | | |
| | | // å·æ°è¡¨æ ¼ |
| | | const refreshTable = () => { |
| | | currentPage.value = 1; |
| | | if (query.month) { |
| | | listForm.value = []; |
| | | init(); |
| | | } else { |
| | | yearList.value = []; |
| | | initYear(); |
| | | } |
| | | }; |
| | | |
| | | // é¡µç æ¹å |
| | | const currentChange = num => { |
| | | currentPage.value = num; |
| | | if (query.month) { |
| | | init(); |
| | | } else { |
| | | initYear(); |
| | | } |
| | | }; |
| | | |
| | | // æ°åè½¬ä¸æ |
| | | const transFromNumber = num => { |
| | | let changeNum = ["é¶", "ä¸", "äº", "ä¸", "å", "äº", "å
", "ä¸", "å
«", "ä¹"]; |
| | | let unit = ["", "å", "ç¾", "å", "ä¸"]; |
| | | num = parseInt(num); |
| | | let getWan = temp => { |
| | | let strArr = temp.toString().split("").reverse(); |
| | | let newNum = ""; |
| | | for (var i = 0; i < strArr.length; i++) { |
| | | newNum = |
| | | (i == 0 && strArr[i] == 0 |
| | | ? "" |
| | | : i > 0 && strArr[i] == 0 && strArr[i - 1] == 0 |
| | | ? "" |
| | | : changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i])) + |
| | | newNum; |
| | | } |
| | | return newNum; |
| | | }; |
| | | let overWan = Math.floor(num / 10000); |
| | | let noWan = num % 10000; |
| | | if (noWan.toString().length < 4) noWan = "0" + noWan; |
| | | return overWan ? getWan(overWan) + "ä¸" + getWan(noWan) : getWan(num); |
| | | }; |
| | | |
| | | // åå§åæåº¦æ°æ® |
| | | const init = () => { |
| | | pageLoading.value = true; |
| | | console.log(query.year, "query.year"); |
| | | let year = query.year.getFullYear(); |
| | | let month0 = query.month ? query.month : new Date().getMonth() + 1; |
| | | let month = month0 > 9 ? month0 : "0" + month0; |
| | | page({ |
| | | size: pageSize.value, |
| | | current: currentPage.value, |
| | | time: year + "-" + month + "-01 00:00:00", |
| | | userName: query.userName, |
| | | sysDeptId: query.sysDeptId, |
| | | }) |
| | | .then(res => { |
| | | pageLoading.value = false; |
| | | total.value = res.data.page.total; |
| | | listForm.value = res.data.page.records.map(item => { |
| | | for (let key in item.monthlyAttendance) { |
| | | let type = getDayByDic(key); |
| | | if (type != undefined || type != null) { |
| | | item[`day${type}`] = item.monthlyAttendance[key]; |
| | | } |
| | | } |
| | | return item; |
| | | }); |
| | | let headerList = res.data.headerList; |
| | | weeks.value = []; |
| | | headerList.forEach(item => { |
| | | let obj = { |
| | | weekNum: item.weekly, |
| | | week: item.headerTime.split(" ")[1], |
| | | day: item.headerTime.split(" ")[0], |
| | | }; |
| | | weeks.value.push(obj); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | pageLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // åå§åå¹´åº¦æ°æ® |
| | | const initYear = () => { |
| | | pageLoading.value = true; |
| | | let year = query.year.getFullYear(); |
| | | pageYear({ |
| | | size: pageSize.value, |
| | | current: currentPage.value, |
| | | time: year + "-01-01 00:00:00", |
| | | userName: query.userName, |
| | | sysDeptId: query.sysDeptId, |
| | | }).then(res => { |
| | | pageLoading.value = false; |
| | | total.value = res.data.total; |
| | | yearList.value = res.data.records.map(item => { |
| | | for (let key in item.year) { |
| | | let type = getDayByDic(key); |
| | | if (type != undefined || type != null) { |
| | | item[`day${type}`] = item.year[key]; |
| | | } |
| | | } |
| | | item.monthList = []; |
| | | for (let m in item.month) { |
| | | let obj = {}; |
| | | for (let key in item.month[m]) { |
| | | let type = getDayByDic(key); |
| | | if (type != undefined || type != null) { |
| | | obj[`day${type}`] = item.month[m][key]; |
| | | } |
| | | } |
| | | obj.totalMonthAttendance = item.month[m].totalMonthAttendance; |
| | | item.monthList.push(obj); |
| | | } |
| | | return item; |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // é¼ æ è¿å
¥ |
| | | const onMouseEnter = index => { |
| | | currentUserIndex.value = index; |
| | | }; |
| | | |
| | | // 确认æç |
| | | const confirmScheduling = () => { |
| | | if (!schedulingQuery.week) { |
| | | proxy.$modal.msgError("è¯·éæ©å¨æ¬¡"); |
| | | return; |
| | | } |
| | | let time = schedulingQuery.week.getTime(); |
| | | |
| | | // æ ¼å¼åæ¥æä¸º YYYY-MM-DD æ ¼å¼ |
| | | const formatDate = date => { |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, "0"); |
| | | const day = String(date.getDate()).padStart(2, "0"); |
| | | return `${year}-${month}-${day}`; |
| | | }; |
| | | |
| | | let startWeek = |
| | | formatDate(new Date(time - 24 * 60 * 60 * 1000)) + " 00:00:00"; |
| | | let endWeek = |
| | | formatDate(new Date(time + 24 * 60 * 60 * 1000 * 5)) + " 00:00:00"; |
| | | |
| | | if (!schedulingQuery.userId || schedulingQuery.userId.length == 0) { |
| | | proxy.$modal.msgError("è¯·éæ©äººå"); |
| | | return; |
| | | } |
| | | if (!schedulingQuery.shift) { |
| | | proxy.$modal.msgError("è¯·éæ©çæ¬¡"); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | add({ |
| | | startWeek, |
| | | endWeek, |
| | | staffOnJobId: schedulingQuery.userId.join(","), |
| | | personalAttendanceLocationConfigId: schedulingQuery.shift, |
| | | }) |
| | | .then(res => { |
| | | loading.value = false; |
| | | proxy.$modal.msgSuccess("æä½æå"); |
| | | schedulingVisible.value = false; |
| | | schedulingQuery.week = ""; |
| | | schedulingQuery.userId = null; |
| | | schedulingQuery.shift = ""; |
| | | refresh(); |
| | | }) |
| | | .catch(err => { |
| | | loading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // æ¶é´é
ç½® |
| | | const configTime = () => { |
| | | // 跳转å°è夿å¡é¡µé¢ |
| | | router.push({ |
| | | path: "/personnelManagement/checkinRules", |
| | | }); |
| | | }; |
| | | |
| | | // 夿æ¯å¦ä¸ºç©ºå¯¹è±¡ |
| | | const isObjectEmpty = obj => { |
| | | return Object.keys(obj).some(key => !obj[key]); |
| | | }; |
| | | |
| | | // å¯¼åº |
| | | const handleDown = () => { |
| | | let year = query.year.getFullYear(); |
| | | let time = ""; |
| | | if (query.month) { |
| | | let month = query.month > 9 ? query.month : "0" + query.month; |
| | | time = year + "-" + month + "-01 00:00:00"; |
| | | } else { |
| | | time = year + "-01-01 00:00:00"; |
| | | } |
| | | downLoading.value = true; |
| | | exportFile({ |
| | | time, |
| | | userName: query.userName, |
| | | sysDeptId: query.sysDeptId, |
| | | isMonth: query.month ? true : false, |
| | | }) |
| | | .then(res => { |
| | | proxy.$modal.msgSuccess("ä¸è½½æå"); |
| | | downLoading.value = false; |
| | | const blob = new Blob([res], { |
| | | type: "application/force-download", |
| | | }); |
| | | let fileName = ""; |
| | | if (query.month) { |
| | | fileName = year + "-" + query.month + " çæ¬¡ä¿¡æ¯"; |
| | | } else { |
| | | fileName = year + " çæ¬¡æ±æ»"; |
| | | } |
| | | proxy.$download.saveAs(blob, fileName + ".xlsx"); |
| | | }) |
| | | .catch(err => { |
| | | downLoading.value = false; |
| | | }); |
| | | }; |
| | | // å¤çå½ä»¤ |
| | | const handleCommand = (e, m) => { |
| | | // if (e != m.shift) { |
| | | update({ |
| | | id: m.id, |
| | | personalAttendanceLocationConfigId: e, |
| | | }).then(res => { |
| | | proxy.$modal.msgSuccess("æä½æå"); |
| | | // m.shift = e; |
| | | if (query.month) { |
| | | init(); |
| | | } else { |
| | | initYear(); |
| | | } |
| | | }); |
| | | // } |
| | | }; |
| | | // æ¥è¯¢è§åå表 |
| | | const fetchData = () => { |
| | | getAttendanceRules({ current: -1, size: -1 }).then(res => { |
| | | classType.value = res.data.records; |
| | | }); |
| | | }; |
| | | // è·åç¨æ· |
| | | const getUsers = () => { |
| | | // selectUserCondition({ type: 1 }).then(res => { |
| | | // let arr = res.data; |
| | | // personList.value = arr; |
| | | // }); |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1, |
| | | }).then(res => { |
| | | let arr = res.data.records; |
| | | personList.value = arr; |
| | | }); |
| | | }; |
| | | |
| | | // æ ¹æ®åå
¸è·åæ¥æ |
| | | const getDayByDic = e => { |
| | | let obj = classType.value.find(m => m.shift == e); |
| | | if (obj) { |
| | | return obj.id; |
| | | } |
| | | }; |
| | | |
| | | // æ ¹æ®åå
¸è·åçæ¬¡ |
| | | const getShiftByDic = e => { |
| | | let obj = classType.value.find(m => m.shift == e); |
| | | if (obj) { |
| | | return obj.shift; |
| | | } |
| | | return "æ "; |
| | | }; |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | fetchData(); |
| | | getUsers(); |
| | | fetchDeptOptions(); |
| | | if (query.month) { |
| | | init(); |
| | | } else { |
| | | initYear(); |
| | | } |
| | | monthList.value = []; |
| | | for (let i = 12; i > 0; i--) { |
| | | monthList.value.push(i); |
| | | } |
| | | monthList.value.reverse(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .class-page { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .form_title { |
| | | height: 36px; |
| | | display: flex; |
| | | flex-direction: row; |
| | | justify-content: space-between; |
| | | font-weight: 800; |
| | | } |
| | | |
| | | /* æç´¢åºåæ ·å¼ */ |
| | | .search-container { |
| | | background: #f9fafb; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | margin-bottom: 20px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .search-form { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .search-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16px; |
| | | flex-wrap: nowrap; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | .search-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | } |
| | | |
| | | .search-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #333; |
| | | min-width: 65px; |
| | | text-align: right; |
| | | } |
| | | |
| | | .search-input-group { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .search-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | .search-buttons { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | flex: 1; |
| | | } |
| | | |
| | | /* ååºå¼è°æ´ */ |
| | | @media (max-width: 1200px) { |
| | | .search-row { |
| | | gap: 12px; |
| | | } |
| | | |
| | | .search-item { |
| | | gap: 4px; |
| | | } |
| | | |
| | | .search-label { |
| | | min-width: 60px; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .search-actions { |
| | | margin-left: 4px; |
| | | } |
| | | |
| | | .search-buttons { |
| | | margin-left: 12px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 992px) { |
| | | .search-row { |
| | | flex-wrap: wrap; |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .search-buttons { |
| | | margin-left: 0; |
| | | margin-top: 12px; |
| | | width: 100%; |
| | | justify-content: flex-start; |
| | | } |
| | | } |
| | | |
| | | /* æçå®¹å¨ */ |
| | | .scheduling-container { |
| | | width: 100%; |
| | | min-height: calc(100vh - 280px); |
| | | background-color: #fff; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | overflow: hidden; |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* æçè¡¨æ ¼ */ |
| | | .scheduling-table { |
| | | display: flex; |
| | | width: 100%; |
| | | height: calc(100vh - 280px); |
| | | } |
| | | |
| | | /* 左侧人åä¿¡æ¯ */ |
| | | .scheduling-left { |
| | | width: 240px; |
| | | min-width: 240px; |
| | | background-color: #f9fafb; |
| | | border-right: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | /* å³ä¾§æçå
容 */ |
| | | .scheduling-right { |
| | | flex: 1; |
| | | overflow-x: auto; |
| | | } |
| | | |
| | | /* 表头 */ |
| | | .scheduling-header { |
| | | height: 48px; |
| | | line-height: 48px; |
| | | padding: 0 20px; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | background-color: #f3f4f6; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | /* 人åä¿¡æ¯è¡ */ |
| | | .scheduling-user { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 10px; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | transition: all 0.3s ease; |
| | | height: 65px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .scheduling-user:hover, |
| | | .scheduling-user-hover { |
| | | background-color: rgba(59, 130, 246, 0.05); |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | /* ç¨æ·å¤´å */ |
| | | .user-avatar { |
| | | width: 42px; |
| | | height: 42px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, #3b82f6, #60a5fa); |
| | | color: #fff; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | text-align: center; |
| | | line-height: 42px; |
| | | margin-right: 16px; |
| | | box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .scheduling-user:hover .user-avatar { |
| | | transform: scale(1.05); |
| | | box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3); |
| | | } |
| | | |
| | | /* ç¨æ·è¯¦æ
*/ |
| | | .user-details { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | height: 100%; |
| | | } |
| | | |
| | | /* ç¨æ·å */ |
| | | .user-name { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | margin: 0 0 6px 0; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | /* ç¨æ·ç»è®¡ */ |
| | | .user-stats { |
| | | /* display: flex; */ |
| | | /* flex-wrap: wrap; |
| | | gap: 10px; */ |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .stat-item { |
| | | font-size: 12px; |
| | | color: #666; |
| | | /* background-color: #f9fafb; */ |
| | | /* padding: 2px 8px; */ |
| | | padding-right: 4px; |
| | | /* border-radius: 10px; */ |
| | | /* border: 1px solid #e5e7eb; */ |
| | | /* transition: all 0.3s ease; */ |
| | | } |
| | | |
| | | .scheduling-user:hover .stat-item { |
| | | background-color: rgba(59, 130, 246, 0.1); |
| | | border-color: rgba(59, 130, 246, 0.3); |
| | | } |
| | | |
| | | /* å计åºå¤ */ |
| | | .user-total { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .total-label { |
| | | font-size: 12px; |
| | | color: #666; |
| | | margin-right: 6px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .total-value { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #3b82f6; |
| | | background-color: rgba(59, 130, 246, 0.1); |
| | | padding: 2px 10px; |
| | | border-radius: 10px; |
| | | border: 1px solid rgba(59, 130, 246, 0.2); |
| | | } |
| | | |
| | | /* æ¥åå¤´é¨ */ |
| | | .calendar-header { |
| | | display: flex; |
| | | |
| | | border-bottom: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | .calendar-header-item { |
| | | width: 50px; |
| | | min-width: 50px; |
| | | height: 48px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-right: 1px solid #e5e7eb; |
| | | background-color: #f3f4f6; |
| | | position: relative; |
| | | } |
| | | |
| | | .week-number { |
| | | position: absolute; |
| | | top: 6px; |
| | | font-size: 10px; |
| | | font-weight: 600; |
| | | color: #3b82f6; |
| | | background-color: #dbeafe; |
| | | padding: 3px 6px; |
| | | border-radius: 12px; |
| | | box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .week-number:hover { |
| | | background-color: #3b82f6; |
| | | color: #fff; |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .day-info { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | } |
| | | |
| | | .day-number { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #333; |
| | | } |
| | | |
| | | .day-week { |
| | | font-size: 12px; |
| | | color: #666; |
| | | } |
| | | |
| | | /* æ¥åä¸»ä½ */ |
| | | .calendar-body { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | /* æ¥åè¡ */ |
| | | .calendar-row { |
| | | display: flex; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .calendar-row:hover, |
| | | .calendar-row-hover { |
| | | background-color: rgba(59, 130, 246, 0.03); |
| | | } |
| | | |
| | | /* æ¥ååå
æ ¼ */ |
| | | .calendar-cell { |
| | | width: 50px; |
| | | min-width: 50px; |
| | | height: 65px; |
| | | border-right: 1px solid #e5e7eb; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | /* çæ¬¡ä¸ææ¡ */ |
| | | .shift-dropdown { |
| | | width: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | /* çæ¬¡æ¡ */ |
| | | .shift-box { |
| | | width: 90%; |
| | | height: 80%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 6px; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .shift-box:hover { |
| | | transform: scale(1.05); |
| | | } |
| | | |
| | | /* çæ¬¡ç±»åæ ·å¼ */ |
| | | .shift-box-early { |
| | | background: rgba(59, 130, 246, 0.15); |
| | | color: #3b82f6; |
| | | } |
| | | |
| | | .shift-box-mid { |
| | | background: rgba(139, 92, 246, 0.15); |
| | | color: #8b5cf6; |
| | | } |
| | | |
| | | .shift-box-night { |
| | | background: rgba(245, 158, 11, 0.15); |
| | | color: #f59e0b; |
| | | } |
| | | |
| | | .shift-box-rest { |
| | | background: rgba(16, 185, 129, 0.15); |
| | | color: #10b981; |
| | | } |
| | | |
| | | .shift-box-leave { |
| | | background: rgba(239, 68, 68, 0.15); |
| | | color: #ef4444; |
| | | } |
| | | |
| | | .shift-box-other { |
| | | background: rgba(236, 72, 153, 0.15); |
| | | color: #ec4899; |
| | | } |
| | | |
| | | .shift-box-business { |
| | | background: rgba(17, 24, 39, 0.15); |
| | | color: #111827; |
| | | } |
| | | |
| | | /* çæ¬¡ææ¬ */ |
| | | .shift-text { |
| | | text-align: center; |
| | | } |
| | | |
| | | /* å¹´åº¦è¡¨æ ¼ */ |
| | | .yearly-table { |
| | | display: flex; |
| | | width: 100%; |
| | | height: calc(100vh - 280px); |
| | | } |
| | | |
| | | /* 年度æ¥å */ |
| | | .yearly-calendar { |
| | | width: 100%; |
| | | } |
| | | |
| | | /* 年度表头 */ |
| | | .yearly-header { |
| | | display: grid; |
| | | grid-template-columns: repeat(12, 1fr); |
| | | background-color: #f3f4f6; |
| | | border-bottom: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | .yearly-header-item { |
| | | height: 48px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-right: 1px solid #e5e7eb; |
| | | } |
| | | |
| | | .month-name { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #333; |
| | | } |
| | | |
| | | /* å¹´åº¦ä¸»ä½ */ |
| | | .yearly-body { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | /* å¹´åº¦è¡ */ |
| | | .yearly-row { |
| | | display: grid; |
| | | grid-template-columns: repeat(12, 1fr); |
| | | border-bottom: 1px solid #e5e7eb; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | /* 年度åå
æ ¼ */ |
| | | .yearly-cell { |
| | | padding: 12px; |
| | | border-right: 1px solid #e5e7eb; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | } |
| | | |
| | | /* æåº¦åºå¤ */ |
| | | .monthly-attendance { |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .attendance-label { |
| | | font-size: 12px; |
| | | color: #666; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .attendance-value { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | |
| | | /* æåº¦ç»è®¡ */ |
| | | .monthly-stats { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 4px; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .monthly-stats .stat-item { |
| | | font-size: 11px; |
| | | } |
| | | |
| | | /* æ»å¨æ¡æ ·å¼ */ |
| | | .scheduling-right::-webkit-scrollbar { |
| | | height: 8px; |
| | | } |
| | | |
| | | .scheduling-right::-webkit-scrollbar-track { |
| | | background: #f1f1f1; |
| | | } |
| | | |
| | | .scheduling-right::-webkit-scrollbar-thumb { |
| | | background: #c1c1c1; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .scheduling-right::-webkit-scrollbar-thumb:hover { |
| | | background: #a8a8a8; |
| | | } |
| | | |
| | | .search_label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #333; |
| | | margin-bottom: 8px; |
| | | margin-top: 12px; |
| | | } |
| | | </style> |
| | |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¦»èæ¥æï¼" prop="leaveDate"> |
| | | <el-date-picker |
| | | v-model="form.leaveDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©ç¦»èæ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="离èåå ï¼" prop="reason"> |
| | | <el-select v-model="form.reason" placeholder="è¯·éæ©ç¦»èåå " style="width: 100%" @change="handleSelectDimissionReason"> |
| | | <el-option |
| | |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="夿³¨ï¼" prop="remark" v-if="form.reason === 'other'"> |
| | | <el-input |
| | |
| | | const data = reactive({ |
| | | form: { |
| | | staffOnJobId: undefined, |
| | | leaveDate: "", |
| | | reason: "", |
| | | remark: "", |
| | | }, |
| | | rules: { |
| | | staffName: [{ required: true, message: "è¯·éæ©äººå" }], |
| | | leaveDate: [{ required: true, message: "è¯·éæ©ç¦»èæ¥æ", trigger: "change" }], |
| | | reason: [{ required: true, message: "è¯·éæ©ç¦»èåå "}], |
| | | }, |
| | | dimissionReasonOptions: [ |
| | |
| | | if (operationType.value === 'edit') { |
| | | currentStaffRecord.value = row |
| | | form.value.staffOnJobId = row.staffOnJobId |
| | | form.value.leaveDate = row.leaveDate |
| | | form.value.reason = row.reason |
| | | form.value.remark = row.remark |
| | | personList.value = [ |
| | |
| | | // 表å已注éï¼æå¨éç½®è¡¨åæ°æ® |
| | | form.value = { |
| | | staffOnJobId: undefined, |
| | | leaveDate: "", |
| | | reason: "", |
| | | remark: "", |
| | | }; |
| | |
| | | }, |
| | | }, |
| | | { |
| | | label: "ç¦»èæ¥æ", |
| | | prop: "leaveDate", |
| | | }, |
| | | { |
| | | label: "åå·¥ç¼å·", |
| | | prop: "staffNo", |
| | | }, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | åºæ¬ä¿¡æ¯ |
| | | </span> |
| | | </template> |
| | | |
| | | <el-row :gutter="24"> |
| | | <el-col :span="5"> |
| | | <el-form-item label="åå·¥ç¼å·" prop="staffNo"> |
| | | <el-input |
| | | v-model="form.staffNo" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | :disabled="operationType !== 'add'" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="å§å" prop="staffName"> |
| | | <el-input |
| | | v-model="form.staffName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="å«å" prop="alias"> |
| | | <el-input |
| | | v-model="form.alias" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="ææº" prop="phone"> |
| | | <el-input |
| | | v-model="form.phone" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="11" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="æ§å«" prop="sex"> |
| | | <el-select |
| | | v-model="form.sex" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="ç·" value="ç·" /> |
| | | <el-option label="女" value="女" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="24"> |
| | | <el-col :span="5"> |
| | | <el-form-item label="åºçæ¥æ" prop="birthDate"> |
| | | <el-date-picker |
| | | v-model="form.birthDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="å¹´é¾" prop="age"> |
| | | <el-input-number |
| | | v-model="form.age" |
| | | :min="0" |
| | | :max="150" |
| | | :precision="0" |
| | | :step="1" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="ç±è´¯" prop="nativePlace"> |
| | | <el-input |
| | | v-model="form.nativePlace" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="5"> |
| | | <el-form-item label="æ°æ" prop="nation"> |
| | | <el-input |
| | | v-model="form.nation" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="4"> |
| | | <el-form-item label="å©å§»ç¶åµ" prop="maritalStatus"> |
| | | <el-select |
| | | v-model="form.maritalStatus" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="æªå©" value="æªå©" /> |
| | | <el-option label="å·²å©" value="å·²å©" /> |
| | | <el-option label="离å¼" value="离å¼" /> |
| | | <el-option label="丧å¶" value="丧å¶" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="24"> |
| | | <el-col :span="10"> |
| | | <el-form-item label="è§è²" prop="roleId"> |
| | | <el-select |
| | | v-model="form.roleId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in roleOptions" |
| | | :key="item.roleId" |
| | | :label="item.roleName" |
| | | :value="item.roleId" |
| | | :disabled="item.status == 1" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true }, |
| | | operationType: { type: String, default: "add" }, |
| | | roleOptions: { type: Array, default: () => [] }, |
| | | }); |
| | | |
| | | const { form, operationType, roleOptions } = toRefs(props); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <!-- æè²ç»å --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | æè²ç»å |
| | | </span> |
| | | </template> |
| | | <el-table :data="form.staffEducationList" border> |
| | | <el-table-column label="å¦å" prop="education" width="120"> |
| | | <template #default="{ row }"> |
| | | <el-select |
| | | v-model="row.education" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="ä¸ä¸å以ä¸" value="secondary" /> |
| | | <el-option label="大ä¸" value="junior_college" /> |
| | | <el-option label="æ¬ç§" value="bachelor" /> |
| | | <el-option label="ç¡å£«" value="master" /> |
| | | <el-option label="å士å以ä¸" value="doctor" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¯ä¸é¢æ ¡" prop="schoolName" min-width="160"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.schoolName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="30" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¥å¦æ¶é´" prop="enrollTime" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.enrollTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ¯ä¸æ¶é´" prop="graduateTime" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.graduateTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ä¸ä¸" prop="major" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.major" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¦ä½" prop="degree" width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.degree" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="form.staffEducationList.length > 1" |
| | | type="primary" |
| | | link |
| | | @click="removeEducationRow(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="table-add-row" @click="addEducationRow">æ°å»ºä¸è¡</div> |
| | | </el-card> |
| | | |
| | | <!-- å·¥ä½ç»å --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | å·¥ä½ç»å |
| | | </span> |
| | | </template> |
| | | <el-table :data="form.staffWorkExperienceList" border> |
| | | <el-table-column label="åå
¬å¸" prop="formerCompany" min-width="180"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.formerCompany" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="30" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åå
¬å¸é¨é¨" prop="formerDept" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.formerDept" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åå
¬å¸èä½" prop="formerPosition" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.formerPosition" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¼å§æ¥æ" prop="startDate" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.startDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç»ææ¥æ" prop="endDate" width="150"> |
| | | <template #default="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.endDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·¥ä½æè¿°" prop="workDesc" min-width="220"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.workDesc" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="500" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="form.staffWorkExperienceList.length > 1" |
| | | type="primary" |
| | | link |
| | | @click="removeWorkRow(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="table-add-row" @click="addWorkRow">æ°å»ºä¸è¡</div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:form"]); |
| | | |
| | | const { form } = toRefs(props); |
| | | |
| | | const addEducationRow = () => { |
| | | form.value.staffEducationList.push({ |
| | | education: "", |
| | | schoolName: "", |
| | | enrollTime: "", |
| | | graduateTime: "", |
| | | major: "", |
| | | degree: "", |
| | | }); |
| | | }; |
| | | |
| | | const removeEducationRow = (index) => { |
| | | if (form.value.staffEducationList.length <= 1) return; |
| | | form.value.staffEducationList.splice(index, 1); |
| | | }; |
| | | |
| | | const addWorkRow = () => { |
| | | form.value.staffWorkExperienceList.push({ |
| | | formerCompany: "", |
| | | formerDept: "", |
| | | formerPosition: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | workDesc: "", |
| | | }); |
| | | }; |
| | | |
| | | const removeWorkRow = (index) => { |
| | | if (form.value.staffWorkExperienceList.length <= 1) return; |
| | | form.value.staffWorkExperienceList.splice(index, 1); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .table-add-row { |
| | | margin-top: 8px; |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <!-- ç´§æ¥è系人 --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | ç´§æ¥è系人 |
| | | </span> |
| | | </template> |
| | | <el-table :data="form.staffEmergencyContactList" border> |
| | | <el-table-column label="ç´§æ¥è系人å§å" prop="contactName" min-width="160"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactName" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç´§æ¥è系人å
³ç³»" prop="contactRelation" min-width="140"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactRelation" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç´§æ¥èç³»äººææº" prop="contactPhone" width="160"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactPhone" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="11" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç´§æ¥è系人ä½å" prop="contactAddress" min-width="220"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.contactAddress" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | v-if="form.staffEmergencyContactList.length > 1" |
| | | type="primary" |
| | | link |
| | | @click="removeEmergencyRow(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="table-add-row" @click="addEmergencyRow">æ°å»ºä¸è¡</div> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true } |
| | | }); |
| | | |
| | | const { form } = toRefs(props); |
| | | |
| | | const addEmergencyRow = () => { |
| | | form.value.staffEmergencyContactList.push({ |
| | | contactName: "", |
| | | contactRelation: "", |
| | | contactPhone: "", |
| | | contactAddress: "", |
| | | }); |
| | | }; |
| | | |
| | | const removeEmergencyRow = (index) => { |
| | | if (form.value.staffEmergencyContactList.length <= 1) return; |
| | | form.value.staffEmergencyContactList.splice(index, 1); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .table-add-row { |
| | | margin-top: 8px; |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"> |
| | | <span class="card-title-line">|</span> |
| | | å¨èä¿¡æ¯ |
| | | </span> |
| | | </template> |
| | | |
| | | <!-- 第ä¸è¡ï¼ååå¼å§ / ååç»æ / è¯ç¨æ / è½¬æ£ --> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å
¥èæ¥æ" prop="contractStartTime"> |
| | | <el-date-picker |
| | | v-model="form.contractStartTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item |
| | | label="ååç»ææ¥æ" |
| | | prop="contractEndTime" |
| | | required |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©ååç»ææ¥æ', |
| | | trigger: 'change', |
| | | }, |
| | | ]" |
| | | > |
| | | <el-date-picker |
| | | v-model="form.contractEndTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="è¯ç¨æï¼æï¼" prop="probationPeriod"> |
| | | <el-input-number |
| | | v-model="form.proTerm" |
| | | :min="0" |
| | | :max="24" |
| | | :precision="0" |
| | | :step="1" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="è½¬æ£æ¥æ" prop="positiveDate"> |
| | | <el-date-picker |
| | | v-model="form.positiveDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 第äºè¡ï¼é¨é¨ / å²ä½ / åºæ¬å·¥èµ --> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="8"> |
| | | <el-form-item label="é¨é¨" prop="sysDeptId"> |
| | | <el-tree-select |
| | | v-model="form.sysDeptId" |
| | | :data="deptOptions" |
| | | check-strictly |
| | | :render-after-expand="false" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="å²ä½" prop="sysPostId"> |
| | | <el-select |
| | | v-model="form.sysPostId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in postOptions" |
| | | :key="item.postId" |
| | | :label="item.postName" |
| | | :value="item.postId" |
| | | :disabled="item.status === '1'" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <el-form-item label="åºæ¬å·¥èµ" prop="basicSalary"> |
| | | <el-input-number |
| | | v-model="form.basicSalary" |
| | | :min="0" |
| | | :max="999999" |
| | | :precision="2" |
| | | :step="100" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { toRefs } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | form: { type: Object, required: true }, |
| | | postOptions: { type: Array, default: () => [] }, |
| | | deptOptions: { type: Array, default: () => [] }, |
| | | }); |
| | | |
| | | const { form, postOptions, deptOptions } = toRefs(props); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | </style> |
| | | |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | :title="operationType === 'add' ? 'æ°å¢å
¥è' : 'ç¼è¾äººå'" |
| | | width="70%" |
| | | @close="closeDia" |
| | | > |
| | | <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åå·¥ç¼å·ï¼" prop="staffNo"> |
| | | <el-input v-model="form.staffNo" placeholder="请è¾å
¥" clearable :disabled="operationType !== 'add'"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å§åï¼" prop="staffName"> |
| | | <el-input v-model="form.staffName" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ§å«ï¼" prop="sex"> |
| | | <el-select v-model="form.sex"> |
| | | <el-option label="ç·" value="ç·" /> |
| | | <el-option label="女" value="女" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ·ç±ä½åï¼" prop="nativePlace"> |
| | | <el-input v-model="form.nativePlace" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å²ä½ï¼" prop="sysPostId"> |
| | | <el-select v-model="form.sysPostId" placeholder="è¯·éæ©å²ä½" clearable> |
| | | <el-option |
| | | v-for="item in postOptions" |
| | | :key="item.postId" |
| | | :label="item.postName" |
| | | :value="item.postId" |
| | | :disabled="item.status === '1'" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç°ä½åï¼" prop="adress"> |
| | | <el-input v-model="form.adress" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¨é¨ï¼" prop="sysDeptId"> |
| | | <el-tree-select |
| | | v-model="form.sysDeptId" |
| | | :data="deptOptions" |
| | | :props="{ value: 'id', label: 'label', children: 'children' }" |
| | | value-key="id" |
| | | placeholder="è¯·éæ©é¨é¨" |
| | | check-strictly |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¹´é¾ï¼" prop="age"> |
| | | <el-input-number v-model="form.age" :precision="0" :step="1" style="width: 100%"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="第ä¸å¦åï¼" prop="firstStudy"> |
| | | <el-input v-model="form.firstStudy" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸ä¸ï¼" prop="profession"> |
| | | <el-input v-model="form.profession" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="èç³»çµè¯ï¼" prop="phone"> |
| | | <el-input v-model="form.phone" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç´§æ¥è系人ï¼" prop="emergencyContact"> |
| | | <el-input v-model="form.emergencyContact" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç´§æ¥è系人èç³»çµè¯ï¼" prop="emergencyContactPhone"> |
| | | <el-input v-model="form.emergencyContactPhone" placeholder="请è¾å
¥" clearable/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ååå¹´éï¼" prop="contractTerm"> |
| | | <el-input-number v-model="form.contractTerm" :precision="0" :step="1" style="width: 100%" :disabled="true"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ååå¼å§æ¥æï¼" prop="contractStartTime"> |
| | | <el-date-picker |
| | | v-model="form.contractStartTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="calculateContractTerm" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ååç»ææ¥æï¼" prop="contractEndTime"> |
| | | <el-date-picker |
| | | v-model="form.contractEndTime" |
| | | type="date" |
| | | placeholder="è¯·éæ©æ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | @change="calculateContractTerm" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <FormDialog |
| | | v-model="dialogFormVisible" |
| | | :operation-type="operationType" |
| | | :title="dialogTitle" |
| | | width="90%" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia" |
| | | > |
| | | <div class="form-dia-body"> |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-position="top" |
| | | > |
| | | <BasicInfoSection |
| | | :form="form" |
| | | :operation-type="operationType" |
| | | :role-options="roleOptions" |
| | | /> |
| | | <JobInfoSection |
| | | :form="form" |
| | | :post-options="postOptions" |
| | | :dept-options="deptOptions" |
| | | /> |
| | | <EducationWorkSection :form="form" /> |
| | | <EmergencyAndAttachmentSection :form="form" /> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, onMounted} from "vue"; |
| | | import {findPostOptions} from "@/api/system/post.js"; |
| | | import {listDept} from "@/api/system/dept.js"; |
| | | import {staffOnJobInfo, createStaffOnJob, updateStaffOnJob} from "@/api/personnelManagement/staffOnJob.js"; |
| | | import {deptTreeSelect} from "@/api/system/user.js"; |
| | | const { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['close']) |
| | | import { |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | onMounted, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { findPostOptions } from "@/api/system/post.js"; |
| | | import { deptTreeSelect, getUser } from "@/api/system/user.js"; |
| | | import { |
| | | staffOnJobInfo, |
| | | createStaffOnJob, |
| | | updateStaffOnJob, |
| | | } from "@/api/personnelManagement/staffOnJob.js"; |
| | | |
| | | import BasicInfoSection from "./BasicInfoSection.vue"; |
| | | import JobInfoSection from "./JobInfoSection.vue"; |
| | | import EducationWorkSection from "./EducationWorkSection.vue"; |
| | | import EmergencyAndAttachmentSection from "./EmergencyAndAttachmentSection.vue"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const emit = defineEmits(["close"]); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | | const id = ref(0) |
| | | const data = reactive({ |
| | | form: { |
| | | staffNo: "", |
| | | staffName: "", |
| | | sex: "", |
| | | nativePlace: "", |
| | | postJob: "", |
| | | adress: "", |
| | | firstStudy: "", |
| | | profession: "", |
| | | age: 0, |
| | | phone: "", |
| | | emergencyContact: "", |
| | | emergencyContactPhone: "", |
| | | contractTerm: 0, |
| | | contractStartTime: "", |
| | | contractEndTime: "", |
| | | sysPostId: undefined, |
| | | sysDeptId: undefined, |
| | | }, |
| | | rules: { |
| | | staffNo: [{ required: true, message: "请è¾å
¥", trigger: "blur" },], |
| | | staffName: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | sex: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | nativePlace: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | postJob: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | adress: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | firstStudy: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | profession: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | age: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | phone: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | emergencyContact: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | emergencyContactPhone: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | contractTerm: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | contractStartTime: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | contractEndTime: [{ required: true, message: "请è¾å
¥", trigger: "blur" }], |
| | | }, |
| | | postOptions: [], // å²ä½é项 |
| | | deptOptions: [], // é¨é¨é项 |
| | | }); |
| | | const { form, rules, postOptions, deptOptions } = toRefs(data); |
| | | const operationType = ref("add"); |
| | | const id = ref(0); |
| | | const formRef = ref(null); |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const dialogTitle = () => |
| | | operationType.value === "add" ? "æ°å¢å
¥è" : "ç¼è¾äººå"; |
| | | |
| | | const createEmptyEducation = () => ({ |
| | | education: "", |
| | | schoolName: "", |
| | | enrollTime: "", |
| | | graduateTime: "", |
| | | major: "", |
| | | degree: "", |
| | | }); |
| | | |
| | | const createEmptyWork = () => ({ |
| | | formerCompany: "", |
| | | formerDept: "", |
| | | formerPosition: "", |
| | | startDate: "", |
| | | endDate: "", |
| | | workDesc: "", |
| | | }); |
| | | |
| | | const createEmptyEmergency = () => ({ |
| | | contactName: "", |
| | | contactRelation: "", |
| | | contactPhone: "", |
| | | contactAddress: "", |
| | | }); |
| | | |
| | | const createDefaultForm = () => ({ |
| | | id: undefined, |
| | | // åºæ¬ä¿¡æ¯ |
| | | staffNo: "", |
| | | staffName: "", |
| | | alias: "", |
| | | phone: "", |
| | | sex: "", |
| | | birthDate: "", |
| | | age: undefined, |
| | | nativePlace: "", |
| | | nation: "", |
| | | maritalStatus: "", |
| | | politicalStatus: "", |
| | | firstWorkDate: "", |
| | | workingYears: undefined, |
| | | idCardNo: "", |
| | | hukouType: "", |
| | | email: "", |
| | | currentAddress: "", |
| | | // å¨èä¿¡æ¯ |
| | | contractStartTime: "", |
| | | contractEndTime: "", |
| | | proTerm: undefined, |
| | | positiveDate: "", |
| | | sysDeptId: undefined, |
| | | sysPostId: undefined, |
| | | basicSalary: undefined, |
| | | // é¶è¡å¡ä¿¡æ¯ |
| | | bankName: "", |
| | | bankCardNo: "", |
| | | // æè²ç»å |
| | | staffEducationList: [createEmptyEducation()], |
| | | // å·¥ä½ç»å |
| | | staffWorkExperienceList: [createEmptyWork()], |
| | | // ç´§æ¥è系人 |
| | | staffEmergencyContactList: [createEmptyEmergency()], |
| | | // è§è²ï¼åéï¼ |
| | | roleId: undefined, |
| | | }); |
| | | |
| | | const state = reactive({ |
| | | form: createDefaultForm(), |
| | | rules: { |
| | | staffNo: [{ required: true, message: "请è¾å
¥åå·¥ç¼å·", trigger: "blur" }], |
| | | staffName: [{ required: true, message: "请è¾å
¥å§å", trigger: "blur" }], |
| | | phone: [{ required: true, message: "请è¾å
¥ææº", trigger: "blur" }], |
| | | sex: [{ required: true, message: "è¯·éæ©æ§å«", trigger: "change" }], |
| | | birthDate: [ |
| | | { required: true, message: "è¯·éæ©åºçæ¥æ", trigger: "change" }, |
| | | ], |
| | | contractStartTime: [ |
| | | { required: true, message: "è¯·éæ©å
¥èæ¥æ", trigger: "change" }, |
| | | ], |
| | | contractEndTime: [ |
| | | { required: true, message: "è¯·éæ©ååç»ææ¥æ", trigger: "change" }, |
| | | ], |
| | | sysDeptId: [ |
| | | { required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }, |
| | | ], |
| | | roleId: [{ required: true, message: "è¯·éæ©è§è²", trigger: "change" }], |
| | | }, |
| | | postOptions: [], |
| | | deptOptions: [], |
| | | }); |
| | | |
| | | const { form, rules, postOptions, deptOptions } = toRefs(state); |
| | | const roleOptions = ref([]); |
| | | |
| | | const resetForm = () => { |
| | | Object.assign(form.value, createDefaultForm()); |
| | | nextTick(() => { |
| | | formRef.value?.clearValidate(); |
| | | }); |
| | | }; |
| | | |
| | | const fetchPostOptions = () => { |
| | | findPostOptions().then((res) => { |
| | | postOptions.value = res.data || []; |
| | | }); |
| | | }; |
| | | |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then((response) => { |
| | | deptOptions.value = filterDisabledDept( |
| | | JSON.parse(JSON.stringify(response.data || [])) |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | const fetchRoleOptions = () => { |
| | | getUser().then((res) => { |
| | | roleOptions.value = res.roles || []; |
| | | }); |
| | | }; |
| | | |
| | | function filterDisabledDept(deptList) { |
| | | return deptList.filter((dept) => { |
| | | if (dept.disabled) { |
| | | return false; |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children); |
| | | } |
| | | return true; |
| | | }); |
| | | } |
| | | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | if (operationType.value === 'edit') { |
| | | id.value = row.id |
| | | staffOnJobInfo(id.value, {}).then(res => { |
| | | form.value = {...res.data} |
| | | fetchPostOptions(); |
| | | fetchDeptOptions(); |
| | | fetchRoleOptions(); |
| | | resetForm(); |
| | | if (type === "edit" && row?.id) { |
| | | id.value = row.id; |
| | | staffOnJobInfo(id.value, {}).then((res) => { |
| | | const d = res.data || {}; |
| | | Object.assign(form.value, { |
| | | ...form.value, |
| | | ...d, |
| | | }); |
| | | if ( |
| | | !Array.isArray(form.value.staffEducationList) || |
| | | !form.value.staffEducationList.length |
| | | ) { |
| | | form.value.staffEducationList = [createEmptyEducation()]; |
| | | } |
| | | if ( |
| | | !Array.isArray(form.value.staffWorkExperienceList) || |
| | | !form.value.staffWorkExperienceList.length |
| | | ) { |
| | | form.value.staffWorkExperienceList = [createEmptyWork()]; |
| | | } |
| | | if ( |
| | | !Array.isArray(form.value.staffEmergencyContactList) || |
| | | !form.value.staffEmergencyContactList.length |
| | | ) { |
| | | form.value.staffEmergencyContactList = [createEmptyEmergency()]; |
| | | } |
| | | if (form.value.sysPostId === 0) { |
| | | form.value.sysPostId = undefined |
| | | form.value.sysPostId = undefined; |
| | | } |
| | | if (form.value.sysDeptId === 0) { |
| | | form.value.sysDeptId = undefined |
| | | form.value.sysDeptId = undefined; |
| | | } |
| | | // ç¼è¾æ¶ä¹è®¡ç®ä¸æ¬¡ååå¹´é |
| | | calculateContractTerm(); |
| | | }) |
| | | } else { |
| | | form.value.id = '' |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | } |
| | | onMounted(() => { |
| | | fetchPostOptions() |
| | | fetchDeptOptions() |
| | | }) |
| | | fetchPostOptions(); |
| | | fetchDeptOptions(); |
| | | }); |
| | | |
| | | const fetchPostOptions = () => { |
| | | findPostOptions().then(res => { |
| | | postOptions.value = res.data |
| | | }) |
| | | } |
| | | |
| | | // æ¥è¯¢é¨é¨å表 |
| | | const fetchDeptOptions = () => { |
| | | deptTreeSelect().then(response => { |
| | | deptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data))) |
| | | }) |
| | | } |
| | | |
| | | /** è¿æ»¤ç¦ç¨çé¨é¨ */ |
| | | function filterDisabledDept(deptList) { |
| | | return deptList.filter(dept => { |
| | | if (dept.disabled) { |
| | | return false |
| | | } |
| | | if (dept.children && dept.children.length) { |
| | | dept.children = filterDisabledDept(dept.children) |
| | | } |
| | | return true |
| | | }) |
| | | } |
| | | |
| | | // æäº¤äº§å表å |
| | | const submitForm = () => { |
| | | if (!form.value.sysPostId) { |
| | | form.value.sysPostId = 0; |
| | | form.value.sysPostId = undefined; |
| | | } |
| | | if (!form.value.sysDeptId) { |
| | | form.value.sysDeptId = 0; |
| | | form.value.sysDeptId = undefined; |
| | | } |
| | | proxy.$refs.formRef.validate(valid => { |
| | | // å
¼å®¹å端å¯è½ä»ä½¿ç¨ roleIds æ°ç» |
| | | form.value.roleIds = form.value.roleId ? [form.value.roleId] : []; |
| | | formRef.value?.validate((valid) => { |
| | | if (valid) { |
| | | if (operationType.value === "add") { |
| | | createStaffOnJob(form.value).then(res => { |
| | | createStaffOnJob(form.value).then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } else { |
| | | updateStaffOnJob(id.value, form.value).then(res => { |
| | | updateStaffOnJob(id.value, form.value).then(() => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | | }) |
| | | }); |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | // 计ç®ååå¹´é |
| | | const calculateContractTerm = () => { |
| | | if (form.value.contractStartTime && form.value.contractEndTime) { |
| | | const startDate = new Date(form.value.contractStartTime); |
| | | const endDate = new Date(form.value.contractEndTime); |
| | | |
| | | if (endDate > startDate) { |
| | | // 计ç®å¹´ä»½å·® |
| | | const yearDiff = endDate.getFullYear() - startDate.getFullYear(); |
| | | const monthDiff = endDate.getMonth() - startDate.getMonth(); |
| | | const dayDiff = endDate.getDate() - startDate.getDate(); |
| | | |
| | | let years = yearDiff; |
| | | |
| | | // å¦æç»ææ¥æçææ¥å°äºå¼å§æ¥æçææ¥ï¼ååå»1å¹´ |
| | | if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) { |
| | | years = yearDiff - 1; |
| | | } |
| | | |
| | | form.value.contractTerm = Math.max(0, years); |
| | | } else { |
| | | form.value.contractTerm = 0; |
| | | } |
| | | } else { |
| | | form.value.contractTerm = 0; |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // å
³éå¼¹æ¡ |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | formRef.value?.resetFields(); |
| | | dialogFormVisible.value = false; |
| | | emit('close') |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ |
| | | openDialog, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-dia-body { |
| | | padding: 0; |
| | | } |
| | | |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | </style> |
| | |
| | | prop: "staffName", |
| | | }, |
| | | { |
| | | label: "å«å", |
| | | prop: "alias", |
| | | }, |
| | | { |
| | | label: "ææº", |
| | | prop: "phone", |
| | | width: 150, |
| | | }, |
| | | { |
| | | label: "æ§å«", |
| | | prop: "sex", |
| | | }, |
| | | { |
| | | label: "æ·ç±ä½å", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "é¨é¨", |
| | | prop: "deptName", |
| | | }, |
| | | { |
| | | label: "å²ä½", |
| | | prop: "postJob", |
| | | }, |
| | | { |
| | | label: "ç°ä½å", |
| | | prop: "adress", |
| | | width:200 |
| | | }, |
| | | { |
| | | label: "第ä¸å¦å", |
| | | prop: "firstStudy", |
| | | }, |
| | | { |
| | | label: "ä¸ä¸", |
| | | prop: "profession", |
| | | width:100 |
| | | label: "åºçæ¥æ", |
| | | prop: "birthDate", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å¹´é¾", |
| | | prop: "age", |
| | | }, |
| | | { |
| | | label: "èç³»çµè¯", |
| | | prop: "phone", |
| | | width:150 |
| | | label: "ç±è´¯", |
| | | prop: "nativePlace", |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人", |
| | | prop: "emergencyContact", |
| | | width: 120 |
| | | label: "æ°æ", |
| | | prop: "nation", |
| | | width: 100, |
| | | }, |
| | | { |
| | | label: "ç´§æ¥è系人çµè¯", |
| | | prop: "emergencyContactPhone", |
| | | width:150 |
| | | }, |
| | | // { |
| | | // label: "ååå¹´é", |
| | | // prop: "contractTerm", |
| | | // }, |
| | | // { |
| | | // label: "ååå¼å§æ¥æ", |
| | | // prop: "contractStartTime", |
| | | // width: 120 |
| | | // }, |
| | | { |
| | | label: "ååç»ææ¥æ", |
| | | prop: "contractExpireTime", |
| | | width: 120 |
| | | label: "å©å§»ç¶åµ", |
| | | prop: "maritalStatus", |
| | | width: 100, |
| | | }, |
| | | { |
| | | dataType: "action", |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <FormDialog |
| | | v-model="dialogVisible" |
| | | title="å·¥èµå®¡æ ¸" |
| | | width="900px" |
| | | @close="handleClose" |
| | | > |
| | | <!-- å·¥èµè¡¨åºç¡ä¿¡æ¯ --> |
| | | <el-card shadow="never" style="margin-bottom: 16px;"> |
| | | <template #header> |
| | | <span>å·¥èµè¡¨ä¿¡æ¯</span> |
| | | </template> |
| | | <el-descriptions :column="3" border> |
| | | <el-descriptions-item label="å·¥èµä¸»é¢">{{ auditData?.salaryTitle || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å·¥èµæä»½">{{ auditData?.salaryMonth || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å·¥èµæ»é¢">Â¥ {{ formatMoney(auditData?.totalSalary) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ¯ä»é¶è¡">{{ auditData?.payBank || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸äºº">{{ auditData?.auditUserName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="夿³¨">{{ auditData?.remark || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- åå·¥å·¥èµæç» --> |
| | | <el-card shadow="never" style="margin-bottom: 16px;"> |
| | | <template #header> |
| | | <span>åå·¥å·¥èµæç»</span> |
| | | </template> |
| | | <div v-if="!employeeList || employeeList.length === 0" style="text-align: center; padding: 20px; color: #909399;"> |
| | | <div>ææ åå·¥å·¥èµæç»æ°æ®</div> |
| | | <div style="font-size: 12px; margin-top: 5px;">åå·¥æç»æ°æ®éè¦å¨å·¥èµè¡¨çææç¼è¾æ¶æä¼ä¿å</div> |
| | | </div> |
| | | <div v-else> |
| | | <el-table :data="employeeList" border max-height="300" style="width: 100%"> |
| | | <el-table-column prop="staffName" label="åå·¥å§å" width="100" /> |
| | | <el-table-column prop="deptName" label="é¨é¨" width="120" /> |
| | | <el-table-column prop="basicSalary" label="åºæ¬å·¥èµ" width="100" align="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.basicSalary) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="pieceSalary" label="计件工èµ" width="100" align="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.pieceSalary) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="hourlySalary" label="计æ¶å·¥èµ" width="100" align="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.hourlySalary) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="otherIncome" label="å
¶ä»æ¶å
¥" width="100" align="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.otherIncome) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="socialPersonal" label="社ä¿ä¸ªäºº" width="100" align="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.socialPersonal) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="fundPersonal" label="å
¬ç§¯é个人" width="120" align="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.fundPersonal) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="salaryTax" label="å·¥èµä¸ªç¨" width="100" align="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.salaryTax) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="netSalary" label="å®åå·¥èµ" width="100" align="right" fixed="right"> |
| | | <template #default="{ row }">Â¥ {{ formatMoney(row.netSalary) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div style="margin-top: 10px; text-align: right; font-weight: bold;"> |
| | | å·¥èµæ»é¢ï¼Â¥ {{ formatMoney(totalSalary) }} |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- å®¡æ ¸æä½ --> |
| | | <el-form label-position="top"> |
| | | <el-form-item label="å®¡æ ¸ç»æ" required> |
| | | <el-radio-group v-model="auditResult"> |
| | | <el-radio :value="4">éè¿</el-radio> |
| | | <el-radio :value="2">ä¸éè¿</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <el-button type="primary" :loading="loading" @click="handleConfirm"> |
| | | ç¡®å® |
| | | </el-button> |
| | | <el-button @click="handleClose">åæ¶</el-button> |
| | | </template> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, reactive, toRefs, getCurrentInstance, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import Cookies from "js-cookie"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { staffSalaryMainUpdate } from "@/api/personnelManagement/staffSalaryMain.js"; |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close", "success"]); |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const loading = ref(false); |
| | | const auditResult = ref(4); // é»è®¤éè¿ |
| | | const auditData = ref({}); |
| | | const employeeList = ref([]); |
| | | |
| | | // çå¬rowæ°æ®åå |
| | | watch(() => props.row, (newRow) => { |
| | | if (newRow && Object.keys(newRow).length > 0) { |
| | | loadAuditData(newRow); |
| | | } |
| | | }, { immediate: true }); |
| | | |
| | | // æ ¼å¼åéé¢ |
| | | const formatMoney = (value) => { |
| | | const num = Number(value) || 0; |
| | | return num.toFixed(2); |
| | | }; |
| | | |
| | | // 计ç®å·¥èµæ»é¢ |
| | | const totalSalary = computed(() => { |
| | | return employeeList.value.reduce((sum, e) => { |
| | | const salary = Number(e.netSalary) || 0; |
| | | return sum + salary; |
| | | }, 0); |
| | | }); |
| | | |
| | | // å è½½å®¡æ ¸æ°æ® |
| | | const loadAuditData = (row) => { |
| | | auditData.value = row || {}; |
| | | auditResult.value = 4; // é»è®¤éæ©éè¿ |
| | | |
| | | // å è½½åå·¥å·¥èµæç»æ°æ® |
| | | if (row?.staffSalaryDetailList && Array.isArray(row.staffSalaryDetailList)) { |
| | | employeeList.value = row.staffSalaryDetailList.map((e) => ({ |
| | | staffName: e.staffName ?? "", |
| | | deptName: e.deptName ?? "", |
| | | basicSalary: Number(e.basicSalary) || 0, |
| | | pieceSalary: Number(e.pieceSalary) || 0, |
| | | hourlySalary: Number(e.hourlySalary) || 0, |
| | | otherIncome: Number(e.otherIncome) || 0, |
| | | socialPersonal: Number(e.socialPersonal) || 0, |
| | | fundPersonal: Number(e.fundPersonal) || 0, |
| | | salaryTax: Number(e.salaryTax) || 0, |
| | | netSalary: Number(e.netSalary) || 0, |
| | | })); |
| | | } else { |
| | | console.log('æ²¡ææ¾å°åå·¥æç»æ°æ®'); |
| | | employeeList.value = []; |
| | | } |
| | | }; |
| | | |
| | | // æå¼å¼¹çª |
| | | const openDialog = (row) => { |
| | | loadAuditData(row); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | // å
³éå¼¹çª |
| | | const handleClose = () => { |
| | | dialogVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | |
| | | // ç¡®è®¤å®¡æ ¸ |
| | | const handleConfirm = () => { |
| | | try { |
| | | const row = auditData.value; |
| | | if (!row?.id) { |
| | | ElMessage.warning("æ°æ®å¼å¸¸ï¼è¯·éè¯"); |
| | | return; |
| | | } |
| | | |
| | | const username = Cookies.get("username") || ""; |
| | | const userIdRaw = Cookies.get("userId"); |
| | | const auditUserId = userIdRaw ? Number(userIdRaw) : undefined; |
| | | |
| | | // æå»ºå®¡æ ¸æ°æ® |
| | | const submitData = { |
| | | id: row.id, |
| | | status: Number(auditResult.value) === 2 ? 2 : 4, // 2=ä¸éè¿ 4=éè¿(å¾
åæ¾) |
| | | auditUserId, |
| | | auditUserName: username, |
| | | }; |
| | | loading.value = true; |
| | | staffSalaryMainUpdate(submitData) |
| | | .then(() => { |
| | | ElMessage.success("å®¡æ ¸æå"); |
| | | dialogVisible.value = false; |
| | | emit("success"); |
| | | }) |
| | | .catch((error) => { |
| | | console.error('å®¡æ ¸å¤±è´¥:', error) |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false; |
| | | }); |
| | | } catch (error) { |
| | | console.error('å®¡æ ¸å¤çå¼å¸¸:', error); |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | :deep(.el-descriptions__label) { |
| | | width: 100px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <FormDialog |
| | | v-model="dialogVisible" |
| | | operation-type="edit" |
| | | title="è®¾ç½®åæ¾é¶è¡ä¸ææ°æ®" |
| | | width="640px" |
| | | @close="handleClose" |
| | | @confirm="handleConfirm" |
| | | @cancel="handleCancel" |
| | | > |
| | | <el-form ref="formRef" :model="form" label-position="top"> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="24" style="display: flex; justify-content: end; gap: 10px;margin-bottom: 10px"> |
| | | <el-button type="primary" @click="addBank">æ°å¢é¶è¡</el-button> |
| | | <el-button @click="resetToEmpty">æ¸
空</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-table :data="form.banks" border style="width: 100%"> |
| | | <el-table-column label="é¶è¡åç§°" min-width="260"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.bankName" |
| | | placeholder="ä¾å¦ï¼ä¸å½å·¥åé¶è¡" |
| | | clearable |
| | | maxlength="50" |
| | | show-word-limit |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="90" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button type="danger" link @click="removeBank($index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div style="margin-top: 10px; color: #909399; font-size: 12px"> |
| | | æç¤ºï¼è¿éç»´æ¤çæ¯âåæ¾é¶è¡â䏿æ¡éé¡¹æ°æ®ï¼ä¿åå卿°å»º/ç¼è¾å·¥èµè¡¨ä¸å¯éæ©ã |
| | | </div> |
| | | </el-form> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, toRefs, watch, getCurrentInstance } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { bankAdd, bankDelete, bankList, bankUpdate } from "@/api/personnelManagement/bank.js"; |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close", "saved"]); |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const formRef = ref(null); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | banks: [], |
| | | }, |
| | | }); |
| | | |
| | | const { form } = toRefs(data); |
| | | |
| | | function newKey() { |
| | | return Math.random().toString(36).slice(2); |
| | | } |
| | | |
| | | const addBank = () => { |
| | | form.value.banks.push({ |
| | | _key: newKey(), |
| | | id: undefined, |
| | | bankName: "", |
| | | _originBankName: "", |
| | | }); |
| | | }; |
| | | |
| | | const removeBank = (index) => { |
| | | const row = form.value.banks?.[index]; |
| | | if (!row) return; |
| | | // æªè½åºçè¡ï¼ç´æ¥ç§»é¤ |
| | | if (!row.id) { |
| | | form.value.banks.splice(index, 1); |
| | | return; |
| | | } |
| | | // å·²è½åºï¼è°ç¨å端å é¤ |
| | | bankDelete([row.id]).then(() => { |
| | | proxy?.$modal?.msgSuccess?.("å 餿å"); |
| | | form.value.banks.splice(index, 1); |
| | | emit("saved"); |
| | | }); |
| | | }; |
| | | |
| | | const resetToEmpty = () => { |
| | | if (!form.value.banks?.length) return; |
| | | const ids = form.value.banks.map((b) => b?.id).filter(Boolean); |
| | | // è¥å
¨é¨æ¯æªä¿åè¡ï¼åä»
æ¸
空æ¬å° |
| | | if (!ids.length) { |
| | | form.value.banks = []; |
| | | return; |
| | | } |
| | | proxy?.$modal |
| | | ?.confirm?.("ç¡®å®æ¸
空ææé¶è¡åï¼") |
| | | .then(() => bankDelete(ids)) |
| | | .then(() => { |
| | | proxy?.$modal?.msgSuccess?.("æ¸
空æå"); |
| | | form.value.banks = []; |
| | | emit("saved"); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | const loadSetting = () => { |
| | | return bankList().then((res) => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | form.value.banks = list.map((b) => ({ |
| | | _key: newKey(), |
| | | id: b?.id, |
| | | bankName: b?.bankName ?? "", |
| | | _originBankName: b?.bankName ?? "", |
| | | })); |
| | | }); |
| | | }; |
| | | |
| | | const openDialog = () => { |
| | | loadSetting(); |
| | | }; |
| | | |
| | | watch( |
| | | () => dialogVisible.value, |
| | | (val) => { |
| | | if (val) openDialog(); |
| | | } |
| | | ); |
| | | |
| | | const handleConfirm = () => { |
| | | const names = (form.value.banks || []) |
| | | .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim())) |
| | | .filter((n) => n !== ""); |
| | | const unique = Array.from(new Set(names)); |
| | | if (!unique.length) { |
| | | proxy?.$modal?.msgWarning?.("请è³å°æ°å¢ä¸ä¸ªé¶è¡é项"); |
| | | return; |
| | | } |
| | | if (unique.length !== names.length) { |
| | | proxy?.$modal?.msgWarning?.("é¶è¡åç§°ä¸å¯éå¤"); |
| | | return; |
| | | } |
| | | |
| | | const rows = form.value.banks.map((b) => ({ |
| | | ...b, |
| | | bankName: b?.bankName == null ? "" : String(b.bankName).trim(), |
| | | })); |
| | | |
| | | const toAdd = rows.filter((b) => !b.id && b.bankName); |
| | | const toUpdate = rows.filter((b) => b.id && b.bankName && b.bankName !== (b._originBankName ?? "")); |
| | | |
| | | Promise.all([ |
| | | ...toAdd.map((b) => bankAdd({ bankName: b.bankName })), |
| | | ...toUpdate.map((b) => bankUpdate({ id: b.id, bankName: b.bankName })), |
| | | ]) |
| | | .then(() => loadSetting()) |
| | | .then(() => { |
| | | proxy?.$modal?.msgSuccess?.("ä¿åæå"); |
| | | dialogVisible.value = false; |
| | | emit("saved", { options: unique }); |
| | | }); |
| | | }; |
| | | |
| | | const handleCancel = () => { |
| | | dialogVisible.value = false; |
| | | }; |
| | | |
| | | const handleClose = () => { |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <FormDialog |
| | | v-model="dialogVisible" |
| | | :title="operationType === 'add' ? 'æ°å»ºå·¥èµè¡¨' : 'ç¼è¾å·¥èµè¡¨'" |
| | | width="90%" |
| | | @close="closeDia" |
| | | > |
| | | <template #footer> |
| | | <el-button type="info" @click="saveDraft">ä¿åè稿</el-button> |
| | | <el-button type="primary" @click="submitForm">确认æäº¤</el-button> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | </template> |
| | | <div class="form-dia-body"> |
| | | <!-- åºç¡èµæ --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> åºç¡èµæ</span> |
| | | <el-icon class="card-collapse"><ArrowUp /></el-icon> |
| | | </template> |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-position="top"> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å·¥èµä¸»é¢" prop="salaryTitle"> |
| | | <el-input |
| | | v-model="form.salaryTitle" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | maxlength="20" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="éæ©é¨é¨" prop="deptIds"> |
| | | <el-select |
| | | v-model="form.deptIds" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | multiple |
| | | collapse-tags-tooltip |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in deptOptions" |
| | | :key="item.deptId" |
| | | :label="item.deptName" |
| | | :value="item.deptId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="éæ©å·¥èµæä»½" prop="salaryMonth"> |
| | | <el-date-picker |
| | | v-model="form.salaryMonth" |
| | | type="month" |
| | | value-format="YYYY-MM" |
| | | format="YYYY-MM" |
| | | placeholder="è¯·éæ©å·¥èµæä»½" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="æ¯ä»é¶è¡" prop="payBank"> |
| | | <el-select |
| | | v-model="form.payBank" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="b in bankOptions" |
| | | :key="b" |
| | | :label="b" |
| | | :value="b" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å®¡æ ¸äºº" prop="auditUserId"> |
| | | <el-select |
| | | v-model="form.auditUserId" |
| | | placeholder="è¯·éæ©å®¡æ ¸äºº" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in userList" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- æä½æé® --> |
| | | <div class="toolbar"> |
| | | <el-button type="primary" @click="handleGenerate">çæå·¥èµè¡¨</el-button> |
| | | <el-button @click="handleClear">æ¸
空</el-button> |
| | | <el-button @click="handleBatchDelete">å é¤</el-button> |
| | | <el-button @click="handleTaxForm">个ç¨è¡¨</el-button> |
| | | </div> |
| | | |
| | | <!-- å工工èµè¯¦æ
è¡¨æ ¼ --> |
| | | <div class="employee-table-wrap"> |
| | | <el-table |
| | | ref="employeeTableRef" |
| | | :data="employeeList" |
| | | border |
| | | max-height="400" |
| | | @selection-change="onEmployeeSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="55" align="center" /> |
| | | <el-table-column label="åå·¥å§å" prop="staffName" minWidth="100" /> |
| | | <el-table-column label="é¨é¨" prop="deptName" minWidth="100" /> |
| | | <el-table-column label="åºæ¬å·¥èµ" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.basicSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.basicSalary = parseNum(row.basicSalary)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计件工èµ" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.pieceSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.pieceSalary = parseNum(row.pieceSalary)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="计æ¶å·¥èµ" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.hourlySalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.hourlySalary = parseNum(row.hourlySalary)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¶ä»æ¶å
¥" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.otherIncome" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.otherIncome = parseNum(row.otherIncome)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="社ä¿ä¸ªäºº" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.socialPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.socialPersonal = parseNum(row.socialPersonal)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¬ç§¯é个人" minWidth="120"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.fundPersonal" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.fundPersonal = parseNum(row.fundPersonal)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å
¶ä»æ¯åº" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.otherDeduct" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.otherDeduct = parseNum(row.otherDeduct)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·¥èµä¸ªç¨" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.salaryTax" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.salaryTax = parseNum(row.salaryTax)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åºåå·¥èµ" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.grossSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.grossSalary = parseNum(row.grossSalary)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åºæ£å·¥èµ" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.deductSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.deductSalary = parseNum(row.deductSalary)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å®åå·¥èµ" minWidth="110"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model.number="row.netSalary" |
| | | type="number" |
| | | placeholder="0" |
| | | size="small" |
| | | @input="row.netSalary = parseNum(row.netSalary)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="夿³¨" minWidth="120"> |
| | | <template #default="{ row }"> |
| | | <el-input |
| | | v-model="row.remark" |
| | | placeholder="请è¾å
¥" |
| | | size="small" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button type="primary" link @click="removeEmployee(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div v-if="!employeeList.length" class="table-empty">ææ æ°æ®</div> |
| | | <div v-else class="salary-total"> |
| | | <span class="total-label">å·¥èµæ»é¢ï¼</span> |
| | | <span class="total-value">Â¥ {{ totalSalary.toFixed(2) }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | |
| | | |
| | | <!-- æ°å¢äººåå¼¹çª --> |
| | | <el-dialog |
| | | v-model="addPersonVisible" |
| | | title="æ°å¢äººå" |
| | | width="400px" |
| | | append-to-body |
| | | @close="addPersonClose" |
| | | > |
| | | <div class="add-person-tree"> |
| | | <el-tree |
| | | ref="personTreeRef" |
| | | :data="deptStaffTree" |
| | | show-checkbox |
| | | node-key="id" |
| | | :props="{ label: 'label', children: 'children' }" |
| | | default-expand-all |
| | | /> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="addPersonVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="confirmAddPerson">ç¡®å®</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- 个ç¨è¡¨å¼¹çª --> |
| | | <el-dialog |
| | | v-model="taxDialogVisible" |
| | | title="个ç¨è¡¨" |
| | | width="700px" |
| | | append-to-body |
| | | > |
| | | <div class="tax-desc">个人æå¾ç¨å
å¾é¢ï¼5000å
</div> |
| | | <el-table :data="taxTableData" border style="width: 100%;margin-bottom: 20px;"> |
| | | <el-table-column prop="level" label="级æ°" width="80" align="center" /> |
| | | <el-table-column |
| | | prop="range" |
| | | label="å
¨å¹´åºçº³ç¨æå¾é¢/å
" |
| | | min-width="220" |
| | | /> |
| | | <el-table-column |
| | | prop="rate" |
| | | label="ç¨ç(%)" |
| | | width="100" |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | prop="quickDeduction" |
| | | label="éç®æ£é¤æ°/å
" |
| | | width="160" |
| | | align="center" |
| | | /> |
| | | </el-table> |
| | | </el-dialog> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js"; |
| | | import { bankList } from "@/api/personnelManagement/bank.js"; |
| | | import { |
| | | staffSalaryMainAdd, |
| | | staffSalaryMainUpdate, |
| | | staffSalaryMainCalculateSalary, |
| | | } from "@/api/personnelManagement/staffSalaryMain.js"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user.js"; |
| | | |
| | | |
| | | const emit = defineEmits(["update:modelValue", "close"]); |
| | | const props = defineProps({ |
| | | modelValue: { type: Boolean, default: false }, |
| | | operationType: { type: String, default: "add" }, |
| | | row: { type: Object, default: () => ({}) }, |
| | | }); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit("update:modelValue", val), |
| | | }); |
| | | |
| | | const formRef = ref(null); |
| | | const employeeTableRef = ref(null); |
| | | const personTreeRef = ref(null); |
| | | const addPersonVisible = ref(false); |
| | | const taxDialogVisible = ref(false); |
| | | const deptOptions = ref([]); |
| | | const deptStaffTree = ref([]); |
| | | const employeeList = ref([]); |
| | | const selectedEmployees = ref([]); |
| | | const bankOptions = ref([]); |
| | | const userList = ref([]); |
| | | const taxTableData = ref([ |
| | | { level: 1, range: "ä¸è¶
è¿36000å
", rate: 3, quickDeduction: 0 }, |
| | | { level: 2, range: "è¶
è¿36000-144000å
", rate: 10, quickDeduction: 2520 }, |
| | | { level: 3, range: "è¶
è¿144000-300000å
", rate: 20, quickDeduction: 16920 }, |
| | | { level: 4, range: "è¶
è¿300000-420000å
", rate: 25, quickDeduction: 31920 }, |
| | | { level: 5, range: "è¶
è¿420000-660000å
", rate: 30, quickDeduction: 52920 }, |
| | | { level: 6, range: "è¶
è¿660000-960000å
", rate: 35, quickDeduction: 85920 }, |
| | | { level: 7, range: "è¶
è¿960000å
", rate: 45, quickDeduction: 181920 }, |
| | | ]); |
| | | |
| | | function parseNum(v) { |
| | | if (v === "" || v == null) return 0; |
| | | const n = Number(v); |
| | | return isNaN(n) ? 0 : n; |
| | | } |
| | | |
| | | // åºç¡èµæè¡¨å |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | salaryTitle: "", |
| | | deptIds: [], |
| | | salaryMonth: "", |
| | | remark: "", |
| | | payBank: "", |
| | | auditUserId: undefined, |
| | | }, |
| | | rules: { |
| | | salaryTitle: [{ required: true, message: "请è¾å
¥å·¥èµä¸»é¢", trigger: "blur" }], |
| | | deptIds: [{ required: true, message: "è¯·éæ©é¨é¨", trigger: "change" }], |
| | | salaryMonth: [{ required: true, message: "è¯·éæ©å·¥èµæä»½", trigger: "change" }], |
| | | auditUserId: [{ required: true, message: "è¯·éæ©å®¡æ ¸äºº", trigger: "change" }], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | // 计ç®å·¥èµæ»é¢ï¼ææåå·¥å®åå·¥èµä¹åï¼ |
| | | const totalSalary = computed(() => { |
| | | return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0); |
| | | }); |
| | | |
| | | // æ ¹æ®å®¡æ ¸äººIDè·åå®¡æ ¸äººåç§° |
| | | const auditUserName = computed(() => { |
| | | if (!form.value.auditUserId) return ""; |
| | | const user = userList.value.find(u => u.userId === form.value.auditUserId); |
| | | return user ? user.nickName : ""; |
| | | }); |
| | | |
| | | const loadBankOptions = () => { |
| | | return bankList().then((res) => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | bankOptions.value = list |
| | | .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim())) |
| | | .filter((v) => v !== ""); |
| | | }); |
| | | }; |
| | | |
| | | const loadUserList = () => { |
| | | return userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data || []; |
| | | }); |
| | | }; |
| | | |
| | | // æå¹³åé¨é¨æ ä¾ä¸æä½¿ç¨ |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree?.length) return list; |
| | | tree.forEach((node) => { |
| | | list.push({ deptId: node.deptId, deptName: node.deptName }); |
| | | if (node.children?.length) flattenDept(node.children, list); |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | const loadDeptOptions = () => { |
| | | listDept().then((res) => { |
| | | const tree = res.data ?? []; |
| | | deptOptions.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | |
| | | // æå»º é¨é¨-äººå æ ï¼ç¨äºæ°å¢äººåå¼¹çªï¼ |
| | | const loadDeptStaffTree = () => { |
| | | Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => { |
| | | const tree = deptRes.data ?? []; |
| | | const staffList = staffRes.data ?? []; |
| | | const deptMap = new Map(); |
| | | function walk(nodes) { |
| | | nodes.forEach((node) => { |
| | | deptMap.set(node.deptId, { |
| | | id: "dept_" + node.deptId, |
| | | deptId: node.deptId, |
| | | label: node.deptName, |
| | | type: "dept", |
| | | children: [], |
| | | }); |
| | | if (node.children?.length) walk(node.children); |
| | | }); |
| | | } |
| | | walk(tree); |
| | | staffList.forEach((s) => { |
| | | const deptId = s.deptId ?? s.dept_id; |
| | | const node = deptMap.get(deptId); |
| | | if (node) { |
| | | node.children.push({ |
| | | id: s.id ?? s.staffId, |
| | | staffId: s.id ?? s.staffId, |
| | | label: s.staffName ?? s.name, |
| | | type: "staff", |
| | | ...s, |
| | | }); |
| | | } |
| | | }); |
| | | deptStaffTree.value = Array.from(deptMap.values()).filter( |
| | | (n) => n.children && n.children.length > 0 |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | nextTick(() => { |
| | | loadDeptOptions(); |
| | | loadBankOptions(); |
| | | loadUserList(); |
| | | employeeList.value = []; |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | | salaryTitle: "", |
| | | deptIds: [], |
| | | salaryMonth: "", |
| | | remark: "", |
| | | payBank: "", |
| | | auditUserId: undefined, |
| | | }); |
| | | // ç¼è¾ï¼å表页已è¿åä¸»è¡¨åæ®µï¼è¿éåªååæ¾ï¼æç»ç±âçæå·¥èµè¡¨/计ç®å·¥èµâå¾å°ï¼ |
| | | if (type === "edit" && row?.id) { |
| | | form.value.id = row.id; |
| | | form.value.salaryTitle = row.salaryTitle ?? ""; |
| | | // deptIds å端æ¯å符串ï¼å¤ä¸ªç¨éå·åéï¼ï¼å½å表å仿¯åé deptId |
| | | form.value.deptIds = row.deptIds |
| | | ? String(row.deptIds).split(",").map((id) => Number(id.trim())).filter(Boolean) |
| | | : []; |
| | | form.value.salaryMonth = row.salaryMonth ?? ""; |
| | | form.value.remark = row.remark ?? ""; |
| | | form.value.payBank = row.payBank ?? ""; |
| | | form.value.auditUserId = row.auditUserId ?? undefined; |
| | | |
| | | // 妿æåå·¥æç»æ°æ®ï¼ç´æ¥åæ¾ |
| | | if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) { |
| | | employeeList.value = row.staffSalaryDetailList.map((e) => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | id: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName ?? "", |
| | | postName: e.postName ?? "", |
| | | deptName: e.deptName ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const openAddPerson = () => { |
| | | loadDeptStaffTree(); |
| | | addPersonVisible.value = true; |
| | | nextTick(() => { |
| | | personTreeRef.value?.setCheckedKeys([]); |
| | | }); |
| | | }; |
| | | |
| | | const addPersonClose = () => {}; |
| | | |
| | | const confirmAddPerson = () => { |
| | | const tree = personTreeRef.value; |
| | | if (!tree) { |
| | | addPersonVisible.value = false; |
| | | return; |
| | | } |
| | | const checked = tree.getCheckedNodes(); |
| | | const staffNodes = checked.filter((n) => n.type === "staff"); |
| | | const existIds = new Set(employeeList.value.map((e) => e.staffId || e.id)); |
| | | staffNodes.forEach((node) => { |
| | | const id = node.staffId ?? node.id; |
| | | if (existIds.has(id)) return; |
| | | existIds.add(id); |
| | | employeeList.value.push({ |
| | | staffOnJobId: id, |
| | | id, |
| | | staffName: node.label, |
| | | postName: node.postName ?? node.post ?? "", |
| | | deptName: node.deptName ?? "", |
| | | basicSalary: 0, |
| | | pieceSalary: 0, |
| | | hourlySalary: 0, |
| | | otherIncome: 0, |
| | | socialPersonal: 0, |
| | | fundPersonal: 0, |
| | | otherDeduct: 0, |
| | | salaryTax: 0, |
| | | grossSalary: 0, |
| | | deductSalary: 0, |
| | | netSalary: 0, |
| | | remark: "", |
| | | }); |
| | | }); |
| | | addPersonVisible.value = false; |
| | | }; |
| | | |
| | | const removeEmployee = (row) => { |
| | | employeeList.value = employeeList.value.filter( |
| | | (e) => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id) |
| | | ); |
| | | }; |
| | | |
| | | const onEmployeeSelectionChange = (selection) => { |
| | | selectedEmployees.value = selection; |
| | | }; |
| | | |
| | | const handleBatchDelete = () => { |
| | | if (!selectedEmployees.value?.length) { |
| | | proxy.$modal.msgWarning("请å
å¾éè¦å é¤çåå·¥"); |
| | | return; |
| | | } |
| | | const ids = new Set(selectedEmployees.value.map((e) => e.staffOnJobId || e.id)); |
| | | employeeList.value = employeeList.value.filter( |
| | | (e) => !ids.has(e.staffOnJobId || e.id) |
| | | ); |
| | | }; |
| | | |
| | | const handleGenerate = () => { |
| | | if (!form.value.deptIds?.length) { |
| | | proxy.$modal.msgWarning("请å
éæ©é¨é¨"); |
| | | return; |
| | | } |
| | | if (!form.value.salaryMonth) { |
| | | proxy.$modal.msgWarning("请å
éæ©å·¥èµæä»½"); |
| | | return; |
| | | } |
| | | const payload = { |
| | | ids: form.value.deptIds, |
| | | date: form.value.salaryMonth, |
| | | }; |
| | | staffSalaryMainCalculateSalary(payload).then((res) => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | if (!list.length) { |
| | | proxy.$modal.msgWarning("æªè®¡ç®å°å·¥èµæ°æ®"); |
| | | return; |
| | | } |
| | | employeeList.value = list.map((e) => ({ |
| | | ...e, |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | | postName: e.postName, |
| | | deptName: e.deptName, |
| | | basicSalary: parseNum(e.basicSalary), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })); |
| | | proxy.$modal.msgSuccess("çææå"); |
| | | }); |
| | | }; |
| | | |
| | | const handleClear = () => { |
| | | proxy.$modal.confirm("ç¡®å®æ¸
空å½ååå·¥å表åï¼").then(() => { |
| | | employeeList.value = []; |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | const handleTaxForm = () => { |
| | | taxDialogVisible.value = true; |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | saveData(3); // 确认æäº¤ï¼ç¶æä¸º3ï¼å¾
å®¡æ ¸ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveDraft = () => { |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | saveData(1); // ä¿åè稿ï¼ç¶æä¸º1ï¼èç¨¿ï¼ |
| | | }); |
| | | }; |
| | | |
| | | const saveData = (status) => { |
| | | const payload = { |
| | | id: form.value.id, |
| | | salaryTitle: form.value.salaryTitle, |
| | | deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "", |
| | | salaryMonth: form.value.salaryMonth, |
| | | remark: form.value.remark, |
| | | payBank: form.value.payBank, |
| | | auditUserId: form.value.auditUserId, |
| | | auditUserName: auditUserName.value, |
| | | totalSalary: totalSalary.value, |
| | | staffSalaryDetailList: employeeList.value.map((e) => ({ |
| | | staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id, |
| | | staffName: e.staffName, |
| | | postName: e.postName ?? "", |
| | | deptName: e.deptName ?? "", |
| | | basicSalary: parseNum(e.basicSalary), |
| | | pieceSalary: parseNum(e.pieceSalary), |
| | | hourlySalary: parseNum(e.hourlySalary), |
| | | otherIncome: parseNum(e.otherIncome), |
| | | socialPersonal: parseNum(e.socialPersonal), |
| | | fundPersonal: parseNum(e.fundPersonal), |
| | | otherDeduct: parseNum(e.otherDeduct), |
| | | salaryTax: parseNum(e.salaryTax), |
| | | grossSalary: parseNum(e.grossSalary), |
| | | deductSalary: parseNum(e.deductSalary), |
| | | netSalary: parseNum(e.netSalary), |
| | | remark: e.remark ?? "", |
| | | })), |
| | | }; |
| | | if (props.operationType === "add") { |
| | | staffSalaryMainAdd({ ...payload, status }).then(() => { |
| | | proxy.$modal.msgSuccess(status === 1 ? "è稿ä¿åæå" : "æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | staffSalaryMainUpdate({ ...payload, status }).then(() => { |
| | | proxy.$modal.msgSuccess(status === 1 ? "è稿ä¿åæå" : "æäº¤æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | dialogVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .form-dia-body { |
| | | padding: 0; |
| | | } |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .toolbar { |
| | | margin-bottom: 16px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | .employee-table-wrap { |
| | | position: relative; |
| | | min-height: 120px; |
| | | } |
| | | .table-empty { |
| | | text-align: center; |
| | | padding: 24px; |
| | | color: #999; |
| | | font-size: 14px; |
| | | } |
| | | .add-person-tree { |
| | | max-height: 360px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | } |
| | | .tax-desc { |
| | | margin-bottom: 12px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | } |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | .salary-total { |
| | | margin-top: 16px; |
| | | padding: 12px 16px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | text-align: right; |
| | | font-size: 16px; |
| | | } |
| | | .salary-total .total-label { |
| | | color: #606266; |
| | | margin-right: 8px; |
| | | } |
| | | .salary-total .total-value { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | font-size: 18px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">主é¢ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.salaryTitle" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥ä¸»é¢" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <span class="search_title ml10">ç¶æï¼</span> |
| | | <el-select v-model="searchForm.status" placeholder="è¯·éæ©ç¶æ" clearable style="width: 180px"> |
| | | <el-option label="è稿" :value="1" /> |
| | | <el-option label="å®¡æ ¸æªéè¿" :value="2" /> |
| | | <el-option label="å¾
å®¡æ ¸" :value="3" /> |
| | | <el-option label="å¾
åæ¾" :value="4" /> |
| | | <el-option label="已忾" :value="5" /> |
| | | </el-select> |
| | | <span class="search_title ml10">å·¥èµæä»½ï¼</span> |
| | | <el-date-picker |
| | | v-model="searchForm.salaryMonth" |
| | | type="month" |
| | | value-format="YYYY-MM" |
| | | format="YYYY-MM" |
| | | placeholder="è¯·éæ©å·¥èµæä»½" |
| | | style="width: 180px" |
| | | clearable |
| | | @change="handleQuery" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px"> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="handleReset">éç½®</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <div style="margin-bottom: 10px"> |
| | | <el-button type="primary" @click="openForm('add')">æ°å»ºå·¥èµè¡¨</el-button> |
| | | <el-button @click="handleDelete">å é¤</el-button> |
| | | <el-button @click="openBankSetting">设置é¶è¡</el-button> |
| | | <el-button @click="handleExport">导åº</el-button> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | :tableLoading="tableLoading" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | /> |
| | | </div> |
| | | <form-dia |
| | | v-model="dialogVisible" |
| | | :operation-type="operationType" |
| | | :row="currentRow" |
| | | ref="formDiaRef" |
| | | @close="handleQuery" |
| | | /> |
| | | <bank-setting-dia |
| | | v-model="bankDialogVisible" |
| | | ref="bankDiaRef" |
| | | @saved="handleBankSaved" |
| | | /> |
| | | <el-dialog v-model="issueDialogVisible" title="å·¥èµåæ¾" width="720px"> |
| | | <el-form label-position="top"> |
| | | <el-form-item label="åæ¾é¶è¡" required> |
| | | <el-select |
| | | v-model="issueForm.bank" |
| | | placeholder="è¯·éæ©åæ¾é¶è¡" |
| | | clearable |
| | | filterable |
| | | style="width: 100%" |
| | | > |
| | | <el-option v-for="b in issueBankOptions" :key="b" :label="b" :value="b" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button type="primary" :loading="issueLoading" @click="confirmIssue"> |
| | | ç¡®å® |
| | | </el-button> |
| | | <el-button @click="issueDialogVisible = false">åæ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <audit-dia |
| | | v-model="auditDialogVisible" |
| | | :row="auditRow" |
| | | @close="auditDialogVisible = false" |
| | | @success="handleAuditSuccess" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | onMounted, |
| | | computed, |
| | | ref, |
| | | reactive, |
| | | toRefs, |
| | | getCurrentInstance, |
| | | nextTick, |
| | | } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import Cookies from "js-cookie"; |
| | | import FormDia from "./components/formDia.vue"; |
| | | import BankSettingDia from "./components/bankSettingDia.vue"; |
| | | import AuditDia from "./components/auditDia.vue"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import { bankList } from "@/api/personnelManagement/bank.js"; |
| | | import { |
| | | staffSalaryMainListPage, |
| | | staffSalaryMainDelete, |
| | | staffSalaryMainUpdate, |
| | | } from "@/api/personnelManagement/staffSalaryMain.js"; |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | salaryTitle: "", |
| | | status: "", |
| | | salaryMonth: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "å·¥èµä¸»é¢", prop: "salaryTitle", minWidth: 140 }, |
| | | { label: "å·¥èµæä»½", prop: "salaryMonth", width: 120 }, |
| | | { |
| | | label: "ç¶æ", |
| | | prop: "statusName", |
| | | width: 110, |
| | | dataType: "tag", |
| | | formatType: (status) => { |
| | | const statusMap = { |
| | | "è稿": "info", |
| | | "å®¡æ ¸æªéè¿": "danger", |
| | | "å¾
å®¡æ ¸": "warning", |
| | | "å¾
åæ¾": "primary", |
| | | "已忾": "success" |
| | | }; |
| | | return statusMap[status] || "info"; |
| | | } |
| | | }, |
| | | { label: "å·¥èµæ»é¢", prop: "totalSalary", width: 120 }, |
| | | { label: "æ¯ä»é¶è¡", prop: "payBank", width: 120 }, |
| | | { label: "å®¡æ ¸äºº", prop: "auditUserName", width: 110 }, |
| | | { label: "夿³¨", prop: "remark", minWidth: 120 }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 180, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | disabled: (row) => Number(row?.status) !== 1 && Number(row?.status) !== 2, |
| | | clickFun: (row) => openForm("edit", row), |
| | | }, |
| | | { |
| | | name: "å®¡æ ¸", |
| | | type: "text", |
| | | disabled: (row) => Number(row?.status) !== 3, |
| | | clickFun: (row) => openAudit(row), |
| | | }, |
| | | { |
| | | name: "åæ¾", |
| | | type: "text", |
| | | disabled: (row) => Number(row?.status) !== 4, |
| | | clickFun: (row) => openIssue(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | const formDiaRef = ref(null); |
| | | const dialogVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const currentRow = ref({}); |
| | | const { proxy } = getCurrentInstance(); |
| | | const bankSetting = ref({}); |
| | | const bankDialogVisible = ref(false); |
| | | const bankDiaRef = ref(null); |
| | | const issueDialogVisible = ref(false); |
| | | const issueLoading = ref(false); |
| | | const issueRow = ref(null); |
| | | const issueForm = reactive({ bank: "" }); |
| | | const auditDialogVisible = ref(false); |
| | | const auditRow = ref(null); |
| | | const auditDiaRef = ref(null); |
| | | |
| | | const issueBankOptions = computed(() => { |
| | | const options = Array.isArray(bankSetting.value?.options) ? bankSetting.value.options : []; |
| | | return options |
| | | .map((v) => (v == null ? "" : String(v).trim())) |
| | | .filter((v) => v !== ""); |
| | | }); |
| | | |
| | | const statusName = (s) => { |
| | | const n = Number(s); |
| | | return ( |
| | | { |
| | | 1: "è稿", |
| | | 2: "å®¡æ ¸æªéè¿", |
| | | 3: "å¾
å®¡æ ¸", |
| | | 4: "å¾
åæ¾", |
| | | 5: "已忾", |
| | | }[n] || "-" |
| | | ); |
| | | }; |
| | | |
| | | const loadBankSetting = () => { |
| | | return bankList().then((res) => { |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const options = list |
| | | .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim())) |
| | | .filter((v) => v !== ""); |
| | | bankSetting.value = { options, defaultBank: "" }; |
| | | }); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.value.salaryTitle = ""; |
| | | searchForm.value.status = ""; |
| | | searchForm.value.salaryMonth = ""; |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | staffSalaryMainListPage({ |
| | | ...searchForm.value, |
| | | current: page.current, |
| | | size: page.size, |
| | | }) |
| | | .then((res) => { |
| | | tableLoading.value = false; |
| | | const records = res.data?.records ?? res.data?.list ?? []; |
| | | console.log('å表æ¥å£è¿åæ°æ®:', records); |
| | | // å
¼å®¹åç«¯åæ®µï¼è¥æ¥å£ä»è¿åå°è´¦ç»æï¼å¯å¨æ¤åæ å° |
| | | tableData.value = records.map((item) => ({ |
| | | ...item, |
| | | salaryTitle: item.salaryTitle ?? "-", |
| | | salaryMonth: item.salaryMonth ?? "-", |
| | | statusName: statusName(item.status), |
| | | totalSalary: item.totalSalary ?? "-", |
| | | payBank: (item.payBank == null ? "" : String(item.payBank).trim()) || "-", |
| | | auditUserName: item.auditUserName ?? "-", |
| | | })); |
| | | page.total = res.data?.total ?? res.data?.count ?? 0; |
| | | }) |
| | | .catch(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | const openForm = (type, row) => { |
| | | operationType.value = type; |
| | | currentRow.value = row || {}; |
| | | dialogVisible.value = true; |
| | | nextTick(() => { |
| | | formDiaRef.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | |
| | | const openBankSetting = () => { |
| | | bankDialogVisible.value = true; |
| | | }; |
| | | |
| | | const openAudit = (row) => { |
| | | console.log('æå¼å®¡æ ¸ï¼ä¼ å
¥çæ°æ®:', row); |
| | | auditRow.value = row || null; |
| | | auditDialogVisible.value = true; |
| | | nextTick(() => { |
| | | auditDiaRef.value?.openDialog(row); |
| | | }); |
| | | }; |
| | | |
| | | const handleAuditSuccess = () => { |
| | | getList(); |
| | | }; |
| | | |
| | | const openIssue = (row) => { |
| | | if (!issueBankOptions.value?.length) { |
| | | proxy?.$modal?.msgWarning?.("请å
å¨â设置é¶è¡âä¸ç»´æ¤åæ¾é¶è¡é项"); |
| | | return; |
| | | } |
| | | issueRow.value = row || null; |
| | | const current = row?.payBank && row.payBank !== "-" ? String(row.payBank).trim() : ""; |
| | | issueForm.bank = current; |
| | | issueDialogVisible.value = true; |
| | | }; |
| | | |
| | | |
| | | |
| | | const confirmIssue = () => { |
| | | const bank = issueForm.bank ? String(issueForm.bank).trim() : ""; |
| | | if (!bank) { |
| | | proxy?.$modal?.msgWarning?.("è¯·éæ©åæ¾é¶è¡"); |
| | | return; |
| | | } |
| | | const row = issueRow.value; |
| | | if (!row?.id) { |
| | | issueDialogVisible.value = false; |
| | | return; |
| | | } |
| | | issueLoading.value = true; |
| | | staffSalaryMainUpdate({ |
| | | id: row.id, |
| | | payBank: bank, |
| | | status: 5, |
| | | }) |
| | | .then(() => { |
| | | proxy?.$modal?.msgSuccess?.("åæ¾æå"); |
| | | issueDialogVisible.value = false; |
| | | getList(); |
| | | }) |
| | | .finally(() => { |
| | | issueLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleBankSaved = () => { |
| | | loadBankSetting(); |
| | | getList(); |
| | | }; |
| | | |
| | | const handleDelete = () => { |
| | | if (!selectedRows.value?.length) { |
| | | proxy.$modal.msgWarning("è¯·éæ©è¦å é¤çæ°æ®"); |
| | | return; |
| | | } |
| | | const ids = selectedRows.value.map((item) => item.id); |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | staffSalaryMainDelete(ids).then(() => { |
| | | proxy.$modal.msgSuccess("å 餿å"); |
| | | getList(); |
| | | }); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | const handleExport = () => { |
| | | proxy.download( |
| | | "/compensationPerformance/export", |
| | | { ...searchForm.value, current: page.current, size: page.size }, |
| | | "å·¥èµè¡¨.xlsx" |
| | | ); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | loadBankSetting(); |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .search_form { |
| | | margin-bottom: 20px; |
| | | } |
| | | .search_title { |
| | | font-weight: 500; |
| | | margin-right: 5px; |
| | | } |
| | | .ml10 { |
| | | margin-left: 10px; |
| | | } |
| | | .table_list { |
| | | margin-top: 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <FormDialog |
| | | v-model="dialogFormVisible" |
| | | :operation-type="operationType" |
| | | :title="dialogTitle" |
| | | width="80%" |
| | | @close="closeDia" |
| | | @confirm="submitForm" |
| | | @cancel="closeDia" |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-position="top"> |
| | | <el-row :gutter="24"> |
| | | <!-- 左侧ï¼éç¨äººå --> |
| | | <el-col :span="8"> |
| | | <el-form-item label="éç¨äººåï¼" prop="deptIds"> |
| | | <div class="dept-checkbox-wrap"> |
| | | <el-checkbox-group |
| | | v-model="form.deptIds" |
| | | :disabled="isDetail" |
| | | > |
| | | <div |
| | | v-for="dept in deptList" |
| | | :key="dept.deptId" |
| | | class="dept-checkbox-item" |
| | | > |
| | | <el-checkbox :value="dept.deptId"> |
| | | {{ dept.deptName }} |
| | | <span v-if="dept.personCount != null" class="dept-count" |
| | | >{{ dept.personCount }}人</span |
| | | > |
| | | </el-checkbox> |
| | | </div> |
| | | </el-checkbox-group> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | <!-- å³ä¾§ï¼åºç¡ä¿¡æ¯ + ä¿é©ç±»å --> |
| | | <el-col :span="16"> |
| | | <!-- åºç¡ä¿¡æ¯ --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> åºç¡ä¿¡æ¯</span> |
| | | <el-icon class="card-collapse"><ArrowUp /></el-icon> |
| | | </template> |
| | | <el-form-item label="æ¹æ¡æ é¢ï¼" prop="title"> |
| | | <el-input |
| | | v-model="form.title" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨ï¼" prop="remark"> |
| | | <el-input |
| | | v-model="form.remark" |
| | | type="textarea" |
| | | :rows="2" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | :disabled="isDetail" |
| | | /> |
| | | </el-form-item> |
| | | </el-card> |
| | | |
| | | <!-- ä¿é©ç±»å --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <template #header> |
| | | <span class="card-title"><span class="card-title-line">|</span> ä¿é©ç±»å</span> |
| | | <el-button |
| | | v-if="!isDetail" |
| | | type="primary" |
| | | size="small" |
| | | @click="addInsuranceBenefit" |
| | | > |
| | | æ·»å ä¿é©ç¦å© |
| | | </el-button> |
| | | </template> |
| | | <el-row :gutter="16"> |
| | | <el-col |
| | | v-for="(item, index) in form.insuranceBenefits" |
| | | :key="item._key" |
| | | :span="12" |
| | | > |
| | | <div class="insurance-benefit-card"> |
| | | <div class="insurance-benefit-title"> |
| | | ä¿é©ç¦å©{{ index + 1 }} |
| | | <el-button |
| | | v-if="!isDetail && form.insuranceBenefits.length > 1" |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | class="card-delete-btn" |
| | | @click="removeInsuranceBenefit(index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </div> |
| | | <el-form-item |
| | | :prop="'insuranceBenefits.' + index + '.insuranceType'" |
| | | label="ä¿é©ç±»åï¼" |
| | | label-width="100px" |
| | | > |
| | | <el-select |
| | | v-model="item.insuranceType" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 100%" |
| | | :disabled="isDetail" |
| | | > |
| | | <el-option |
| | | v-for="opt in insuranceTypeOptions" |
| | | :key="opt.value" |
| | | :label="opt.label" |
| | | :value="opt.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ç¼´è´¹åºæ°ï¼" label-width="100px"> |
| | | <div class="base-salary-wrap"> |
| | | <el-input |
| | | v-model="item.paymentBase" |
| | | placeholder="æ ¹æ®åºæ¬å·¥èµç¼´çº³" |
| | | clearable |
| | | style="width: 120px" |
| | | type="number" |
| | | :disabled="isDetail || item.useBasicSalary" |
| | | @input="handlePaymentBaseInput(item)" |
| | | /> |
| | | <el-checkbox |
| | | v-model="item.useBasicSalary" |
| | | @change="handleUseBasicSalaryChange(item)" |
| | | :disabled="isDetail" |
| | | > |
| | | è°ç¨åºæ¬å·¥èµ |
| | | </el-checkbox> |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="个人缴费æ¯ä¾ï¼" label-width="100px"> |
| | | <div class="personal-ratio-wrap"> |
| | | <el-input |
| | | v-model="item.personalRatio" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | /> |
| | | <span class="ratio-unit">(%)</span> |
| | | <span class="ratio-plus">+</span> |
| | | <el-input |
| | | v-model="item.personalFixed" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 100px" |
| | | type="number" |
| | | :disabled="isDetail" |
| | | /> |
| | | </div> |
| | | </el-form-item> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue"; |
| | | import FormDialog from "@/components/Dialog/FormDialog.vue"; |
| | | import { ArrowUp } from "@element-plus/icons-vue"; |
| | | import { listDept } from "@/api/system/dept.js"; |
| | | import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js"; |
| | | |
| | | const emit = defineEmits(["close"]); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref("add"); |
| | | const formRef = ref(null); |
| | | const deptList = ref([]); |
| | | |
| | | const isDetail = computed(() => operationType.value === "detail"); |
| | | |
| | | const dialogTitle = () => |
| | | operationType.value === "add" |
| | | ? "æ°å¢æ¹æ¡" |
| | | : operationType.value === "edit" |
| | | ? "ç¼è¾æ¹æ¡" |
| | | : "æ¹æ¡è¯¦æ
"; |
| | | |
| | | // ä¿é©ç±»åé项ï¼å¯æåå
¸æ¿æ¢ï¼ |
| | | const insuranceTypeOptions = [ |
| | | { label: "å
»èä¿é©", value: "å
»èä¿é©" }, |
| | | { label: "å»çä¿é©", value: "å»çä¿é©" }, |
| | | { label: "失ä¸ä¿é©", value: "失ä¸ä¿é©" }, |
| | | { label: "工伤ä¿é©", value: "工伤ä¿é©" }, |
| | | { label: "çè²ä¿é©", value: "çè²ä¿é©" }, |
| | | { label: "å
¬ç§¯é", value: "å
¬ç§¯é" }, |
| | | ]; |
| | | |
| | | const defaultBenefit = () => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: "", |
| | | paymentBase: "", |
| | | useBasicSalary: false, |
| | | personalRatio: "", |
| | | personalFixed: "", |
| | | }); |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }, |
| | | rules: { |
| | | title: [{ required: true, message: "请è¾å
¥æ¹æ¡æ é¢", trigger: "blur" }], |
| | | deptIds: [ |
| | | { |
| | | required: true, |
| | | type: "array", |
| | | min: 1, |
| | | message: "请è³å°éæ©ä¸ä¸ªéç¨é¨é¨", |
| | | trigger: "change", |
| | | }, |
| | | ], |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | function flattenDept(tree, list = []) { |
| | | if (!tree || !tree.length) return list; |
| | | tree.forEach((node) => { |
| | | list.push({ |
| | | deptId: node.deptId, |
| | | deptName: node.deptName, |
| | | personCount: node.personCount ?? null, |
| | | }); |
| | | if (node.children && node.children.length) { |
| | | flattenDept(node.children, list); |
| | | } |
| | | }); |
| | | return list; |
| | | } |
| | | |
| | | const loadDeptList = () => { |
| | | listDept().then((res) => { |
| | | const tree = res.data ?? []; |
| | | deptList.value = flattenDept(tree); |
| | | }); |
| | | }; |
| | | |
| | | const addInsuranceBenefit = () => { |
| | | form.value.insuranceBenefits.push(defaultBenefit()); |
| | | }; |
| | | |
| | | const removeInsuranceBenefit = (index) => { |
| | | form.value.insuranceBenefits.splice(index, 1); |
| | | }; |
| | | |
| | | const handleUseBasicSalaryChange = (item) => { |
| | | if (item.useBasicSalary) { |
| | | item.paymentBase = ""; |
| | | } |
| | | }; |
| | | |
| | | const handlePaymentBaseInput = (item) => { |
| | | if (item.paymentBase !== "" && item.paymentBase != null) { |
| | | item.useBasicSalary = false; |
| | | } |
| | | }; |
| | | |
| | | const resetForm = () => { |
| | | form.value = { |
| | | id: undefined, |
| | | title: "", |
| | | remark: "", |
| | | deptIds: [], |
| | | insuranceBenefits: [defaultBenefit()], |
| | | }; |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | loadDeptList(); |
| | | resetForm(); |
| | | if ((type === "edit" || type === "detail") && row) { |
| | | const d = row || {}; |
| | | form.value.id = d.id; |
| | | form.value.title = d.title; |
| | | form.value.remark = d.remark ?? ""; |
| | | // deptIds å端å¯è½æ¯éå·åéåç¬¦ä¸²ææ°ç»ï¼è¿éç»ä¸è½¬ä¸ºæ°ç»å¹¶å°½éè¿åæ°å¼ç±»å |
| | | if (d.deptIds) { |
| | | form.value.deptIds = String(d.deptIds) |
| | | .split(",") |
| | | .filter((v) => v !== "") |
| | | .map((v) => { |
| | | const num = Number(v); |
| | | return Number.isNaN(num) ? v : num; |
| | | }); |
| | | } else { |
| | | form.value.deptIds = []; |
| | | } |
| | | const detailList = d.schemeInsuranceDetailList || []; |
| | | form.value.insuranceBenefits = |
| | | detailList.length > 0 |
| | | ? detailList.map((b) => ({ |
| | | _key: Math.random().toString(36).slice(2), |
| | | insuranceType: b.insuranceType || "", |
| | | paymentBase: b.paymentBase ?? "", |
| | | useBasicSalary: b.useBasicSalary === 2, |
| | | personalRatio: b.personalRatio ?? "", |
| | | personalFixed: b.personalFixed ?? "", |
| | | })) |
| | | : [defaultBenefit()]; |
| | | } |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | // 详æ
模å¼ä¸ä¸æäº¤ï¼åªå
³éå¼¹çª |
| | | if (operationType.value === "detail") { |
| | | closeDia(); |
| | | return; |
| | | } |
| | | formRef.value?.validate((valid) => { |
| | | if (!valid) return; |
| | | const deptIds = |
| | | Array.isArray(form.value.deptIds) && form.value.deptIds.length |
| | | ? form.value.deptIds.join(",") |
| | | : ""; |
| | | const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map( |
| | | ({ _key, ...rest }) => ({ |
| | | ...rest, |
| | | useBasicSalary: rest.useBasicSalary ? 2 : 1, |
| | | }) |
| | | ); |
| | | const insuranceTypes = schemeInsuranceDetailList |
| | | .map((item) => item.insuranceType) |
| | | .filter((v) => v) |
| | | .join(","); |
| | | // é¨é¨åç§°ï¼å¤ä¸ªä½¿ç¨éå·éå¼ï¼æ ¹æ®éä¸ç deptIds ä¸ deptList 计ç®ï¼ |
| | | const deptNames = (deptList.value || []) |
| | | .filter((d) => |
| | | (form.value.deptIds || []).some( |
| | | (id) => String(id) === String(d.deptId) |
| | | ) |
| | | ) |
| | | .map((d) => d.deptName) |
| | | .join(","); |
| | | const submitData = { |
| | | id: form.value.id, |
| | | title: form.value.title, |
| | | remark: form.value.remark ?? "", |
| | | deptIds, |
| | | insuranceTypes, |
| | | deptNames, |
| | | schemeInsuranceDetailList, |
| | | }; |
| | | if (operationType.value === "add") { |
| | | socialSecurityAdd(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | | closeDia(); |
| | | }); |
| | | } else { |
| | | socialSecurityUpdate(submitData).then(() => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | closeDia(); |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const closeDia = () => { |
| | | proxy.resetForm?.("formRef"); |
| | | dialogFormVisible.value = false; |
| | | emit("close"); |
| | | }; |
| | | |
| | | defineExpose({ openDialog }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .card-title-line { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | .form-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .form-card :deep(.el-card__header) { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | } |
| | | .card-title { |
| | | font-weight: 500; |
| | | } |
| | | .card-collapse { |
| | | color: #999; |
| | | cursor: pointer; |
| | | } |
| | | .dept-checkbox-wrap { |
| | | max-height: 320px; |
| | | overflow-y: auto; |
| | | padding: 8px 0; |
| | | border: 1px solid var(--el-border-color); |
| | | border-radius: 4px; |
| | | background: #fff; |
| | | } |
| | | .dept-checkbox-item { |
| | | padding: 6px 12px; |
| | | } |
| | | .dept-count { |
| | | color: #909399; |
| | | font-size: 12px; |
| | | margin-left: 4px; |
| | | } |
| | | .insurance-benefit-card { |
| | | border: 1px solid var(--el-border-color-lighter); |
| | | border-radius: 4px; |
| | | padding: 12px 16px; |
| | | margin-bottom: 12px; |
| | | background: #fafafa; |
| | | } |
| | | .insurance-benefit-title { |
| | | font-size: 14px; |
| | | margin-bottom: 12px; |
| | | font-weight: 500; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | .card-delete-btn { |
| | | margin-left: auto; |
| | | } |
| | | .checkbox-group-inline { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | } |
| | | .base-salary-wrap { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .base-salary-text { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | .personal-ratio-wrap { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | .ratio-unit, |
| | | .ratio-plus { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div> |
| | | <span class="search_title">主é¢ï¼</span> |
| | | <el-input |
| | | v-model="searchForm.title" |
| | | style="width: 240px" |
| | | placeholder="请è¾å
¥ä¸»é¢" |
| | | clearable |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px"> |
| | | æç´¢ |
| | | </el-button> |
| | | <el-button @click="handleReset">éç½®</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="table_list"> |
| | | <div style="margin-bottom: 10px; display: flex; gap: 10px"> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢æ¹æ¡</el-button> |
| | | <el-button |
| | | type="danger" |
| | | @click="handleBatchDelete" |
| | | :disabled="selectedRows.length === 0" |
| | | > |
| | | æ¹éå é¤ |
| | | </el-button> |
| | | </div> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :tableLoading="tableLoading" |
| | | :total="page.total" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="pagination" |
| | | /> |
| | | </div> |
| | | <form-dia ref="formDiaRef" @close="handleQuery" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue"; |
| | | import { ElMessageBox, ElMessage } from "element-plus"; |
| | | import FormDia from "./components/formDia.vue"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import { |
| | | socialSecurityListPage, |
| | | socialSecurityDelete, |
| | | } from "@/api/personnelManagement/socialSecuritySet.js"; |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | title: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const tableColumn = ref([ |
| | | { label: "主é¢", prop: "title", minWidth: 120 }, |
| | | { label: "ä¿é©ç±»å", prop: "insuranceTypes", width: 120 }, |
| | | { label: "使ç¨èå´", prop: "deptNames", width: 120 }, |
| | | { label: "夿³¨", prop: "remark", minWidth: 120 }, |
| | | { label: "å建æ¶é´", prop: "createTime", width: 160 }, |
| | | { label: "å建人", prop: "createUserName", width: 100 }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 180, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => openForm("edit", row), |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => openForm("detail", row), |
| | | }, |
| | | { |
| | | name: "å é¤", |
| | | type: "text", |
| | | style: { |
| | | color: "#F56C6C", |
| | | }, |
| | | clickFun: (row) => handleDelete(row), |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | |
| | | const tableData = ref([]); |
| | | const tableLoading = ref(false); |
| | | const selectedRows = ref([]); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | const formDiaRef = ref(null); |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.value.title = ""; |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | socialSecurityListPage({ |
| | | ...searchForm.value, |
| | | current: page.current, |
| | | size: page.size, |
| | | }) |
| | | .then((res) => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data?.records ?? []; |
| | | page.total = res.data?.total ?? 0; |
| | | }) |
| | | .catch(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDiaRef.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | |
| | | // å 餿¹æ¡ï¼é»è¾ä¸å
¶å®é¡µé¢ä¿æä¸è´ï¼ç¡®è®¤å¼¹çª + è°ç¨å 餿¥å£ + å·æ°åè¡¨ï¼ |
| | | const handleDelete = (row) => { |
| | | ElMessageBox.confirm( |
| | | `确认å 餿¹æ¡"${row.title}"åï¼`, |
| | | "å é¤ç¡®è®¤", |
| | | { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | } |
| | | ) |
| | | .then(() => { |
| | | socialSecurityDelete([row.id]) |
| | | .then(() => { |
| | | ElMessage.success("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("å é¤å¤±è´¥"); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.info("已忶å é¤"); |
| | | }); |
| | | }; |
| | | |
| | | // æ¹éå é¤ |
| | | const handleBatchDelete = () => { |
| | | if (!selectedRows.value.length) return; |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤éä¸ç ${selectedRows.value.length} æ¡æ¹æ¡åï¼`, |
| | | "æ¹éå é¤ç¡®è®¤", |
| | | { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | } |
| | | ) |
| | | .then(() => { |
| | | const ids = selectedRows.value.map((item) => item.id); |
| | | socialSecurityDelete(ids) |
| | | .then(() => { |
| | | ElMessage.success("å 餿å"); |
| | | getList(); |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("å é¤å¤±è´¥"); |
| | | }); |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.info("已忶å é¤"); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped></style> |
| | |
| | | /> |
| | | <el-table-column label="æ¬æ¬¡å¼ç¥¨æ°" prop="ticketsNum" width="180"> |
| | | <template #default="scope"> |
| | | <el-input-number :step="0.1" :min="0" :max="scope.row.tempFutureTickets || 0" style="width: 100%" |
| | | :precision="2" |
| | | v-model="scope.row.ticketsNum" |
| | | @change="invoiceNumBlur(scope.row)" |
| | | <el-input-number |
| | | :step="0.1" |
| | | :min="0" |
| | | :max="scope.row.tempFutureTickets || 0" |
| | | style="width: 100%" |
| | | :precision="2" |
| | | v-model="scope.row.ticketsNum" |
| | | :disabled="isProductDisabled(scope.row)" |
| | | @change="invoiceNumBlur(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | width="180" |
| | | > |
| | | <template #default="scope"> |
| | | <el-input-number :step="0.01" :min="0" style="width: 100%" |
| | | :precision="2" |
| | | v-model="scope.row.ticketsAmount" |
| | | @change="invoiceAmountBlur(scope.row)" |
| | | <el-input-number |
| | | :step="0.01" |
| | | :min="0" |
| | | style="width: 100%" |
| | | :precision="2" |
| | | v-model="scope.row.ticketsAmount" |
| | | :disabled="isProductDisabled(scope.row)" |
| | | @change="invoiceAmountBlur(scope.row)" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | |
| | | // è®¾ç½®äº§åæ°æ®ï¼å¹¶åå§åå¼ç¥¨æ°éåéé¢ |
| | | allProductData.forEach(item => { |
| | | // ä¿åâåå§æªæ¥ç¥¨æ°/éé¢âï¼ç¨äºæ ¡éªä¸è®¡ç®ï¼ |
| | | // ä¿å"åå§æªæ¥ç¥¨æ°/éé¢"ï¼ç¨äºæ ¡éªä¸è®¡ç®ï¼ |
| | | // ä¼å
使ç¨å端è¿åç futureTickets/futureTicketsAmountï¼æ²¡æååéå° quantity/taxInclusiveTotalPrice |
| | | item.tempFutureTickets = Number( |
| | | item.futureTickets !== undefined ? item.futureTickets : (item.quantity || 0) |
| | |
| | | item.futureTicketsAmount !== undefined ? item.futureTicketsAmount : (item.taxInclusiveTotalPrice || 0) |
| | | ); |
| | | |
| | | // æ°å¢æ¶ï¼æ¬æ¬¡å¼ç¥¨æ°é»è®¤ = æªæ¥ç¥¨æ°ï¼ä¸ä¸è½å¤§äºæªæ¥ç¥¨æ°ï¼ |
| | | item.ticketsNum = Number(item.tempFutureTickets || 0); |
| | | // èå¨è®¡ç®æ¬æ¬¡å¼ç¥¨éé¢ãæªæ¥ç¥¨æ°ãæªæ¥ç¥¨éé¢ |
| | | const unitPrice = Number(item.taxInclusiveUnitPrice || 0); |
| | | item.ticketsAmount = Number((item.ticketsNum * unitPrice).toFixed(2)); |
| | | item.futureTickets = Number((item.tempFutureTickets - item.ticketsNum).toFixed(2)); |
| | | item.futureTicketsAmount = Number( |
| | | (item.tempFutureTicketsAmount - item.ticketsAmount).toFixed(2) |
| | | ); |
| | | // å¦ææªæ¥ç¥¨éé¢ä¸º0ï¼åæ¬æ¬¡å¼ç¥¨æ°åéé¢é½è®¾ç½®ä¸º0 |
| | | if (item.tempFutureTicketsAmount <= 0) { |
| | | item.ticketsNum = 0; |
| | | item.ticketsAmount = 0; |
| | | item.futureTickets = Number(item.tempFutureTickets || 0); |
| | | item.futureTicketsAmount = 0; |
| | | } else { |
| | | // æ°å¢æ¶ï¼æ¬æ¬¡å¼ç¥¨æ°é»è®¤ = æªæ¥ç¥¨æ°ï¼ä¸ä¸è½å¤§äºæªæ¥ç¥¨æ°ï¼ |
| | | item.ticketsNum = Number(item.tempFutureTickets || 0); |
| | | // èå¨è®¡ç®æ¬æ¬¡å¼ç¥¨éé¢ãæªæ¥ç¥¨æ°ãæªæ¥ç¥¨éé¢ |
| | | const unitPrice = Number(item.taxInclusiveUnitPrice || 0); |
| | | item.ticketsAmount = Number((item.ticketsNum * unitPrice).toFixed(2)); |
| | | item.futureTickets = Number((item.tempFutureTickets - item.ticketsNum).toFixed(2)); |
| | | item.futureTicketsAmount = Number( |
| | | (item.tempFutureTicketsAmount - item.ticketsAmount).toFixed(2) |
| | | ); |
| | | } |
| | | }); |
| | | |
| | | form.productData = allProductData; |
| | |
| | | }); |
| | | } else if (type == "edit") { |
| | | const id = Array.isArray(selectedRows) ? selectedRows[0].id : selectedRows; |
| | | const data = await getPurchaseById({ id, type: 2 }); |
| | | form.purchaseLedgerNo = data.purchaseContractNumber; |
| | | const response = await getPurchaseById({ id, type: 2 }); |
| | | // å
¼å®¹ä¸åçè¿åæ ¼å¼ï¼å¯è½æ¯ { code, data } æç´æ¥è¿åæ°æ® |
| | | const data = response.data || response; |
| | | |
| | | // å
¼å®¹ä¸åçåæ®µåï¼purchaseContractNumber æ purchaseLedgerNo |
| | | form.purchaseLedgerNo = data.purchaseContractNumber || data.purchaseLedgerNo || ""; |
| | | form.invoiceAmount = data.invoiceAmount; |
| | | form.invoiceNumber = data.invoiceNumber; |
| | | form.salesContractNo = data.salesContractNo; |
| | | form.projectName = data.projectName; |
| | | form.supplierName = data.supplierName; |
| | | form.entryDate = data.entryDate; |
| | | form.productData = data.productData; |
| | | form.enterDate = data.enterDate || dayjs().format("YYYY-MM-DD"); |
| | | |
| | | // ç¼è¾æ¶ä¹éè¦åå§åäº§åæ°æ®ç tempFutureTickets å tempFutureTicketsAmount |
| | | // åæ¶ä¸ºæ¯ä¸ªäº§åæ·»å ååå·çä¿¡æ¯ |
| | | const contractNumber = data.purchaseContractNumber || data.purchaseLedgerNo || ""; |
| | | if (data.productData && Array.isArray(data.productData)) { |
| | | data.productData.forEach(item => { |
| | | // ä¿å"åå§æªæ¥ç¥¨æ°/éé¢"ï¼ç¨äºæ ¡éªä¸è®¡ç®ï¼ |
| | | // ä¼å
使ç¨å端è¿åç futureTickets/futureTicketsAmountï¼æ²¡æååéå° quantity/taxInclusiveTotalPrice |
| | | item.tempFutureTickets = Number( |
| | | item.futureTickets !== undefined ? item.futureTickets : (item.quantity || 0) |
| | | ); |
| | | item.tempFutureTicketsAmount = Number( |
| | | item.futureTicketsAmount !== undefined ? item.futureTicketsAmount : (item.taxInclusiveTotalPrice || 0) |
| | | ); |
| | | |
| | | // ç¡®ä¿æ¯ä¸ªäº§å齿ååå·ï¼ç¨äºæ¾ç¤ºå¨"æå±åå"å |
| | | if (!item.purchaseLedgerNo) { |
| | | item.purchaseLedgerNo = contractNumber; |
| | | } |
| | | }); |
| | | } |
| | | |
| | | form.productData = data.productData || []; |
| | | |
| | | // ç¼è¾æ¨¡å¼ä¸ï¼æ ¹æ®äº§åæ°æ®ä¸çæ¬æ¬¡å¼ç¥¨éé¢èªå¨è®¡ç®å票éé¢ |
| | | calculateinvoiceAmount(); |
| | | } |
| | | }; |
| | | // å表åè®¡æ¹æ³ |
| | |
| | | form.invoiceAmount = Number(invoiceAmountTotal.toFixed(2)); |
| | | }; |
| | | |
| | | const open = async (type, selectedRows) => { |
| | | visible.value = true; |
| | | // å¤æäº§åæ¯å¦å¯ä»¥ç»§ç»æ¥ç¥¨æä½ï¼å¦ææªæ¥ç¥¨æ°åæªæ¥ç¥¨éé¢é½ä¸º0æå°äºçäº0ï¼åç¦ç¨ |
| | | const isProductDisabled = (row) => { |
| | | // ä¼å
ä½¿ç¨ tempFutureTicketsï¼åå§æªæ¥ç¥¨æ°ï¼ï¼å¦ææ²¡æåä½¿ç¨ futureTickets |
| | | const futureTickets = Number(row.tempFutureTickets !== undefined |
| | | ? row.tempFutureTickets |
| | | : (row.futureTickets !== undefined ? row.futureTickets : 0)); |
| | | |
| | | // å¦ææ¯æ¹éæä½ï¼è®¾ç½®æ é¢ |
| | | if (Array.isArray(selectedRows) && selectedRows.length > 1) { |
| | | modalOptions.value = { |
| | | ...(modalOptions.value || {}), |
| | | title: `æ¹éæ°å¢ (${selectedRows.length}æ¡)`, |
| | | }; |
| | | } else { |
| | | modalOptions.value = { |
| | | ...(modalOptions.value || {}), |
| | | title: type === "add" ? "æ°å¢" : "ç¼è¾", |
| | | }; |
| | | // ä¼å
ä½¿ç¨ tempFutureTicketsAmountï¼åå§æªæ¥ç¥¨éé¢ï¼ï¼å¦ææ²¡æåä½¿ç¨ futureTicketsAmount |
| | | const futureAmount = Number(row.tempFutureTicketsAmount !== undefined |
| | | ? row.tempFutureTicketsAmount |
| | | : (row.futureTicketsAmount !== undefined ? row.futureTicketsAmount : 0)); |
| | | |
| | | // åªæå½æªæ¥ç¥¨æ°åæªæ¥ç¥¨éé¢é½ä¸º0æå°äºçäº0æ¶ï¼æç¦ç¨ |
| | | return futureTickets <= 0 && futureAmount <= 0; |
| | | }; |
| | | |
| | | const open = async (type, selectedRows) => { |
| | | // ç¡®ä¿ modalOptions.value æ¯å¯¹è±¡ |
| | | if (!modalOptions.value || typeof modalOptions.value !== 'object') { |
| | | modalOptions.value = {}; |
| | | } |
| | | |
| | | // æ ¹æ®æä½ç±»ååé䏿°æ®è®¾ç½®æ é¢ |
| | | if (Array.isArray(selectedRows) && selectedRows.length > 1) { |
| | | // æ¹éæä½ |
| | | modalOptions.value.title = type === "add" ? `æ¹éæ°å¢ (${selectedRows.length}æ¡)` : `æ¹éç¼è¾ (${selectedRows.length}æ¡)`; |
| | | } else { |
| | | // å个æä½ - æç¡®å¤æ type çå¼ |
| | | if (type === "add" || type === "æ°å¢") { |
| | | modalOptions.value.title = "æ°å¢"; |
| | | } else if (type === "edit" || type === "ç¼è¾") { |
| | | modalOptions.value.title = "ç¼è¾"; |
| | | } else { |
| | | modalOptions.value.title = "æ¥ç¥¨ç»è®°"; // é»è®¤æ é¢ |
| | | } |
| | | } |
| | | |
| | | visible.value = true; |
| | | |
| | | // 妿æ¯å个æä½ï¼è·åid |
| | | if (!Array.isArray(selectedRows) || selectedRows.length === 1) { |
| | | const idValue = Array.isArray(selectedRows) ? selectedRows[0].id : selectedRows; |
| | |
| | | <div></div> |
| | | <div> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导åº</el-button> |
| | | <el-button type="primary" @click="handleAdd('add')"> |
| | | <el-button type="primary" @click="handleAdd('add')" :disabled="isInvoiceDisabled"> |
| | | æ¥ç¥¨ç»è®° |
| | | </el-button> |
| | | <!-- <el-button type="danger" plain @click="handleDelete">å é¤</el-button>--> |
| | |
| | | <script setup> |
| | | import { usePaginationApi } from "@/hooks/usePaginationApi"; |
| | | import {delRegistration, gePurchaseListPage} from "@/api/procurementManagement/invoiceEntry.js"; |
| | | import { nextTick, onMounted, getCurrentInstance, ref } from "vue"; |
| | | import { nextTick, onMounted, getCurrentInstance, ref, computed } from "vue"; |
| | | import ExpandTable from "./components/ExpandTable.vue"; |
| | | import Modal from "./components/Modal.vue"; |
| | | import {ElMessageBox} from "element-plus"; |
| | |
| | | ); |
| | | }; |
| | | |
| | | // è®¡ç®æ¯å¦å¯ä»¥æ¥ç¥¨ç»è®°ï¼å¦æææéä¸è¡çå¾
æ¥ç¥¨éé¢é½ä¸º0ï¼åç¦ç¨æé® |
| | | const isInvoiceDisabled = computed(() => { |
| | | if (selectedRows.value.length === 0) { |
| | | return true; |
| | | } |
| | | // 妿ææéä¸è¡çå¾
æ¥ç¥¨éé¢é½ä¸º0æå°äºçäº0ï¼åç¦ç¨ |
| | | return selectedRows.value.every(row => { |
| | | const amount = parseFloat(row.unReceiptPaymentAmount || 0); |
| | | return amount <= 0; |
| | | }); |
| | | }); |
| | | |
| | | const handleAdd = (type) => { |
| | | if (selectedRows.value.length < 1) { |
| | | proxy.$modal.msgWarning("请è³å°éä¸ä¸æ¡æ°æ®"); |
| | |
| | | } |
| | | ); |
| | | |
| | | // 主表åè®¡æ¹æ³ |
| | | const summarizeMainTable = (param) => { |
| | | return proxy.summarizeTable( |
| | | const sums = proxy.summarizeTable( |
| | | param, |
| | | [ |
| | | "taxInclusiveTotalPrice", |
| | | "ticketsAmount", |
| | | "unTicketsPrice", |
| | | "invoiceAmount", |
| | | ], |
| | | ["ticketsAmount", "unTicketsPrice", "invoiceAmount"], |
| | | { |
| | | ticketsNum: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | futureTickets: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | ticketsNum: { noDecimal: true }, |
| | | futureTickets: { noDecimal: true }, |
| | | } |
| | | ); |
| | | |
| | | const keySet = new Set(); |
| | | let taxInclusiveSum = 0; |
| | | (param.data || []).forEach((row) => { |
| | | const key = `${row.purchaseContractNumber ?? ""}\n${row.salesContractNo ?? ""}\n${row.productCategory ?? ""}\n${row.specificationModel ?? ""}`; |
| | | if (keySet.has(key)) return; |
| | | keySet.add(key); |
| | | const val = Number(row.taxInclusiveTotalPrice); |
| | | if (!isNaN(val)) taxInclusiveSum += val; |
| | | }); |
| | | const taxInclusiveIndex = (param.columns || []).findIndex( |
| | | (c) => c.property === "taxInclusiveTotalPrice" |
| | | ); |
| | | if (taxInclusiveIndex !== -1) { |
| | | sums[taxInclusiveIndex] = taxInclusiveSum.toFixed(2); |
| | | } |
| | | return sums; |
| | | }; |
| | | |
| | | const handleSelectionChange = (val) => { |
| | |
| | | <el-option v-for="item in supplierList" |
| | | :key="item.id" |
| | | :label="item.supplierName" |
| | | :value="item.id" /> |
| | | :value="item.id" >{{item.supplierName + '---' + item.supplierType}}</el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="isShow" |
| | | title="æ°å¢éè´éè´§" |
| | | width="1600" |
| | | @close="closeModal" |
| | | > |
| | | <el-form label-width="140px" :model="formState" label-position="top" ref="formRef" :inline="true"> |
| | | <div class="section-title"> |
| | | <span class="title-dot"></span> |
| | | <span class="title-text">åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | <el-form-item |
| | | label="éæåå·" |
| | | prop="no" |
| | | :rules="[ |
| | | { |
| | | required: !formState.isDefaultNo, |
| | | message: '请è¾å
¥éæåå·', |
| | | trigger: 'blur', |
| | | } |
| | | ]" |
| | | > |
| | | <el-input |
| | | v-model="formState.no" |
| | | :placeholder="formState.isDefaultNo ? '使ç¨ç³»ç»ç¼å·' : '请è¾å
¥éæåå·'" |
| | | :disabled="formState.isDefaultNo" |
| | | > |
| | | <template #append> |
| | | <el-checkbox v-model="formState.isDefaultNo" size="large" @change="handleChangeIsDefaultNo" /> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="éè´§æ¹å¼" |
| | | prop="returnType" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©éè´§æ¹å¼', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.returnType" |
| | | placeholder="è¯·éæ©éè´§æ¹å¼" |
| | | style="width: 240px" |
| | | > |
| | | <el-option label="éè´§éæ¬¾" :value="0" /> |
| | | <el-option label="ææ¶" :value="1" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="ä¾åºååç§°" |
| | | prop="supplierId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©ä¾åºå', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.supplierId" |
| | | placeholder="è¯·éæ©ä¾åºå" |
| | | style="width: 240px" |
| | | @focus="fetchSupplierOptions" |
| | | @change="handleChangeSupplierId" |
| | | > |
| | | <el-option |
| | | v-for="item in supplierOptions" |
| | | :key="item.id" |
| | | :label="item.supplierName" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="项ç®" |
| | | prop="projectId" |
| | | > |
| | | <el-select |
| | | v-model="formState.projectId" |
| | | placeholder="è¯·éæ©é¡¹ç®" |
| | | style="width: 240px" |
| | | @focus="fetchProjectOptions" |
| | | > |
| | | <el-option |
| | | v-for="item in projectOptions" |
| | | :key="item.id" |
| | | :label="item.name" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="项ç®é¶æ®µ" |
| | | prop="projectPhase" |
| | | > |
| | | <el-select |
| | | v-model="formState.projectPhase" |
| | | placeholder="è¯·éæ©é¡¹ç®é¶æ®µ" |
| | | style="width: 240px" |
| | | > |
| | | <el-option |
| | | v-for="item in projectStageOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="å¶ä½æ¥æ" |
| | | prop="preparedAt" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©å¶ä½æ¥æ', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-date-picker |
| | | v-model="formState.preparedAt" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | type="date" |
| | | placeholder="è¯·éæ©å¶ä½æ¥æ" |
| | | style="width: 240px" |
| | | clearable /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="å¶å人ï¼" |
| | | prop="preparedUserId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©å¶å人', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.preparedUserId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | filterable |
| | | default-first-option |
| | | :reserve-keyword="false" |
| | | style="width: 240px" |
| | | @focus="fetchUserOptions" |
| | | > |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="éæäººï¼" |
| | | prop="returnUserId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©éæäºº', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.returnUserId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | filterable |
| | | default-first-option |
| | | style="width: 240px" |
| | | :reserve-keyword="false" |
| | | @focus="fetchUserOptions" |
| | | > |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="éè´ååå·ï¼" |
| | | prop="purchaseLedgerId" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©éè´ååå·', |
| | | trigger: 'change', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select |
| | | v-model="formState.purchaseLedgerId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | filterable |
| | | default-first-option |
| | | style="width: 240px" |
| | | :reserve-keyword="false" |
| | | @change="handleChangePurchaseLedgerId" |
| | | > |
| | | <el-option |
| | | v-for="item in purchaseLedgerOptions" |
| | | :key="item.id" |
| | | :label="item.purchaseContractNumber" |
| | | :value="item.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="夿³¨ï¼" |
| | | prop="remark" |
| | | > |
| | | <el-input v-model="formState.remark" type="textarea" placeholder="请è¾å
¥å¤æ³¨"/> |
| | | </el-form-item> |
| | | |
| | | <div style="margin: 20px 0;"> |
| | | <div class="section-title"> |
| | | <span class="title-dot"></span> |
| | | <span class="title-text">产åå表</span> |
| | | </div> |
| | | <el-button type="primary" size="small" style="margin-bottom: 20px" @click="isShowProductsModal = true" :disabled="!formState.purchaseLedgerId">æ·»å 产å</el-button> |
| | | <el-table :data="formState.purchaseReturnOrderProductsDtos" |
| | | border |
| | | max-height="400" |
| | | :scroll-y="true" |
| | | show-summary |
| | | :summary-method="summarizeChildrenTable"> |
| | | <el-table-column align="center" |
| | | type="selection" |
| | | width="55" /> |
| | | <el-table-column align="center" |
| | | label="åºå·" |
| | | type="index" |
| | | width="60" /> |
| | | <el-table-column label="产å大类" |
| | | prop="productCategory" /> |
| | | <el-table-column label="è§æ ¼åå·" |
| | | prop="specificationModel" /> |
| | | <el-table-column label="åä½" |
| | | prop="unit" |
| | | width="70" /> |
| | | <el-table-column label="æ°é" |
| | | prop="quantity" |
| | | width="70" /> |
| | | <el-table-column label="éè´§æ°é" |
| | | prop="returnQuantity" |
| | | width="180"> |
| | | <template #default="scope"> |
| | | <el-input-number v-model="scope.row.returnQuantity" |
| | | controls-position="right" |
| | | :step="1" |
| | | :min="1" |
| | | :max="scope.row.quantity" |
| | | required |
| | | placeholder="请è¾å
¥éè´§æ°é" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åºåé¢è¦æ°é" |
| | | prop="warnNum" |
| | | width="120" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="ç¨ç(%)" |
| | | prop="taxRate" |
| | | width="80" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" |
| | | prop="taxInclusiveUnitPrice" |
| | | :formatter="formattedNumber" |
| | | width="150" /> |
| | | <el-table-column label="å«ç¨æ»ä»·(å
)" |
| | | prop="taxInclusiveTotalPrice" |
| | | :formatter="formattedNumber" |
| | | width="150" /> |
| | | <el-table-column label="ä¸å«ç¨æ»ä»·(å
)" |
| | | prop="taxExclusiveTotalPrice" |
| | | :formatter="formattedNumber" |
| | | width="150" /> |
| | | <el-table-column label="æ¯å¦è´¨æ£" |
| | | prop="isChecked" |
| | | width="150"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.isChecked ? 'success' : 'info'"> |
| | | {{ scope.row.isChecked ? 'æ¯' : 'å¦' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column fixed="right" |
| | | label="æä½" |
| | | width="100" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="danger" |
| | | size="small" |
| | | @click="delProduct(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div class="section-title"> |
| | | <span class="title-dot"></span> |
| | | <span class="title-text">è´¹ç¨ä¿¡æ¯</span> |
| | | </div> |
| | | |
| | | <el-form-item |
| | | label="æ´åææ£é¢ï¼" |
| | | prop="totalDiscountAmount" |
| | | > |
| | | <el-input-number v-model="formState.totalDiscountAmount" |
| | | controls-position="right" |
| | | :step="0.01" |
| | | :precision="2" |
| | | style="width: 100%;" |
| | | @change="handleChangeTotalDiscountAmount" |
| | | placeholder="请è¾å
¥æ´åææ£é¢"/> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="æ´åææ£çï¼" |
| | | prop="totalDiscountAmount" |
| | | > |
| | | <el-input-number v-model="formState.totalDiscountRate" |
| | | controls-position="right" |
| | | :step="0.01" |
| | | :precision="2" |
| | | style="width: 100%;" |
| | | placeholder="请è¾å
¥æ´åææ£ç"/> |
| | | </el-form-item> |
| | | |
| | | <el-form-item |
| | | label="æäº¤éé¢ï¼" |
| | | prop="totalAmount" |
| | | > |
| | | <el-input-number v-model="formState.totalAmount" |
| | | controls-position="right" |
| | | :step="0.01" |
| | | :precision="2" |
| | | style="width: 100%;" |
| | | @change="handleChangeTotalAmount" |
| | | placeholder="请è¾å
¥æäº¤éé¢"/> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="handleSubmit">确认</el-button> |
| | | <el-button @click="closeModal">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <ProductList |
| | | v-if="isShowProductsModal" |
| | | v-model:visible="isShowProductsModal" |
| | | :purchase-ledger-id="formState.purchaseLedgerId" |
| | | @completed="handleAddProduct" |
| | | /> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, computed, getCurrentInstance} from "vue"; |
| | | import {createPurchaseReturnOrder} from "@/api/procurementManagement/purchase_return_order.js"; |
| | | import {getOptions, purchaseList} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | const ProductList = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/ProductList.vue")); |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | required: true, |
| | | } |
| | | }); |
| | | let { proxy } = getCurrentInstance() |
| | | const emit = defineEmits(['update:visible', 'completed']); |
| | | |
| | | // ååºå¼æ°æ®ï¼æ¿ä»£é项å¼ç dataï¼ |
| | | const formState = ref({ |
| | | no: '', |
| | | isDefaultNo: true, |
| | | returnType: 0, |
| | | remark: '', |
| | | supplierId: undefined, |
| | | projectId: undefined, |
| | | projectPhase: undefined, |
| | | preparedAt: undefined, |
| | | preparedUserId: undefined, |
| | | returnUserId: undefined, |
| | | purchaseLedgerId: undefined, |
| | | purchaseReturnOrderProductsDtos: [], |
| | | totalDiscountAmount: 0, |
| | | totalDiscountRate: undefined, |
| | | totalAmount: 0, |
| | | }); |
| | | // ä¾åºåé项 |
| | | const supplierOptions = ref([]) |
| | | // 项ç®é项 |
| | | const projectOptions = ref([]) |
| | | // 项ç®é¶æ®µé项 |
| | | const projectStageOptions = ref([ |
| | | { |
| | | label: 'ç«é¡¹', |
| | | value: 0, |
| | | }, |
| | | { |
| | | label: '设计', |
| | | value: 1, |
| | | }, |
| | | { |
| | | label: 'éè´', |
| | | value: 2, |
| | | }, |
| | | { |
| | | label: 'ç产', |
| | | value: 3, |
| | | }, |
| | | { |
| | | label: 'åºè´§', |
| | | value: 4, |
| | | } |
| | | ]) |
| | | // ç¨æ·é项 |
| | | const userOptions = ref([]) |
| | | // éè´å°è´¦é项 |
| | | const purchaseLedgerOptions = ref([]) |
| | | // æ¯å¦å±ç¤ºäº§ååè¡¨æ°æ® |
| | | const isShowProductsModal = ref(false) |
| | | |
| | | const isShow = computed({ |
| | | get() { |
| | | return props.visible; |
| | | }, |
| | | set(val) { |
| | | emit('update:visible', val); |
| | | }, |
| | | }); |
| | | |
| | | const formattedNumber = (row, column, cellValue) => { |
| | | return parseFloat(cellValue).toFixed(2); |
| | | }; |
| | | |
| | | const closeModal = () => { |
| | | isShow.value = false; |
| | | }; |
| | | |
| | | const summarizeChildrenTable = (param) => { |
| | | return proxy.summarizeTable( |
| | | param, |
| | | [ |
| | | "quantity", |
| | | "returnQuantity", |
| | | "taxInclusiveUnitPrice", |
| | | "taxInclusiveTotalPrice", |
| | | "taxExclusiveTotalPrice", |
| | | ], |
| | | { |
| | | quantity: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | returnQuantity: { noDecimal: true }, // ä¸ä¿çå°æ° |
| | | } |
| | | ); |
| | | }; |
| | | |
| | | const handleChangeTotalDiscountAmount= () => { |
| | | formState.value.totalAmount = formState.value.totalDiscountAmount * -1 |
| | | } |
| | | |
| | | const handleChangeTotalAmount= () => { |
| | | formState.value.totalDiscountAmount = formState.value.totalAmount * -1 |
| | | } |
| | | |
| | | // è·åä¾åºåé项 |
| | | const fetchSupplierOptions = () => { |
| | | if (supplierOptions.value.length > 0) { |
| | | return |
| | | } |
| | | getOptions().then((res) => { |
| | | supplierOptions.value = res.data; |
| | | }); |
| | | } |
| | | |
| | | // è·å项ç®é项 |
| | | const fetchProjectOptions = () => { |
| | | if (projectOptions.value.length > 0) { |
| | | return |
| | | } |
| | | // todo 项ç®é项 |
| | | } |
| | | |
| | | // è·åç¨æ·é项 |
| | | const fetchUserOptions = () => { |
| | | if (userOptions.value.length > 0) { |
| | | return |
| | | } |
| | | userListNoPageByTenantId().then((res) => { |
| | | userOptions.value = res.data; |
| | | }); |
| | | } |
| | | |
| | | // å¤çæ¹åä¾åºåæ°æ® |
| | | const handleChangeSupplierId = () => { |
| | | formState.value.purchaseLedgerId = undefined |
| | | fetchPurchaseLedgerOptions() |
| | | } |
| | | |
| | | // è·åéè´å°è´¦é项 |
| | | const fetchPurchaseLedgerOptions = () => { |
| | | purchaseLedgerOptions.value = [] |
| | | if (formState.value.supplierId) { |
| | | purchaseList({supplierId: formState.value.supplierId}).then((res) => { |
| | | purchaseLedgerOptions.value = res.rows; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // å¤çæ¹åéè´å°è´¦æ°æ® |
| | | const handleChangePurchaseLedgerId = () => { |
| | | formState.value.purchaseReturnOrderProductsDtos = [] |
| | | } |
| | | |
| | | // å¤çæ¹åæ¯å¦é»è®¤ç¼å· |
| | | const handleChangeIsDefaultNo = (checked) => { |
| | | if (checked) { |
| | | formState.value.no = '' |
| | | } |
| | | } |
| | | |
| | | // å¢å 产å |
| | | const handleAddProduct = (selectedRows) => { |
| | | const existingIds = new Set(formState.value.purchaseReturnOrderProductsDtos.map(item => item.id)); |
| | | const newProducts = selectedRows.filter(item => !existingIds.has(item.id)).map(item => ({ |
| | | ...item, |
| | | returnQuantity: undefined, |
| | | salesLedgerProductId: item.id, |
| | | })); |
| | | formState.value.purchaseReturnOrderProductsDtos.push(...newProducts); |
| | | } |
| | | |
| | | // å é¤å项产å |
| | | const delProduct = (index) => { |
| | | formState.value.purchaseReturnOrderProductsDtos.splice(index, 1) |
| | | } |
| | | |
| | | // æäº¤è¡¨å |
| | | const handleSubmit = () => { |
| | | // éªè¯éè´§æ°é |
| | | const hasEmptyReturnQuantity = formState.value.purchaseReturnOrderProductsDtos.some(item => !item.returnQuantity || item.returnQuantity <= 0); |
| | | if (hasEmptyReturnQuantity) { |
| | | proxy.$modal.msgError("请为ææäº§åå¡«åéè´§æ°é"); |
| | | return; |
| | | } |
| | | |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | createPurchaseReturnOrder(formState.value).then(res => { |
| | | // å
³éæ¨¡ææ¡ |
| | | isShow.value = false; |
| | | // åç¥ç¶ç»ä»¶å·²å®æ |
| | | emit('completed'); |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | }) |
| | | } |
| | | }) |
| | | }; |
| | | |
| | | defineExpose({ |
| | | closeModal, |
| | | handleSubmit, |
| | | isShow, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .section-title { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | width: 100%; |
| | | clear: both; |
| | | } |
| | | |
| | | .title-dot { |
| | | display: inline-block; |
| | | width: 8px; |
| | | height: 8px; |
| | | background-color: #409EFF; |
| | | border-radius: 50%; |
| | | margin-right: 8px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="isShow" |
| | | title="æ°å¢äº§å" |
| | | width="1200" |
| | | @close="closeModal" |
| | | > |
| | | <div class="table_list"> |
| | | <el-table :data="tableData" |
| | | border |
| | | @selection-change="handleChangeSelection"> |
| | | <el-table-column align="center" |
| | | type="selection" |
| | | width="55" /> |
| | | <el-table-column align="center" |
| | | label="åºå·" |
| | | type="index" |
| | | width="60" /> |
| | | <el-table-column label="产å大类" |
| | | prop="productCategory" /> |
| | | <el-table-column label="è§æ ¼åå·" |
| | | prop="specificationModel" /> |
| | | <el-table-column label="åä½" |
| | | prop="unit" |
| | | width="70" /> |
| | | <el-table-column label="æ°é" |
| | | prop="quantity" |
| | | width="70" /> |
| | | <el-table-column label="åºåé¢è¦æ°é" |
| | | prop="warnNum" |
| | | width="120" |
| | | show-overflow-tooltip /> |
| | | <el-table-column label="ç¨ç(%)" |
| | | prop="taxRate" |
| | | width="80" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" |
| | | prop="taxInclusiveUnitPrice" |
| | | :formatter="formattedNumber" |
| | | width="150" /> |
| | | <el-table-column label="å«ç¨æ»ä»·(å
)" |
| | | prop="taxInclusiveTotalPrice" |
| | | :formatter="formattedNumber" |
| | | width="150" /> |
| | | <el-table-column label="ä¸å«ç¨æ»ä»·(å
)" |
| | | prop="taxExclusiveTotalPrice" |
| | | :formatter="formattedNumber" |
| | | width="150" /> |
| | | <el-table-column label="æ¯å¦è´¨æ£" |
| | | prop="isChecked" |
| | | width="150"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.isChecked ? 'success' : 'info'"> |
| | | {{ scope.row.isChecked ? 'æ¯' : 'å¦' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" :limit="page.size" @pagination="paginationChange" /> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" :disabled="selectedRows.length === 0" @click="handleSubmit">确认</el-button> |
| | | <el-button @click="closeModal">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {computed, reactive, ref, onMounted} from "vue"; |
| | | import {productList} from "@/api/procurementManagement/procurementLedger.js"; |
| | | import {ElMessage} from "element-plus"; |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | required: true, |
| | | }, |
| | | |
| | | purchaseLedgerId: { |
| | | type: Number, |
| | | required: true, |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:visible', 'completed']); |
| | | |
| | | const isShow = computed({ |
| | | get() { |
| | | return props.visible; |
| | | }, |
| | | set(val) { |
| | | emit('update:visible', val); |
| | | }, |
| | | }); |
| | | |
| | | const tableData = ref([]) |
| | | const selectedRows = ref([]) |
| | | const tableLoading = ref(false) |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | const total = ref(0) |
| | | const formattedNumber = (row, column, cellValue) => { |
| | | return parseFloat(cellValue).toFixed(2); |
| | | }; |
| | | |
| | | const paginationChange = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList() |
| | | } |
| | | |
| | | const handleChangeSelection = (val) => { |
| | | selectedRows.value = val; |
| | | } |
| | | |
| | | const fetchData = () => { |
| | | tableLoading.value = true; |
| | | productList({salesLedgerId: props.purchaseLedgerId, type: 2}).then((res) => { |
| | | tableData.value = res.data; |
| | | }).finally(() => { |
| | | tableLoading.value = false; |
| | | }) |
| | | } |
| | | |
| | | const handleSubmit = () => { |
| | | if (selectedRows.value.length === 0) { |
| | | ElMessage.warning("è¯·éæ©ä¸æ¡äº§å"); |
| | | return; |
| | | } |
| | | |
| | | emit('completed', selectedRows.value); |
| | | closeModal() |
| | | } |
| | | |
| | | const closeModal = () => { |
| | | isShow.value = false; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | fetchData() |
| | | }) |
| | | |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <el-form :model="searchForm" |
| | | :inline="true"> |
| | | <el-form-item label="éæåå·ï¼"> |
| | | <el-input v-model="searchForm.no" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | prefix-icon="Search" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item> |
| | | <el-button type="primary" |
| | | @click="handleQuery"> æç´¢ </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <div> |
| | | <el-button type="primary" @click="isShowNewModal = true">æ°å¢</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange" :row-key="row => row.id" style="width: 100%" height="calc(100vh - 18.5em)"> |
| | | <el-table-column align="center" type="selection" width="55" /> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="éæåå·" prop="no" show-overflow-tooltip /> |
| | | <el-table-column label="éè´§æ¹å¼" prop="returnType" show-overflow-tooltip /> |
| | | <el-table-column label="ä¾åºååç§°" prop="supplierName" show-overflow-tooltip /> |
| | | <el-table-column label="å
³èåå·" prop="purchaseContractNumber" show-overflow-tooltip /> |
| | | <el-table-column label="éæäºº" prop="returnUserName" show-overflow-tooltip /> |
| | | <el-table-column label="夿³¨" prop="remark" show-overflow-tooltip /> |
| | | <el-table-column label="å建人" prop="createUserName" show-overflow-tooltip /> |
| | | <el-table-column label="å建æ¶é´" prop="createTime" show-overflow-tooltip /> |
| | | <el-table-column label="æè¿æ´æ°æ¶é´" prop="updateTime" show-overflow-tooltip /> |
| | | <el-table-column fixed="right" label="æä½" min-width="60" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small">详æ
</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper" |
| | | :page="page.current" :limit="page.size" @pagination="paginationChange" /> |
| | | </div> |
| | | <new v-if="isShowNewModal" |
| | | v-model:visible="isShowNewModal" |
| | | @completed="handleQuery" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import pagination from '@/components/PIMTable/Pagination.vue' |
| | | import { ref, reactive, toRefs, onMounted } from 'vue' |
| | | import {findPurchaseReturnOrderListPage} from "@/api/procurementManagement/purchase_return_order.js"; |
| | | const New = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/New.vue")); |
| | | const tableData = ref([]) |
| | | const selectedRows = ref([]) |
| | | const tableLoading = ref(false) |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 100, |
| | | }) |
| | | const total = ref(0) |
| | | // æ¯å¦æ¾ç¤ºæ°å¢å¼¹æ¡ |
| | | const isShowNewModal = ref(false) |
| | | const data = reactive({ |
| | | searchForm: { |
| | | no: '', |
| | | } |
| | | }) |
| | | const { searchForm } = toRefs(data) |
| | | |
| | | // æ¥è¯¢å表 |
| | | /** æç´¢æé®æä½ */ |
| | | const handleQuery = () => { |
| | | page.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | const paginationChange = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList() |
| | | } |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true |
| | | findPurchaseReturnOrderListPage({ ...searchForm.value, ...page }).then(res => { |
| | | tableLoading.value = false |
| | | tableData.value = res.data.records |
| | | total.value = res.data.total |
| | | }).catch(() => { |
| | | tableLoading.value = false |
| | | }) |
| | | } |
| | | |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | // è¿æ»¤æåæ°æ® |
| | | selectedRows.value = selection.filter(item => item.id); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <el-button @click="closeDialog">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleSubmit" :loading="submitLoading">ç¡®å®</el-button> |
| | | <el-button @click="closeDialog">åæ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="closeDialog">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleSubmit">ç¡®å®</el-button> |
| | | <el-button @click="closeDialog">åæ¶</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="bindRouteDialogVisible = false">å æ¶</el-button> |
| | | <el-button type="primary" |
| | | :loading="bindRouteSaving" |
| | | @click="handleBindRouteConfirm">确 认</el-button> |
| | | <el-button @click="bindRouteDialogVisible = false">å æ¶</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | |
| | | <el-form-item label="å·¥åºç¼å·" prop="no"> |
| | | <el-input v-model="formState.no" /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="å·¥åºç±»å" |
| | | prop="type" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©å·¥åºç±»å', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select v-model="formState.type" placeholder="è¯·éæ©å·¥åºç±»å"> |
| | | <el-option label="计æ¶" :value="0" /> |
| | | <el-option label="计件" :value="1" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥èµå®é¢" prop="salaryQuota"> |
| | | <el-input v-model="formState.salaryQuota" type="number" :step="0.001" /> |
| | | </el-form-item> |
| | |
| | | const formState = ref({ |
| | | id: props.record.id, |
| | | name: props.record.name, |
| | | type: props.record.type, |
| | | no: props.record.no, |
| | | remark: props.record.remark, |
| | | salaryQuota: props.record.salaryQuota, |
| | |
| | | id: newRecord.id, |
| | | name: newRecord.name || '', |
| | | no: newRecord.no || '', |
| | | type: newRecord.type, |
| | | remark: newRecord.remark || '', |
| | | salaryQuota: newRecord.salaryQuota || '', |
| | | isQuality: props.record.isQuality, |
| | |
| | | id: props.record.id, |
| | | name: props.record.name || '', |
| | | no: props.record.no || '', |
| | | type: props.record.type, |
| | | remark: props.record.remark || '', |
| | | salaryQuota: props.record.salaryQuota || '', |
| | | isQuality: props.record.isQuality, |
| | |
| | | <el-form-item label="å·¥åºç¼å·" prop="no"> |
| | | <el-input v-model="formState.no" /> |
| | | </el-form-item> |
| | | <el-form-item |
| | | label="å·¥åºç±»å" |
| | | prop="type" |
| | | :rules="[ |
| | | { |
| | | required: true, |
| | | message: 'è¯·éæ©å·¥åºç±»å', |
| | | } |
| | | ]" |
| | | > |
| | | <el-select v-model="formState.type" placeholder="è¯·éæ©å·¥åºç±»å"> |
| | | <el-option label="计æ¶" :value="0" /> |
| | | <el-option label="计件" :value="1" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="å·¥èµå®é¢" prop="salaryQuota"> |
| | | <el-input v-model="formState.salaryQuota" type="number" :step="0.001" /> |
| | | <el-input v-model="formState.salaryQuota" type="number" :step="0.001"> |
| | | <template #append>å
</template> |
| | | </el-input> |
| | | </el-form-item> |
| | | <el-form-item label="æ¯å¦è´¨æ£" prop="isQuality"> |
| | | <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/> |
| | |
| | | // ååºå¼æ°æ®ï¼æ¿ä»£é项å¼ç dataï¼ |
| | | const formState = ref({ |
| | | name: '', |
| | | type: undefined, |
| | | remark: '', |
| | | salaryQuota: '', |
| | | isQuality: false, |
| | |
| | | prop: "name", |
| | | }, |
| | | { |
| | | label: "å·¥åºç±»å", |
| | | prop: "typeText", |
| | | }, |
| | | { |
| | | label: "å·¥èµå®é¢", |
| | | prop: "salaryQuota", |
| | | }, |
| | |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records.map(item => ({ |
| | | ...item, |
| | | typeText: item.type !== undefined && item.type !== null ? (item.type === 0 ? "计æ¶" : "计件") : "", |
| | | })); |
| | | page.total = res.data.total; |
| | | }) |
| | |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å·¥åº", |
| | | prop: "process", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å·¥åç¼å·", |
| | | prop: "workOrderNo", |
| | | width: 120, |
| | |
| | | :page="page" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination"> |
| | | <template #completionStatus="{ row }"> |
| | | <el-progress :percentage="toProgressPercentage(row?.completionStatus)" :color="progressColor(toProgressPercentage(row?.completionStatus))" :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> |
| | | </template> |
| | | </PIMTable> |
| | | <template #completionStatus="{ row }"> |
| | | <el-progress :percentage="toProgressPercentage(row?.completionStatus)" |
| | | :color="progressColor(toProgressPercentage(row?.completionStatus))" |
| | | :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" /> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <el-dialog v-model="editDialogVisible" |
| | | title="ç¼è¾æ¶é´" |
| | |
| | | transferCardRowData.status |
| | | }}</span> |
| | | </div> --> |
| | | |
| | | <div class="info-item"> |
| | | <span class="info-label">计åå¼å§æ¶é´</span> |
| | | <span class="info-value">{{ transferCardRowData.planStartTime }}</span> |
| | |
| | | <el-dialog v-model="reportDialogVisible" |
| | | title="æ¥å·¥" |
| | | width="500px"> |
| | | <el-form :model="reportForm" |
| | | <el-form ref="reportFormRef" |
| | | :model="reportForm" |
| | | :rules="reportFormRules" |
| | | label-width="120px"> |
| | | <el-form-item label="å¾
ç产æ°é"> |
| | | <el-input v-model="reportForm.planQuantity" |
| | | readonly |
| | | style="width: 300px" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¬æ¬¡ç产æ°é"> |
| | | <el-form-item label="æ¬æ¬¡ç产æ°é" |
| | | prop="quantity"> |
| | | <el-input v-model.number="reportForm.quantity" |
| | | type="number" |
| | | min="1" |
| | | step="1" |
| | | style="width: 300px" |
| | | placeholder="请è¾å
¥æ¬æ¬¡ç产æ°é" /> |
| | | placeholder="请è¾å
¥æ¬æ¬¡ç产æ°é" |
| | | @input="handleQuantityInput" /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥åºæ°é"> |
| | | <el-form-item label="æ¥åºæ°é" |
| | | prop="scrapQty"> |
| | | <el-input v-model.number="reportForm.scrapQty" |
| | | type="number" |
| | | min="1" |
| | | min="0" |
| | | step="1" |
| | | style="width: 300px" |
| | | placeholder="请è¾å
¥æ¥åºæ°é" /> |
| | | placeholder="请è¾å
¥æ¥åºæ°é" |
| | | @input="handleScrapQtyInput" /> |
| | | </el-form-item> |
| | | <el-form-item label="çç»ä¿¡æ¯"> |
| | | <el-select v-model="reportForm.userId" |
| | |
| | | @change="handleUserChange"> |
| | | <el-option v-for="user in userOptions" |
| | | :key="user.userId" |
| | | :label="user.userName" |
| | | :label="user.nickName" |
| | | :value="user.userId" /> |
| | | </el-select> |
| | | </el-form-item> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { onMounted, ref } from "vue"; |
| | | import { onMounted, ref, nextTick } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | |
| | | const transferCardRowData = ref(null); |
| | | const reportDialogVisible = ref(false); |
| | | const workOrderFilesRef = ref(null); |
| | | const reportFormRef = ref(null); |
| | | const userOptions = ref([]); |
| | | const reportForm = reactive({ |
| | | planQuantity: 0, |
| | | quantity: 0, |
| | | quantity: null, |
| | | scrapQty: null, |
| | | userName: "", |
| | | workOrderId: "", |
| | | reportWork: "", |
| | |
| | | userId: "", |
| | | productMainId: null, |
| | | }); |
| | | |
| | | // æ¬æ¬¡ç产æ°ééªè¯è§å |
| | | const validateQuantity = (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(new Error("请è¾å
¥æ¬æ¬¡ç产æ°é")); |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | // æ´æ°ä¸å¤§äºçäº1 |
| | | if (isNaN(num) || !Number.isInteger(num) || num < 1) { |
| | | callback(new Error("æ¬æ¬¡ç产æ°éå¿
须大äºçäº1")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }; |
| | | |
| | | // æ¥åºæ°ééªè¯è§å |
| | | const validateScrapQty = (rule, value, callback) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | callback(); |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | // æ´æ°ä¸å¤§äºçäº0 |
| | | if (isNaN(num) || !Number.isInteger(num) || num < 0) { |
| | | callback(new Error("æ¥åºæ°éå¿
须大äºçäº0")); |
| | | return; |
| | | } |
| | | callback(); |
| | | }; |
| | | |
| | | // éªè¯è§å |
| | | const reportFormRules = { |
| | | quantity: [{ required: true, validator: validateQuantity, trigger: "blur" }], |
| | | scrapQty: [{ validator: validateScrapQty, trigger: "blur" }], |
| | | }; |
| | | |
| | | // å¤çæ¬æ¬¡ç产æ°éè¾å
¥ï¼éå¶å¿
须大äºçäº1 |
| | | const handleQuantityInput = value => { |
| | | if (value === "" || value === null || value === undefined) { |
| | | reportForm.quantity = null; |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | if (isNaN(num)) { |
| | | return; |
| | | } |
| | | // 妿å°äº1ï¼æ¸
é¤ |
| | | if (num < 1) { |
| | | reportForm.quantity = null; |
| | | return; |
| | | } |
| | | // 妿æ¯å°æ°åæ´æ°é¨å |
| | | if (!Number.isInteger(num)) { |
| | | const intValue = Math.floor(num); |
| | | // 妿忴åå°äº1ï¼æ¸
é¤ |
| | | if (intValue < 1) { |
| | | reportForm.quantity = null; |
| | | return; |
| | | } |
| | | reportForm.quantity = intValue; |
| | | return; |
| | | } |
| | | reportForm.quantity = num; |
| | | }; |
| | | |
| | | // å¤çæ¥åºæ°é |
| | | const handleScrapQtyInput = value => { |
| | | if (value === "" || value === null || value === undefined) { |
| | | reportForm.scrapQty = null; |
| | | return; |
| | | } |
| | | const num = Number(value); |
| | | // 妿æ¯NaNï¼ä¿æåå¼ |
| | | if (isNaN(num)) { |
| | | return; |
| | | } |
| | | // 妿æ¯è´æ°ï¼æ¸
é¤è¾å
¥ |
| | | if (num < 0) { |
| | | reportForm.scrapQty = null; |
| | | return; |
| | | } |
| | | // 妿æ¯å°æ°ï¼åæ´æ°é¨å |
| | | if (!Number.isInteger(num)) { |
| | | reportForm.scrapQty = Math.floor(num); |
| | | return; |
| | | } |
| | | // ææçéè´æ´æ°ï¼å
æ¬0ï¼ |
| | | reportForm.scrapQty = num; |
| | | }; |
| | | const currentReportRowData = ref(null); |
| | | const page = reactive({ |
| | | current: 1, |
| | |
| | | |
| | | // å建 Blob URL |
| | | const fileBlob = |
| | | blob instanceof Blob ? blob : new Blob([blob], { type: blob.type || "application/octet-stream" }); |
| | | blob instanceof Blob |
| | | ? blob |
| | | : new Blob([blob], { type: blob.type || "application/octet-stream" }); |
| | | const url = window.URL.createObjectURL(fileBlob); |
| | | |
| | | // å建éè iframeï¼ç¨äºè§¦åæµè§å¨æå° |
| | |
| | | const showReportDialog = row => { |
| | | currentReportRowData.value = row; |
| | | reportForm.planQuantity = row.planQuantity; |
| | | reportForm.quantity = row.quantity; |
| | | reportForm.quantity = |
| | | row.quantity !== undefined && row.quantity !== null ? row.quantity : null; |
| | | reportForm.productProcessRouteItemId = row.productProcessRouteItemId; |
| | | reportForm.workOrderId = row.id; |
| | | reportForm.reportWork = row.reportWork; |
| | | reportForm.productMainId = row.productMainId; |
| | | reportForm.scrapQty = row.scrapQty; |
| | | reportForm.scrapQty = |
| | | row.scrapQty !== undefined && row.scrapQty !== null ? row.scrapQty : null; |
| | | nextTick(() => { |
| | | reportFormRef.value?.clearValidate(); |
| | | }); |
| | | // è·åå½åç»å½ç¨æ·ä¿¡æ¯ï¼è®¾ç½®ä¸ºé»è®¤éä¸ |
| | | getUserProfile() |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | reportForm.userId = res.data.userId; |
| | | reportForm.userName = res.data.userName; |
| | | reportForm.userName = res.data.nickName; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | |
| | | }; |
| | | |
| | | const handleReport = () => { |
| | | if (reportForm.planQuantity <= 0) { |
| | | ElMessageBox.alert("å¾
ç产æ°é为0ï¼æ æ³æ¥å·¥", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | if (!reportForm.quantity || reportForm.quantity <= 0) { |
| | | ElMessageBox.alert("请è¾å
¥ææçæ¬æ¬¡ç产æ°é", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | if (reportForm.quantity > reportForm.planQuantity) { |
| | | ElMessageBox.alert("æ¬æ¬¡ç产æ°éä¸è½è¶
è¿å¾
ç产æ°é", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | // console.log(reportForm); |
| | | addProductMain(reportForm).then(res => { |
| | | if (res.code === 200) { |
| | | proxy.$modal.msgSuccess("æ¥å·¥æå"); |
| | | reportDialogVisible.value = false; |
| | | getList(); |
| | | } else { |
| | | ElMessageBox.alert(res.msg || "æ¥å·¥å¤±è´¥", "æç¤º", { |
| | | reportFormRef.value?.validate(valid => { |
| | | if (!valid) { |
| | | return false; |
| | | } |
| | | |
| | | if (reportForm.planQuantity <= 0) { |
| | | ElMessageBox.alert("å¾
ç产æ°é为0ï¼æ æ³æ¥å·¥", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // éªè¯æ¬æ¬¡ç产æ°é |
| | | if ( |
| | | reportForm.quantity === null || |
| | | reportForm.quantity === undefined || |
| | | reportForm.quantity === "" |
| | | ) { |
| | | ElMessageBox.alert("请è¾å
¥æ¬æ¬¡ç产æ°é", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const quantity = Number(reportForm.quantity); |
| | | const scrapQty = |
| | | reportForm.scrapQty === null || |
| | | reportForm.scrapQty === undefined || |
| | | reportForm.scrapQty === "" |
| | | ? 0 |
| | | : Number(reportForm.scrapQty); |
| | | |
| | | // æ¬æ¬¡ç产æ°é |
| | | if (isNaN(quantity) || !Number.isInteger(quantity) || quantity < 1) { |
| | | ElMessageBox.alert("æ¬æ¬¡ç产æ°éå¿
须大äºçäº1", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // æ¥åºæ°éå¿
é¡»æ¯æ´æ°ä¸å¤§äºçäº0 |
| | | if (isNaN(scrapQty) || !Number.isInteger(scrapQty) || scrapQty < 0) { |
| | | ElMessageBox.alert("æ¥åºæ°éå¿
须大äºçäº0", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | if (quantity > reportForm.planQuantity) { |
| | | ElMessageBox.alert("æ¬æ¬¡ç产æ°éä¸è½è¶
è¿å¾
ç产æ°é", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const submitData = { |
| | | ...reportForm, |
| | | quantity: quantity, |
| | | scrapQty: scrapQty, |
| | | }; |
| | | |
| | | // console.log(submitData); |
| | | addProductMain(submitData).then(res => { |
| | | if (res.code === 200) { |
| | | proxy.$modal.msgSuccess("æ¥å·¥æå"); |
| | | reportDialogVisible.value = false; |
| | | getList(); |
| | | } else { |
| | | ElMessageBox.alert(res.msg || "æ¥å·¥å¤±è´¥", "æç¤º", { |
| | | confirmButtonText: "ç¡®å®", |
| | | }); |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | |
| | | }; |
| | | |
| | | // ç¨æ·éæ©ååæ¶æ´æ° userName |
| | | const handleUserChange = (userId) => { |
| | | const handleUserChange = userId => { |
| | | if (userId) { |
| | | const selectedUser = userOptions.value.find(user => user.userId === userId); |
| | | if (selectedUser) { |
| | | reportForm.userName = selectedUser.userName; |
| | | reportForm.userName = selectedUser.nickName; |
| | | } |
| | | } else { |
| | | reportForm.userName = ""; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | width="95%" |
| | | top="5vh" |
| | | destroy-on-close |
| | | @close="closeDialog" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-position="top" |
| | | label-width="120px" |
| | | :disabled="isView" |
| | | > |
| | | <div class="section"> |
| | | <div class="section-header" @click="toggleSection('base')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>åºç¡èµæ</span> |
| | | </div> |
| | | <el-icon class="toggle-icon"> |
| | | <ArrowDown v-if="sectionCollapsed.base" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.base" class="section-body"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="åæ®ç¼å·" prop="billNo"> |
| | | <el-input v-model="form.billNo" placeholder="ç³»ç»çæ" disabled /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="项ç®åç§°" prop="projectName"> |
| | | <el-input v-model="form.projectName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="客æ·åç§°" prop="customerName"> |
| | | <el-input v-model="form.customerName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="ç«é¡¹æ¥æ" prop="setupDate"> |
| | | <el-date-picker |
| | | v-model="form.setupDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="é¡¹ç®æ¥æº" prop="projectSource"> |
| | | <el-input v-model="form.projectSource" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="ç«é¡¹äºº" prop="creatorName"> |
| | | <el-input v-model="form.creatorName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="é¢è®¡å·¥æ(天)" prop="estimatedDays"> |
| | | <el-input-number v-model="form.estimatedDays" :min="0" controls-position="right" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="计åå¼å§æ¥æ" prop="planStartDate"> |
| | | <el-date-picker |
| | | v-model="form.planStartDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="计åå®ææ¥æ" prop="planEndDate"> |
| | | <el-date-picker |
| | | v-model="form.planEndDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="项ç®ç±»å" prop="projectManagementPlanId"> |
| | | <el-select v-model="form.projectManagementPlanId" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option v-for="opt in projectTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="项ç®éé¢" prop="projectAmount"> |
| | | <el-input-number v-model="form.projectAmount" :min="0" controls-position="right" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="å®¡æ ¸ç¶æ" prop="auditStatus"> |
| | | <el-select v-model="form.auditStatus" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option v-for="d in project_management" :key="d.value" :label="d.label" :value="d.value" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | |
| | | </el-row> |
| | | <el-row :gutter="10" > |
| | | <el-col :span="24"> |
| | | <el-upload |
| | | v-model:file-list="fileList" |
| | | :action="upload.url" |
| | | :headers="upload.headers" |
| | | multiple |
| | | :disabled="isView" |
| | | :before-upload="beforeUpload" |
| | | :on-success="handleUploadSuccess" |
| | | :on-error="handleUploadError" |
| | | name="files" |
| | | :on-remove="handleRemove" |
| | | > |
| | | <el-button type="primary" :disabled="isView">ä¸ä¼ æä»¶</el-button> |
| | | </el-upload> |
| | | <div v-if="existingAttachments.length > 0" class="attachment-list"> |
| | | <div |
| | | v-for="(att, idx) in existingAttachments" |
| | | :key="att.id || att.url || idx" |
| | | class="attachment-item" |
| | | > |
| | | <el-icon><Document /></el-icon> |
| | | <span class="attachment-name">{{ att.name || att.fileName || att.url || 'éä»¶' }}</span> |
| | | <el-button link type="primary" size="small" @click="downloadAttachment(att)">ä¸è½½</el-button> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请è¾å
¥" maxlength="100" show-word-limit /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="section"> |
| | | <div class="section-header" @click="toggleSection('product')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>产åä¿¡æ¯</span> |
| | | </div> |
| | | <div class="section-actions" @click.stop> |
| | | <el-button v-if="!isView" type="primary" @click="openProductForm('add')">æ·»å </el-button> |
| | | <el-button v-if="!isView" plain type="danger" @click="deleteProduct">å é¤</el-button> |
| | | <el-icon class="toggle-icon" @click="toggleSection('product')"> |
| | | <ArrowDown v-if="sectionCollapsed.product" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.product" class="section-body"> |
| | | <el-table |
| | | :data="productData" |
| | | border |
| | | show-summary |
| | | :summary-method="summarizeProductTable" |
| | | @selection-change="productSelected" |
| | | > |
| | | <el-table-column v-if="!isView" align="center" type="selection" width="55" /> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="产å大类" prop="productCategory" /> |
| | | <el-table-column label="è§æ ¼åå·" prop="specificationModel" /> |
| | | <el-table-column label="åä½" prop="unit" /> |
| | | <el-table-column label="æ°é" prop="quantity" /> |
| | | <el-table-column label="ç¨ç(%)" prop="taxRate" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="å«ç¨æ»ä»·(å
)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="ä¸å«ç¨æ»ä»·(å
)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column v-if="!isView" fixed="right" label="æä½" min-width="60" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row, scope.$index)">ç¼è¾</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="section"> |
| | | <div class="section-header" @click="toggleSection('team')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>项ç®å¢é</span> |
| | | </div> |
| | | <div class="section-actions" @click.stop> |
| | | <el-button v-if="!isView" type="primary" :icon="Plus" @click="addTeamRow">æ°å¢è¡</el-button> |
| | | <el-icon class="toggle-icon" @click="toggleSection('team')"> |
| | | <ArrowDown v-if="sectionCollapsed.team" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.team" class="section-body"> |
| | | <PIMTable |
| | | :column="teamColumns" |
| | | :tableData="form.teamList" |
| | | :tableLoading="false" |
| | | :isSelection="false" |
| | | :isShowPagination="false" |
| | | height="220" |
| | | > |
| | | <template #memberId="{ row }"> |
| | | <el-select v-model="row.memberId" placeholder="è¯·éæ©" filterable clearable style="width: 100%" :disabled="isView"> |
| | | <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #roleId="{ row }"> |
| | | <el-select v-model="row.roleId" placeholder="è¯·éæ©" clearable style="width: 100%" :disabled="isView"> |
| | | <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #enterDate="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.enterDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #leaveDate="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.leaveDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #phone="{ row }"> |
| | | <el-input v-model="row.phone" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #teamRemark="{ row }"> |
| | | <el-input v-model="row.remark" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #teamAction="{ row, index }"> |
| | | <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeTeamRow(index)">å é¤</el-button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- <div class="section"> |
| | | <div class="section-header" @click="toggleSection('phase')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>项ç®é¶æ®µ</span> |
| | | </div> |
| | | <div class="section-actions" @click.stop> |
| | | <el-button v-if="!isView" type="primary" :icon="Plus" @click="addPhaseRow">æ°å¢è¡</el-button> |
| | | <el-icon class="toggle-icon" @click="toggleSection('phase')"> |
| | | <ArrowDown v-if="sectionCollapsed.phase" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.phase" class="section-body"> |
| | | <PIMTable |
| | | :column="phaseColumns" |
| | | :tableData="form.phaseList" |
| | | :tableLoading="false" |
| | | :isSelection="false" |
| | | :isShowPagination="false" |
| | | height="240" |
| | | > |
| | | <template #phaseName="{ row }"> |
| | | <el-input v-model="row.phaseName" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #phaseDesc="{ row }"> |
| | | <el-input v-model="row.description" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #ownerId="{ row }"> |
| | | <el-select v-model="row.ownerId" placeholder="è¯·éæ©" filterable clearable style="width: 100%" :disabled="isView"> |
| | | <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #planDays="{ row }"> |
| | | <el-input-number v-model="row.planDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" /> |
| | | </template> |
| | | <template #planStart="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.planStartDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #planEnd="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.planEndDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #progress="{ row }"> |
| | | <el-input-number v-model="row.progress" :min="0" :max="100" controls-position="right" style="width: 100%" :disabled="isView" /> |
| | | </template> |
| | | <template #actualStart="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.actualStartDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #actualEnd="{ row }"> |
| | | <el-date-picker |
| | | v-model="row.actualEndDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | :disabled="isView" |
| | | /> |
| | | </template> |
| | | <template #overdueDays="{ row }"> |
| | | <el-input-number v-model="row.overdueDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" /> |
| | | </template> |
| | | <template #completion="{ row }"> |
| | | <el-input v-model="row.completionRemark" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #phaseAction="{ row, index }"> |
| | | <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removePhaseRow(index)">å é¤</el-button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> --> |
| | | |
| | | <div class="section"> |
| | | <div class="section-header" @click="toggleSection('address')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>æ¶è´§å°å</span> |
| | | </div> |
| | | <div class="section-actions" @click.stop> |
| | | <el-button v-if="!isView" type="primary" :icon="Plus" @click="addAddressRow">æ°å¢è¡</el-button> |
| | | <el-icon class="toggle-icon" @click="toggleSection('address')"> |
| | | <ArrowDown v-if="sectionCollapsed.address" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.address" class="section-body"> |
| | | <PIMTable |
| | | :column="addressColumns" |
| | | :tableData="form.addressList" |
| | | :tableLoading="false" |
| | | :isSelection="false" |
| | | :isShowPagination="false" |
| | | height="200" |
| | | > |
| | | <template #receiver="{ row }"> |
| | | <el-input v-model="row.receiver" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #receiverPhone="{ row }"> |
| | | <el-input v-model="row.phone" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #receiverAddress="{ row }"> |
| | | <el-input v-model="row.address" placeholder="请è¾å
¥" clearable :disabled="isView" /> |
| | | </template> |
| | | <template #addressAction="{ row, index }"> |
| | | <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeAddressRow(index)">å é¤</el-button> |
| | | <span v-else>â</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="section"> |
| | | <div class="section-header" @click="toggleSection('contact')"> |
| | | <div class="section-title"> |
| | | <span class="section-bar" /> |
| | | <span>è系信æ¯</span> |
| | | </div> |
| | | <el-icon class="toggle-icon"> |
| | | <ArrowDown v-if="sectionCollapsed.contact" /> |
| | | <ArrowUp v-else /> |
| | | </el-icon> |
| | | </div> |
| | | <div v-show="!sectionCollapsed.contact" class="section-body"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="è系人å§å" prop="contactName"> |
| | | <el-input v-model="form.contactName" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="æ§å«" prop="contactGender"> |
| | | <el-select v-model="form.contactGender" placeholder="è¯·éæ©" clearable style="width: 100%"> |
| | | <el-option label="ç·" value="1" /> |
| | | <el-option label="女" value="2" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="çæ¥" prop="contactBirthday"> |
| | | <el-date-picker |
| | | v-model="form.contactBirthday" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | placeholder="è¯·éæ©" |
| | | style="width: 100%" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="é®ç®±" prop="contactEmail"> |
| | | <el-input v-model="form.contactEmail" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="é¨é¨" prop="contactDept"> |
| | | <el-input v-model="form.contactDept" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="èå¡" prop="contactJob"> |
| | | <el-input v-model="form.contactJob" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="ææºå·ç " prop="contactMobile"> |
| | | <el-input v-model="form.contactMobile" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="微信å·ç " prop="contactWechat"> |
| | | <el-input v-model="form.contactWechat" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <el-form-item label="QQ" prop="contactQq"> |
| | | <el-input v-model="form.contactQq" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-form-item label="ä¼ä¸å¾®ä¿¡" prop="contactWorkWechat"> |
| | | <el-input v-model="form.contactWorkWechat" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å°å" prop="contactAddress"> |
| | | <el-input v-model="form.contactAddress" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="夿³¨" prop="contactRemark"> |
| | | <el-input v-model="form.contactRemark" type="textarea" :rows="2" placeholder="请è¾å
¥" maxlength="200" show-word-limit /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button v-if="!isView" type="primary" @click="submitForm">确认</el-button> |
| | | <el-button @click="closeDialog">{{ isView ? 'å
³é' : 'åæ¶' }}</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <FormDialog |
| | | v-model="productFormVisible" |
| | | :title="productOperationType === 'add' ? 'æ°å¢äº§å' : 'ç¼è¾äº§å'" |
| | | :width="'40%'" |
| | | :operation-type="productOperationType" |
| | | @close="closeProductDia" |
| | | @confirm="submitProduct" |
| | | @cancel="closeProductDia" |
| | | > |
| | | <el-form ref="productFormRef" :model="productForm" label-width="140px" label-position="top" :rules="productRules"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="产å大类ï¼" prop="productCategoryId"> |
| | | <el-tree-select |
| | | v-model="productForm.productCategoryId" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | check-strictly |
| | | :data="productCategoryOptions" |
| | | :render-after-expand="false" |
| | | style="width: 100%" |
| | | @change="getModels" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="è§æ ¼åå·ï¼" prop="productModelId"> |
| | | <el-select v-model="productForm.productModelId" placeholder="è¯·éæ©" clearable filterable @change="getProductModel"> |
| | | <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åä½ï¼" prop="unit"> |
| | | <el-input v-model="productForm.unit" placeholder="请è¾å
¥" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¨ç(%)ï¼" prop="taxRate"> |
| | | <el-select v-model="productForm.taxRate" placeholder="è¯·éæ©" clearable @change="calculateFromTaxRate"> |
| | | <el-option label="1" value="1" /> |
| | | <el-option label="6" value="6" /> |
| | | <el-option label="13" value="13" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å«ç¨åä»·(å
)ï¼" prop="taxInclusiveUnitPrice"> |
| | | <el-input-number |
| | | v-model="productForm.taxInclusiveUnitPrice" |
| | | :step="0.01" |
| | | :min="0" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @change="calculateFromUnitPrice" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æ°éï¼" prop="quantity"> |
| | | <el-input-number |
| | | v-model="productForm.quantity" |
| | | :step="0.1" |
| | | :min="0" |
| | | :precision="2" |
| | | style="width: 100%" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | @change="calculateFromQuantity" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å«ç¨æ»ä»·(å
)ï¼" prop="taxInclusiveTotalPrice"> |
| | | <el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="请è¾å
¥" clearable @change="calculateFromTotalPrice" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¸å«ç¨æ»ä»·(å
)ï¼" prop="taxExclusiveTotalPrice"> |
| | | <el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="请è¾å
¥" clearable @change="calculateFromExclusiveTotalPrice" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å票类åï¼" prop="invoiceType"> |
| | | <el-select v-model="productForm.invoiceType" placeholder="è¯·éæ©" clearable> |
| | | <el-option label="墿®ç¥¨" value="墿®ç¥¨" /> |
| | | <el-option label="å¢ä¸ç¥¨" value="å¢ä¸ç¥¨" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagementFormDia"> |
| | | import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue' |
| | | import { ArrowDown, ArrowUp, Delete, Plus, Document } from '@element-plus/icons-vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { getToken } from '@/utils/auth' |
| | | import PIMTable from '@/components/PIMTable/PIMTable.vue' |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue' |
| | | import { listPlan } from '@/api/projectManagement/projectType' |
| | | import { findRoleListPage } from '@/api/projectManagement/role' |
| | | import { userListAll } from '@/api/publicApi' |
| | | import { addProject, getProject, updateProject } from '@/api/projectManagement/project' |
| | | import { modelList, productTreeList } from '@/api/basicData/product' |
| | | import { delProduct as delSalesProduct } from '@/api/salesManagement/salesLedger' |
| | | |
| | | const emit = defineEmits(['completed']) |
| | | const { proxy } = getCurrentInstance() |
| | | const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status') |
| | | |
| | | const dialogVisible = ref(false) |
| | | const operationType = ref('add') |
| | | const formRef = ref() |
| | | const fileList = ref([]) |
| | | const existingAttachments = ref([]) |
| | | const upload = reactive({ |
| | | url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload', |
| | | headers: { Authorization: 'Bearer ' + getToken() } |
| | | }) |
| | | |
| | | const projectTypeOptions = ref([]) |
| | | const roleOptions = ref([]) |
| | | const userOptions = ref([]) |
| | | const productData = ref([]) |
| | | const productSelectedRows = ref([]) |
| | | const productCategoryOptions = ref([]) |
| | | const modelOptions = ref([]) |
| | | const productFormVisible = ref(false) |
| | | const productOperationType = ref('add') |
| | | const productFormRef = ref() |
| | | const productIndex = ref(0) |
| | | const isCalculating = ref(false) |
| | | |
| | | const productFormData = reactive({ |
| | | productForm: { |
| | | productCategoryId: undefined, |
| | | productCategory: '', |
| | | productModelId: undefined, |
| | | specificationModel: '', |
| | | unit: '', |
| | | quantity: '', |
| | | taxInclusiveUnitPrice: '', |
| | | taxRate: '', |
| | | taxInclusiveTotalPrice: '', |
| | | taxExclusiveTotalPrice: '', |
| | | invoiceType: '' |
| | | }, |
| | | productRules: { |
| | | productCategoryId: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], |
| | | productModelId: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], |
| | | unit: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | quantity: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | taxInclusiveUnitPrice: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | taxRate: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }], |
| | | taxInclusiveTotalPrice: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | taxExclusiveTotalPrice: [{ required: true, message: '请è¾å
¥', trigger: 'blur' }], |
| | | invoiceType: [{ required: true, message: 'è¯·éæ©', trigger: 'change' }] |
| | | } |
| | | }) |
| | | |
| | | const { productForm, productRules } = toRefs(productFormData) |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | id: undefined, |
| | | clientId: undefined, |
| | | parentProjectId: undefined, |
| | | projectManagementPlanId: undefined, |
| | | managerId: undefined, |
| | | salesmanId: undefined, |
| | | salesmanName: '', |
| | | actualStartDate: '', |
| | | actualEndDate: '', |
| | | departmentId: undefined, |
| | | departmentName: '', |
| | | orderDate: '', |
| | | billNo: '', |
| | | projectName: '', |
| | | customerName: '', |
| | | parentProjectName: '', |
| | | setupDate: '', |
| | | projectSource: '', |
| | | creatorName: '', |
| | | billStatus: '', |
| | | projectStage: '', |
| | | estimatedDays: 0, |
| | | planStartDate: '', |
| | | planEndDate: '', |
| | | projectManagementPlanId: undefined, |
| | | projectAmount: 0, |
| | | auditStatus: '', |
| | | remark: '', |
| | | attachmentIds: [], |
| | | teamList: [], |
| | | phaseList: [], |
| | | addressList: [], |
| | | contactName: '', |
| | | contactGender: '', |
| | | contactBirthday: '', |
| | | contactEmail: '', |
| | | contactDept: '', |
| | | contactJob: '', |
| | | contactMobile: '', |
| | | contactWechat: '', |
| | | contactQq: '', |
| | | contactWorkWechat: '', |
| | | contactAddress: '', |
| | | contactRemark: '' |
| | | }, |
| | | rules: { |
| | | projectName: [{ required: true, message: '请è¾å
¥é¡¹ç®åç§°', trigger: 'blur' }] |
| | | } |
| | | }) |
| | | |
| | | const { form, rules } = toRefs(data) |
| | | |
| | | const sectionCollapsed = reactive({ |
| | | base: false, |
| | | product: false, |
| | | team: false, |
| | | phase: false, |
| | | address: false, |
| | | contact: false |
| | | }) |
| | | |
| | | const isView = computed(() => operationType.value === 'view') |
| | | const dialogTitle = computed(() => { |
| | | if (operationType.value === 'add') return 'æ°å¢é¡¹ç®' |
| | | if (operationType.value === 'edit') return 'ç¼è¾é¡¹ç®' |
| | | return '项ç®è¯¦æ
' |
| | | }) |
| | | |
| | | const teamColumns = [ |
| | | { label: 'å§å', prop: 'memberId', align: 'center', width: 180, dataType: 'slot', slot: 'memberId' }, |
| | | { label: '项ç®ç»è§è²', prop: 'roleId', align: 'center', width: 160, dataType: 'slot', slot: 'roleId' }, |
| | | { label: 'è¿å
¥æ¥æ', prop: 'enterDate', align: 'center', width: 160, dataType: 'slot', slot: 'enterDate' }, |
| | | { label: 'ç¦»å¼æ¥æ', prop: 'leaveDate', align: 'center', width: 160, dataType: 'slot', slot: 'leaveDate' }, |
| | | { label: 'èç³»æ¹å¼', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'phone' }, |
| | | { label: '夿³¨', prop: 'remark', align: 'center', dataType: 'slot', slot: 'teamRemark' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'teamAction', fixed: 'right' } |
| | | ] |
| | | |
| | | const phaseColumns = [ |
| | | { label: 'é¶æ®µåç§°', prop: 'phaseName', align: 'center', width: 160, dataType: 'slot', slot: 'phaseName' }, |
| | | { label: 'æè¿°', prop: 'description', align: 'center', width: 200, dataType: 'slot', slot: 'phaseDesc' }, |
| | | { label: 'è´è´£äºº', prop: 'ownerId', align: 'center', width: 160, dataType: 'slot', slot: 'ownerId' }, |
| | | { label: 'é¢è®¡å·¥æ(天)', prop: 'planDays', align: 'center', width: 140, dataType: 'slot', slot: 'planDays' }, |
| | | { label: '计åå¼å§æ¥æ', prop: 'planStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'planStart' }, |
| | | { label: '计åç»ææ¥æ', prop: 'planEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'planEnd' }, |
| | | { label: 'è¿åº¦(%)', prop: 'progress', align: 'center', width: 120, dataType: 'slot', slot: 'progress' }, |
| | | { label: 'å®é
å¼å§æ¥æ', prop: 'actualStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualStart' }, |
| | | { label: 'å®é
ç»ææ¥æ', prop: 'actualEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualEnd' }, |
| | | { label: '龿天æ°', prop: 'overdueDays', align: 'center', width: 120, dataType: 'slot', slot: 'overdueDays' }, |
| | | { label: '宿æ
åµ', prop: 'completionRemark', align: 'center', width: 200, dataType: 'slot', slot: 'completion' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'phaseAction', fixed: 'right' } |
| | | ] |
| | | |
| | | const addressColumns = [ |
| | | { label: 'æ¶è´§äºº', prop: 'receiver', align: 'center', width: 180, dataType: 'slot', slot: 'receiver' }, |
| | | { label: 'èç³»æ¹å¼', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'receiverPhone' }, |
| | | { label: 'æ¶è´§å°å', prop: 'address', align: 'center', dataType: 'slot', slot: 'receiverAddress' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'addressAction', fixed: 'right' } |
| | | ] |
| | | |
| | | function toggleSection(key) { |
| | | sectionCollapsed[key] = !sectionCollapsed[key] |
| | | } |
| | | |
| | | function resetFormData() { |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | | clientId: undefined, |
| | | parentProjectId: undefined, |
| | | projectManagementPlanId: undefined, |
| | | managerId: undefined, |
| | | salesmanId: undefined, |
| | | salesmanName: '', |
| | | actualStartDate: '', |
| | | actualEndDate: '', |
| | | departmentId: undefined, |
| | | departmentName: '', |
| | | orderDate: '', |
| | | billNo: '', |
| | | projectName: '', |
| | | customerName: '', |
| | | parentProjectName: '', |
| | | setupDate: '', |
| | | projectSource: '', |
| | | creatorName: '', |
| | | billStatus: '', |
| | | projectStage: '', |
| | | estimatedDays: 0, |
| | | planStartDate: '', |
| | | planEndDate: '', |
| | | projectManagementPlanId: undefined, |
| | | projectAmount: 0, |
| | | auditStatus: '', |
| | | remark: '', |
| | | attachmentIds: [], |
| | | teamList: [], |
| | | phaseList: [], |
| | | addressList: [], |
| | | contactName: '', |
| | | contactGender: '', |
| | | contactBirthday: '', |
| | | contactEmail: '', |
| | | contactDept: '', |
| | | contactJob: '', |
| | | contactMobile: '', |
| | | contactWechat: '', |
| | | contactQq: '', |
| | | contactWorkWechat: '', |
| | | contactAddress: '', |
| | | contactRemark: '' |
| | | }) |
| | | fileList.value = [] |
| | | productData.value = [] |
| | | } |
| | | |
| | | function formattedNumber(row, column, cellValue) { |
| | | const val = Number(cellValue ?? 0) |
| | | return Number.isFinite(val) ? val.toFixed(2) : '0.00' |
| | | } |
| | | |
| | | function summarizeProductTable(param) { |
| | | return proxy.summarizeTable(param, ['taxInclusiveTotalPrice', 'taxExclusiveTotalPrice']) |
| | | } |
| | | |
| | | function productSelected(selection) { |
| | | productSelectedRows.value = selection |
| | | } |
| | | |
| | | function convertIdToValue(data) { |
| | | return (Array.isArray(data) ? data : []).map(item => { |
| | | const { id, children, ...rest } = item |
| | | const newItem = { |
| | | ...rest, |
| | | value: id |
| | | } |
| | | if (children && children.length > 0) { |
| | | newItem.children = convertIdToValue(children) |
| | | } |
| | | return newItem |
| | | }) |
| | | } |
| | | |
| | | function findNodeById(nodes, productId) { |
| | | for (let i = 0; i < (nodes || []).length; i++) { |
| | | if (nodes[i].value === productId) { |
| | | return nodes[i].label |
| | | } |
| | | if (nodes[i].children && nodes[i].children.length > 0) { |
| | | const foundNode = findNodeById(nodes[i].children, productId) |
| | | if (foundNode) return foundNode |
| | | } |
| | | } |
| | | return null |
| | | } |
| | | |
| | | function findNodeIdByLabel(nodes, label) { |
| | | if (!label) return null |
| | | for (let i = 0; i < (nodes || []).length; i++) { |
| | | const node = nodes[i] |
| | | if (node.label === label) return node.value |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeIdByLabel(node.children, label) |
| | | if (found !== null && found !== undefined) return found |
| | | } |
| | | } |
| | | return null |
| | | } |
| | | |
| | | function getProductOptions() { |
| | | return productTreeList().then(res => { |
| | | const list = res?.data || res |
| | | productCategoryOptions.value = convertIdToValue(list) |
| | | return productCategoryOptions.value |
| | | }) |
| | | } |
| | | |
| | | function getModels(value) { |
| | | const categoryLabel = findNodeById(productCategoryOptions.value, value) |
| | | productForm.value.productCategory = categoryLabel || '' |
| | | modelList({ id: value }).then(res => { |
| | | modelOptions.value = res?.data || res || [] |
| | | }) |
| | | } |
| | | |
| | | function getProductModel(value) { |
| | | const index = (modelOptions.value || []).findIndex(item => item.id === value) |
| | | if (index !== -1) { |
| | | productForm.value.specificationModel = modelOptions.value[index].model |
| | | productForm.value.unit = modelOptions.value[index].unit |
| | | } else { |
| | | productForm.value.specificationModel = '' |
| | | productForm.value.unit = '' |
| | | } |
| | | } |
| | | |
| | | async function openProductForm(type, row, index) { |
| | | productOperationType.value = type |
| | | productIndex.value = index || 0 |
| | | productForm.value = {} |
| | | proxy.resetForm('productFormRef') |
| | | |
| | | if (!productCategoryOptions.value || productCategoryOptions.value.length === 0) { |
| | | await getProductOptions() |
| | | } |
| | | |
| | | if (type === 'edit' && row) { |
| | | productForm.value = { ...row } |
| | | try { |
| | | const categoryId = findNodeIdByLabel(productCategoryOptions.value, productForm.value.productCategory) |
| | | if (categoryId) { |
| | | productForm.value.productCategoryId = categoryId |
| | | const models = await modelList({ id: categoryId }) |
| | | modelOptions.value = models?.data || models || [] |
| | | const currentModel = (modelOptions.value || []).find(m => m.model === productForm.value.specificationModel) |
| | | if (currentModel) { |
| | | productForm.value.productModelId = currentModel.id |
| | | } |
| | | } |
| | | } catch {} |
| | | } else { |
| | | productForm.value = { |
| | | productCategoryId: undefined, |
| | | productCategory: '', |
| | | productModelId: undefined, |
| | | specificationModel: '', |
| | | unit: '', |
| | | quantity: '', |
| | | taxInclusiveUnitPrice: '', |
| | | taxRate: '', |
| | | taxInclusiveTotalPrice: '', |
| | | taxExclusiveTotalPrice: '', |
| | | invoiceType: '' |
| | | } |
| | | } |
| | | |
| | | productFormVisible.value = true |
| | | } |
| | | |
| | | function closeProductDia() { |
| | | proxy.resetForm('productFormRef') |
| | | productFormVisible.value = false |
| | | } |
| | | |
| | | function submitProduct() { |
| | | productFormRef.value?.validate?.(valid => { |
| | | if (!valid) return |
| | | const payload = { ...productForm.value } |
| | | if (productOperationType.value === 'add') { |
| | | productData.value.push(payload) |
| | | } else { |
| | | productData.value[productIndex.value] = payload |
| | | } |
| | | closeProductDia() |
| | | }) |
| | | } |
| | | |
| | | function deleteProduct() { |
| | | if (!productSelectedRows.value || productSelectedRows.value.length === 0) { |
| | | proxy.$modal?.msgWarning?.('è¯·éæ©æ°æ®') |
| | | return |
| | | } |
| | | const selectedIds = productSelectedRows.value.map(r => r?.id).filter(Boolean) |
| | | if (operationType.value !== 'add' && selectedIds.length > 0) { |
| | | delSalesProduct(selectedIds) |
| | | .then(() => { |
| | | proxy.$modal?.msgSuccess?.('å 餿å') |
| | | productData.value = productData.value.filter(row => !selectedIds.includes(row?.id)) |
| | | productSelectedRows.value = [] |
| | | }) |
| | | .catch(() => {}) |
| | | return |
| | | } |
| | | |
| | | productData.value = productData.value.filter(row => !productSelectedRows.value.includes(row)) |
| | | productSelectedRows.value = [] |
| | | } |
| | | |
| | | function calculateFromTotalPrice() { |
| | | if (isCalculating.value) return |
| | | const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice) |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | if (!totalPrice || !quantity || quantity <= 0) return |
| | | isCalculating.value = true |
| | | productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2) |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(totalPrice, productForm.value.taxRate) |
| | | } |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromExclusiveTotalPrice() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const exclusiveTotalPrice = parseFloat(productForm.value.taxExclusiveTotalPrice) |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | const taxRate = parseFloat(productForm.value.taxRate) |
| | | if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) return |
| | | isCalculating.value = true |
| | | const taxRateDecimal = taxRate / 100 |
| | | const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal) |
| | | productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2) |
| | | productForm.value.taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2) |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromQuantity() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice) |
| | | if (!quantity || quantity <= 0 || !unitPrice) return |
| | | isCalculating.value = true |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2) |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice( |
| | | productForm.value.taxInclusiveTotalPrice, |
| | | productForm.value.taxRate |
| | | ) |
| | | } |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromUnitPrice() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const quantity = parseFloat(productForm.value.quantity) |
| | | const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice) |
| | | if (!quantity || quantity <= 0 || !unitPrice) return |
| | | isCalculating.value = true |
| | | productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2) |
| | | if (productForm.value.taxRate) { |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice( |
| | | productForm.value.taxInclusiveTotalPrice, |
| | | productForm.value.taxRate |
| | | ) |
| | | } |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | function calculateFromTaxRate() { |
| | | if (!productForm.value.taxRate) { |
| | | proxy.$modal?.msgWarning?.('请å
éæ©ç¨ç') |
| | | return |
| | | } |
| | | if (isCalculating.value) return |
| | | const inclusiveTotalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice) |
| | | const taxRate = parseFloat(productForm.value.taxRate) |
| | | if (!inclusiveTotalPrice || !taxRate) return |
| | | isCalculating.value = true |
| | | productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate) |
| | | isCalculating.value = false |
| | | } |
| | | |
| | | async function loadProjectTypeOptions() { |
| | | try { |
| | | const res = await listPlan({ current: 1, size: 999 }) |
| | | const records = res?.data?.records || res?.records || res?.rows || [] |
| | | projectTypeOptions.value = records.map(item => ({ label: item.name, value: item.id })) |
| | | } catch { |
| | | projectTypeOptions.value = [] |
| | | } |
| | | } |
| | | |
| | | async function loadRoleOptions() { |
| | | try { |
| | | const res = await findRoleListPage({ pageNum: 1, pageSize: 999 }) |
| | | const records = res?.data?.records || res?.rows || res?.records || [] |
| | | roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id })) |
| | | } catch { |
| | | roleOptions.value = [] |
| | | } |
| | | } |
| | | |
| | | async function loadUserOptions() { |
| | | try { |
| | | const res = await userListAll() |
| | | const list = res?.data || res?.rows || res || [] |
| | | userOptions.value = (Array.isArray(list) ? list : []).map(u => ({ |
| | | label: u.nickName || u.userName || u.username || u.name, |
| | | value: u.userId || u.id |
| | | })) |
| | | } catch { |
| | | userOptions.value = [] |
| | | } |
| | | } |
| | | |
| | | function addTeamRow() { |
| | | form.value.teamList.push({ |
| | | memberId: undefined, |
| | | roleId: undefined, |
| | | enterDate: '', |
| | | leaveDate: '', |
| | | phone: '', |
| | | remark: '' |
| | | }) |
| | | } |
| | | |
| | | function removeTeamRow(index) { |
| | | if (index > -1) form.value.teamList.splice(index, 1) |
| | | } |
| | | |
| | | function addPhaseRow() { |
| | | form.value.phaseList.push({ |
| | | phaseName: '', |
| | | description: '', |
| | | ownerId: undefined, |
| | | planDays: 0, |
| | | planStartDate: '', |
| | | planEndDate: '', |
| | | progress: 0, |
| | | actualStartDate: '', |
| | | actualEndDate: '', |
| | | overdueDays: 0, |
| | | completionRemark: '' |
| | | }) |
| | | } |
| | | |
| | | function removePhaseRow(index) { |
| | | if (index > -1) form.value.phaseList.splice(index, 1) |
| | | } |
| | | |
| | | function addAddressRow() { |
| | | form.value.addressList.push({ |
| | | receiver: '', |
| | | phone: '', |
| | | address: '' |
| | | }) |
| | | } |
| | | |
| | | function removeAddressRow(index) { |
| | | if (index > -1) form.value.addressList.splice(index, 1) |
| | | } |
| | | |
| | | function beforeUpload() { |
| | | if (isView.value) return false |
| | | proxy.$modal?.loading?.('æ£å¨ä¸ä¼ æä»¶ï¼è¯·ç¨å...') |
| | | return true |
| | | } |
| | | |
| | | function handleUploadError() { |
| | | proxy.$modal?.closeLoading?.() |
| | | ElMessage.error('ä¸ä¼ æä»¶å¤±è´¥') |
| | | } |
| | | |
| | | function handleUploadSuccess(res, file) { |
| | | console.log(res, file) |
| | | proxy.$modal?.closeLoading?.() |
| | | if (res?.code !== 200) { |
| | | ElMessage.error(res?.msg || 'ä¸ä¼ 失败') |
| | | return |
| | | } |
| | | const attachmentId = res?.data?.[0]?.id ?? "" |
| | | if (!attachmentId) return |
| | | form.value.attachmentIds.push(attachmentId) |
| | | console.log(form.value.attachmentIds) |
| | | ElMessage.success('ä¸ä¼ æå') |
| | | } |
| | | |
| | | function handleRemove(file) { |
| | | const attachmentId = file?.attachmentId |
| | | if (!attachmentId) return |
| | | form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId) |
| | | } |
| | | |
| | | async function openDialog(payload = {}) { |
| | | operationType.value = payload.operationType || 'add' |
| | | resetFormData() |
| | | await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), getProductOptions()]) |
| | | if (payload.row?.id) { |
| | | try { |
| | | const res = await getProject(payload.row.id) |
| | | const detail = res?.data?.data ?? res?.data ?? res |
| | | const info = detail?.info || {} |
| | | const shippingAddress = detail?.shippingAddress || {} |
| | | const contractInfo = detail?.contractInfo || {} |
| | | |
| | | const normalizeId = v => { |
| | | if (v === undefined || v === null || v === '') return undefined |
| | | const n = Number(v) |
| | | return Number.isNaN(n) ? v : n |
| | | } |
| | | |
| | | const normalizeDictValue = v => { |
| | | if (v === undefined || v === null || v === '') return '' |
| | | return String(v) |
| | | } |
| | | |
| | | const computeEstimatedDays = (start, end) => { |
| | | if (!start || !end) return 0 |
| | | const startTime = new Date(`${start}T00:00:00`).getTime() |
| | | const endTime = new Date(`${end}T00:00:00`).getTime() |
| | | if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) return 0 |
| | | if (endTime < startTime) return 0 |
| | | return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1 |
| | | } |
| | | |
| | | Object.assign(form.value, { |
| | | id: info.id, |
| | | billNo: info.no ?? '', |
| | | projectManagementPlanId: info.projectManagementPlanId ?? '', |
| | | estimatedDays: Number(info.estimatedDays) || computeEstimatedDays(info.planStartTime, info.planEndTime) || 0, |
| | | projectName: info.title ?? '', |
| | | customerName: info.clientName ?? '', |
| | | parentProjectName: info.projectManagementInfoParentName ?? '', |
| | | setupDate: info.establishTime ?? '', |
| | | projectSource: info.source ?? '', |
| | | creatorName: info.managerName ?? '', |
| | | billStatus: normalizeDictValue(info.status), |
| | | projectStage: normalizeDictValue(info.stage ?? info.projectStage), |
| | | planStartDate: info.planStartTime ?? '', |
| | | planEndDate: info.planEndTime ?? '', |
| | | projectAmount: info.orderAmount ?? 0, |
| | | auditStatus: normalizeDictValue(info.reviewStatus), |
| | | remark: info.remark ?? '', |
| | | attachmentIds: Array.isArray(info.attachmentIds) ? info.attachmentIds : [], |
| | | teamList: Array.isArray(info.teamList) ? info.teamList.map(t => ({ |
| | | memberId: normalizeId(t.userId), |
| | | roleId: normalizeId(t.userRoleId), |
| | | enterDate: t.joinTime, |
| | | leaveDate: t.departTime, |
| | | phone: t.contact, |
| | | remark: t.remark |
| | | })) : [], |
| | | addressList: shippingAddress?.address |
| | | ? [{ |
| | | receiver: shippingAddress.consignee, |
| | | phone: shippingAddress.contract, |
| | | address: shippingAddress.address |
| | | }] |
| | | : [], |
| | | contactName: contractInfo.name ?? '', |
| | | contactGender: contractInfo.sex === 'ç·' ? '1' : contractInfo.sex === '女' ? '2' : '', |
| | | contactBirthday: contractInfo.birthday ?? '', |
| | | contactDept: contractInfo.department ?? '', |
| | | contactJob: contractInfo.job ?? '', |
| | | contactMobile: contractInfo.phoneNumber ?? '', |
| | | contactEmail: contractInfo.email ?? '', |
| | | contactQq: contractInfo.qq ?? '', |
| | | contactWechat: contractInfo.wx ?? '', |
| | | contactWorkWechat: contractInfo.lineaFissa ?? '', |
| | | contactAddress: contractInfo.origineEtnica ?? '', |
| | | contactRemark: contractInfo.rappresentanteLegale ?? '' |
| | | }) |
| | | |
| | | existingAttachments.value = Array.isArray(info.attachmentList) |
| | | ? info.attachmentList.map(a => ({ |
| | | id: a.id ?? a.fileId, |
| | | name: a.fileName ?? a.name, |
| | | url: a.url ?? a.fileUrl ?? a.path |
| | | })) |
| | | : [] |
| | | |
| | | const rawPhaseList = |
| | | detail?.phaseList || |
| | | detail?.projectPhaseList || |
| | | detail?.projectStageList || |
| | | info?.phaseList || |
| | | info?.projectPhaseList || |
| | | [] |
| | | form.value.phaseList = Array.isArray(rawPhaseList) |
| | | ? rawPhaseList.map(p => ({ |
| | | phaseName: p.phaseName ?? p.name ?? p.title ?? '', |
| | | description: p.description ?? p.workContent ?? p.desc ?? '', |
| | | ownerId: normalizeId(p.ownerId ?? p.leaderId ?? p.userId), |
| | | planDays: Number(p.planDays ?? p.estimatedDuration ?? p.estimatedDays) || 0, |
| | | planStartDate: p.planStartDate ?? p.planStartTime ?? p.startDate ?? '', |
| | | planEndDate: p.planEndDate ?? p.planEndTime ?? p.endDate ?? '', |
| | | progress: Number(p.progress ?? p.schedule) || 0, |
| | | actualStartDate: p.actualStartDate ?? p.actualStartTime ?? '', |
| | | actualEndDate: p.actualEndDate ?? p.actualEndTime ?? '', |
| | | overdueDays: Number(p.overdueDays ?? p.overDays) || 0, |
| | | completionRemark: p.completionRemark ?? p.remark ?? '' |
| | | })) |
| | | : [] |
| | | |
| | | productData.value = detail?.salesLedgerProductList || detail?.productData || [] |
| | | } catch {} |
| | | } |
| | | if (form.value.teamList.length === 0 && !isView.value) addTeamRow() |
| | | if (form.value.phaseList.length === 0 && !isView.value) addPhaseRow() |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | function downloadAttachment(att) { |
| | | if (att?.name) { |
| | | try { |
| | | proxy.$download.name(att.url); |
| | | return |
| | | } catch (e) {} |
| | | } |
| | | ElMessage.warning('éä»¶ææ ä¸è½½å°å') |
| | | } |
| | | function closeDialog() { |
| | | dialogVisible.value = false |
| | | } |
| | | |
| | | async function submitForm() { |
| | | if (isView.value) { |
| | | closeDialog() |
| | | return |
| | | } |
| | | await formRef.value?.validate?.() |
| | | if (!productData.value || productData.value.length === 0) { |
| | | proxy.$modal?.msgWarning?.('请添å 产åä¿¡æ¯') |
| | | return |
| | | } |
| | | const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label |
| | | const teamList = (form.value.teamList || []).map(t => ({ |
| | | userId: t.memberId, |
| | | userName: findLabel(userOptions.value, t.memberId), |
| | | userRoleId: t.roleId, |
| | | userRoleName: findLabel(roleOptions.value, t.roleId), |
| | | joinTime: t.enterDate, |
| | | departTime: t.leaveDate, |
| | | contact: t.phone, |
| | | remark: t.remark |
| | | })) |
| | | |
| | | const shippingRow = (form.value.addressList || [])[0] || {} |
| | | const shippingAddress = { |
| | | id: undefined, |
| | | consignee: shippingRow.receiver, |
| | | contract: shippingRow.phone, |
| | | address: shippingRow.address |
| | | } |
| | | |
| | | const contractInfo = { |
| | | id: undefined, |
| | | name: form.value.contactName, |
| | | sex: form.value.contactGender === '1' ? 'ç·' : form.value.contactGender === '2' ? '女' : '', |
| | | birthday: form.value.contactBirthday, |
| | | department: form.value.contactDept, |
| | | job: form.value.contactJob, |
| | | phoneNumber: form.value.contactMobile, |
| | | email: form.value.contactEmail, |
| | | qq: form.value.contactQq, |
| | | lineaFissa: form.value.contactWorkWechat, |
| | | wx: form.value.contactWechat, |
| | | origineEtnica: form.value.contactAddress, |
| | | rappresentanteLegale: form.value.contactRemark |
| | | } |
| | | const info = { |
| | | id: form.value.id ?? null, |
| | | no: form.value.billNo, |
| | | title: form.value.projectName, |
| | | clientId: form.value.clientId ?? null, |
| | | clientName: form.value.customerName, |
| | | projectManagementInfoParentId: form.value.parentProjectId ?? null, |
| | | projectManagementPlanId: form.value.projectManagementPlanId ?? null, |
| | | establishTime: form.value.setupDate, |
| | | source: form.value.projectSource, |
| | | managerId: form.value.managerId ?? null, |
| | | managerName: form.value.creatorName, |
| | | salesmanId: form.value.salesmanId ?? null, |
| | | salesmanName: form.value.salesmanName ?? '', |
| | | planStartTime: form.value.planStartDate, |
| | | planEndTime: form.value.planEndDate, |
| | | actualStartTime: form.value.actualStartDate, |
| | | actualEndTime: form.value.actualEndDate, |
| | | status: form.value.billStatus === '' || form.value.billStatus === undefined || form.value.billStatus === null ? null : Number(form.value.billStatus), |
| | | departmentId: form.value.departmentId ?? null, |
| | | departmentName: form.value.departmentName ?? '', |
| | | orderDate: form.value.orderDate, |
| | | orderAmount: form.value.projectAmount, |
| | | reviewStatus: form.value.auditStatus === '' || form.value.auditStatus === undefined || form.value.auditStatus === null ? null : Number(form.value.auditStatus), |
| | | stage: form.value.projectStage === '' || form.value.projectStage === undefined || form.value.projectStage === null ? null : Number(form.value.projectStage), |
| | | remark: form.value.remark, |
| | | attachmentIds: Array.isArray(form.value.attachmentIds) ? form.value.attachmentIds : [], |
| | | teamList |
| | | } |
| | | |
| | | const payload = { |
| | | info, |
| | | shippingAddress, |
| | | contractInfo, |
| | | salesLedgerProductList: productData.value |
| | | } |
| | | |
| | | const req = operationType.value === 'edit' ? updateProject : addProject |
| | | const res = await req(payload) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('ä¿åæå') |
| | | closeDialog() |
| | | emit('completed') |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'ä¿å失败') |
| | | } |
| | | |
| | | defineExpose({ openDialog }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .section { |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 8px; |
| | | margin-bottom: 14px; |
| | | background: #fff; |
| | | } |
| | | |
| | | .section-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 14px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .section-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .section-bar { |
| | | width: 3px; |
| | | height: 14px; |
| | | background: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .section-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .toggle-icon { |
| | | color: #909399; |
| | | } |
| | | |
| | | .section-body { |
| | | padding: 0 14px 14px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 12px; |
| | | } |
| | | .attachment-upload{ |
| | | |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <SearchPanel |
| | | v-model="queryParams" |
| | | :schema="searchSchema" |
| | | @search="handleQuery" |
| | | @reset="resetQuery" |
| | | > |
| | | <template #billStatus="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©åæ®ç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in bill_status" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #auditStatus="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©è®¡åç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in project_management" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | <template #projectStage="{ item }"> |
| | | <el-select v-model="queryParams[item.prop]" placeholder="è¯·éæ©å®¡æ ¸ç¶æ" clearable style="width: 100%"> |
| | | <el-option v-for="dict in plan_status" :key="dict.value" :label="dict.label" :value="dict.value" /> |
| | | </el-select> |
| | | </template> |
| | | </SearchPanel> |
| | | |
| | | <div class="table-container"> |
| | | <div class="table-actions"> |
| | | <el-button style="background-color: #002FA7; color: #fff" @click="handleAdd">æ°å¢</el-button> |
| | | <!-- <el-dropdown split-button type="default" @command="handleGenerateBill" style="margin-left: 10px;"> |
| | | çæåæ® |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="1">çæåæ®1</el-dropdown-item> |
| | | <el-dropdown-item command="2">çæåæ®2</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> --> |
| | | <el-button :loading="submitLoading" @click="handleSubmit">æäº¤</el-button> |
| | | <el-button :loading="auditLoading" @click="handleAudit">å®¡æ ¸</el-button> |
| | | <el-button :loading="reverseAuditLoading" @click="handleReverseAudit">åå®¡æ ¸</el-button> |
| | | <el-button :loading="deleteLoading" @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | | |
| | | <PIMTable |
| | | :column="columns" |
| | | :tableData="tableData" |
| | | :page="pagination" |
| | | :tableLoading="loading" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="handlePagination" |
| | | > |
| | | <template #auditStatus="{ row }"> |
| | | <dict-tag :options="project_management" :value="row.auditStatus" /> |
| | | </template> |
| | | <template #projectStage="{ row }"> |
| | | <dict-tag :options="plan_status" :value="row.projectStage" /> |
| | | </template> |
| | | <template #action="{ row }"> |
| | | <el-button link type="primary" @click="handleEdit(row)">ç¼è¾</el-button> |
| | | <el-button link type="primary" :loading="progressBtnLoadingId===row.id" @click="handleProgressReport(row)">è¿åº¦æ±æ¥</el-button> |
| | | <el-button link type="primary" @click="handleDetail(row)">详æ
</el-button> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | |
| | | <FormDia ref="formDiaRef" @completed="getList" /> |
| | | <ProgressReportDialog |
| | | v-model="progressReportVisible" |
| | | :project-id="progressProjectId" |
| | | :project-info="progressProjectInfo" |
| | | :plan-nodes="progressPlanNodes" |
| | | :default-plan-node-id="progressDefaultPlanNodeId" |
| | | @submitted="handleProgressSubmitted" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagement"> |
| | | import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import SearchPanel from '@/components/SearchPanel/index.vue' |
| | | import PIMTable from '@/components/PIMTable/PIMTable.vue' |
| | | import FormDia from './components/formDia.vue' |
| | | import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue' |
| | | import { |
| | | listProject, |
| | | delProject, |
| | | submitProject, |
| | | auditProject, |
| | | reverseAuditProject, |
| | | getProject, |
| | | saveStage |
| | | } from '@/api/projectManagement/project' |
| | | import { listPlan } from '@/api/projectManagement/projectType' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import useUserStore from '@/store/modules/user' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status') |
| | | const router = useRouter() |
| | | const userStore = useUserStore() |
| | | |
| | | const loading = ref(false) |
| | | const ids = ref([]) |
| | | const tableData = ref([]) |
| | | const formDiaRef = ref() |
| | | const progressReportVisible = ref(false) |
| | | const progressProjectId = ref(undefined) |
| | | const progressProjectInfo = ref({}) |
| | | const progressPlanNodes = ref([]) |
| | | const progressDefaultPlanNodeId = ref(undefined) |
| | | const progressBtnLoadingId = ref(null) |
| | | const submitLoading = ref(false) |
| | | const auditLoading = ref(false) |
| | | const reverseAuditLoading = ref(false) |
| | | const deleteLoading = ref(false) |
| | | |
| | | const data = reactive({ |
| | | queryParams: { |
| | | projectNameOrCode: undefined, |
| | | customerName: undefined, |
| | | billStatus: undefined, |
| | | projectStage: undefined, |
| | | auditStatus: undefined, |
| | | salesperson: undefined, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | }, |
| | | pagination: { |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | layout: 'total, sizes, prev, pager, next, jumper' |
| | | } |
| | | }) |
| | | |
| | | const { queryParams, pagination } = toRefs(data) |
| | | |
| | | const searchSchema = [ |
| | | { prop: 'projectNameOrCode', label: '项ç®åç§°/ç¼å·', type: 'input', placeholder: '请è¾å
¥é¡¹ç®åç§°/ç¼å·' }, |
| | | { prop: 'customerName', label: '客æ·åç§°', type: 'input', placeholder: '请è¾å
¥å®¢æ·åç§°' }, |
| | | { prop: 'billStatus', label: 'åæ®ç¶æ', slot: 'billStatus' }, |
| | | { prop: 'projectStage', label: '计åç¶æ', slot: 'projectStage' }, |
| | | { prop: 'auditStatus', label: 'å®¡æ ¸ç¶æ', slot: 'auditStatus' }, |
| | | { prop: 'salesperson', label: 'ä¸å¡äººå', type: 'input', placeholder: '请è¾å
¥ä¸å¡äººå' } |
| | | ] |
| | | |
| | | const columns = [ |
| | | { label: 'åæ®ç¼å·', prop: 'billNo', align: 'center', width: '150' }, |
| | | { label: '项ç®åç§°', prop: 'projectName', align: 'center' }, |
| | | { label: 'å®¡æ ¸ç¶æ', prop: 'auditStatus', align: 'center', dataType: 'slot', slot: 'auditStatus' }, |
| | | { label: '客æ·åç§°', prop: 'customerName', align: 'center' }, |
| | | { label: 'ç«é¡¹æ¥æ', prop: 'setupDate', align: 'center', width: '120' }, |
| | | { label: 'é¡¹ç®æ¥æº', prop: 'projectSource', align: 'center' }, |
| | | { label: '项ç®åç±»', prop: 'projectClassification', align: 'center' }, |
| | | { label: 'æä½', prop: 'action', align: 'center', width: '250', dataType: 'slot', slot: 'action', fixed: 'right' } |
| | | ] |
| | | |
| | | function getList() { |
| | | loading.value = true |
| | | const params = { |
| | | noOrName: queryParams.value.projectNameOrCode, |
| | | clientName: queryParams.value.customerName, |
| | | salesmanName: queryParams.value.salesperson, |
| | | reviewStatus: queryParams.value.auditStatus, |
| | | stage: queryParams.value.projectStage, |
| | | current: queryParams.value.pageNum, |
| | | size: queryParams.value.pageSize |
| | | } |
| | | listProject(params) |
| | | .then(response => { |
| | | const records = response?.data?.records || response?.rows || response?.records || [] |
| | | const billFilter = queryParams.value.billStatus |
| | | const filtered = billFilter === undefined || billFilter === null || billFilter === '' |
| | | ? records |
| | | : records.filter(r => String(r.billStatus ?? r.status) === String(billFilter)) |
| | | tableData.value = filtered.map(r => ({ |
| | | id: r.id, |
| | | billNo: r.no ?? r.billNo, |
| | | projectName: r.title ?? r.projectName, |
| | | billStatus: r.billStatus ?? r.status, |
| | | auditStatus: r.reviewStatus ?? r.auditStatus, |
| | | projectStage: r.stage ?? r.projectStage, |
| | | customerName: r.clientName ?? r.customerName, |
| | | parentProject: r.parentTitle ?? r.parentName ?? r.parentProject, |
| | | setupDate: r.establishTime ?? r.setupDate, |
| | | projectType: r.planName ?? r.projectType, |
| | | projectSource: r.source ?? r.projectSource, |
| | | projectClassification: r.departmentName ?? r.projectClassification, |
| | | raw: r |
| | | })) |
| | | pagination.value.total = response?.total || response?.data?.total || 0 |
| | | }) |
| | | .finally(() => { |
| | | loading.value = false |
| | | }) |
| | | } |
| | | |
| | | function handleQuery() { |
| | | queryParams.value.pageNum = 1 |
| | | pagination.value.current = 1 |
| | | getList() |
| | | } |
| | | |
| | | function resetQuery() { |
| | | queryParams.value = { |
| | | projectNameOrCode: undefined, |
| | | customerName: undefined, |
| | | billStatus: undefined, |
| | | projectStage: undefined, |
| | | auditStatus: undefined, |
| | | salesperson: undefined, |
| | | pageNum: 1, |
| | | pageSize: 10 |
| | | } |
| | | handleQuery() |
| | | } |
| | | |
| | | function handleSelectionChange(selection) { |
| | | ids.value = selection.map(item => item.id) |
| | | } |
| | | |
| | | function handlePagination({ page, limit }) { |
| | | queryParams.value.pageNum = page |
| | | queryParams.value.pageSize = limit |
| | | pagination.value.current = page |
| | | pagination.value.size = limit |
| | | getList() |
| | | } |
| | | |
| | | function handleAdd() { |
| | | formDiaRef.value?.openDialog({ operationType: 'add' }) |
| | | } |
| | | |
| | | async function handleDelete() { |
| | | const delIds = ids.value |
| | | if (delIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦å é¤çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å 餿鿰æ®é¡¹?', 'è¦å', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | deleteLoading.value = true |
| | | await delProject(delIds) |
| | | getList() |
| | | ElMessage.success('å 餿å') |
| | | } catch {} finally { |
| | | deleteLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | | const submitIds = ids.value |
| | | if (submitIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦æäº¤çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤æäº¤æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | submitLoading.value = true |
| | | await Promise.all(submitIds.map(id => submitProject({ id }))) |
| | | getList() |
| | | ElMessage.success('æäº¤æå') |
| | | } catch {} finally { |
| | | submitLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleAudit() { |
| | | const auditIds = ids.value |
| | | if (auditIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦å®¡æ ¸çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | auditLoading.value = true |
| | | await Promise.all(auditIds.map(id => auditProject({ id }))) |
| | | getList() |
| | | ElMessage.success('å®¡æ ¸æå') |
| | | } catch {} finally { |
| | | auditLoading.value = false |
| | | } |
| | | } |
| | | |
| | | async function handleReverseAudit() { |
| | | const reverseAuditIds = ids.value |
| | | if (reverseAuditIds.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦åå®¡æ ¸çæ°æ®é¡¹') |
| | | return |
| | | } |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤åå®¡æ ¸æéæ°æ®é¡¹?', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | reverseAuditLoading.value = true |
| | | await Promise.all(reverseAuditIds.map(id => reverseAuditProject({ id }))) |
| | | getList() |
| | | ElMessage.success('åå®¡æ ¸æå') |
| | | } catch {} finally { |
| | | reverseAuditLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleGenerateBill(command) { |
| | | ElMessage.info(`çæåæ®: ${command}`) |
| | | } |
| | | |
| | | function computeDefaultPlanNodeId(stageVal, nodes) { |
| | | const list = Array.isArray(nodes) ? nodes : [] |
| | | if (list.length === 0) return undefined |
| | | const direct = list.find(n => String(n.id) === String(stageVal)) |
| | | if (direct?.id) return direct.id |
| | | const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) |
| | | const idx = Number(stageVal) |
| | | if (Number.isFinite(idx)) { |
| | | const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0] |
| | | if (byIndex?.id) return byIndex.id |
| | | } |
| | | return sorted[0]?.id |
| | | } |
| | | |
| | | async function handleProgressReport(row) { |
| | | if (!row?.id) return |
| | | try { |
| | | progressBtnLoadingId.value = row.id |
| | | const res = await getProject(row.id) |
| | | const detail = res?.data?.data ?? res?.data ?? res |
| | | const info = detail?.info || {} |
| | | progressProjectId.value = info.id |
| | | progressProjectInfo.value = info |
| | | |
| | | const planId = info.projectManagementPlanId |
| | | if (planId) { |
| | | const planRes = await listPlan({ current: 1, size: 999 }) |
| | | const records = planRes?.data?.records || planRes?.records || planRes?.rows || [] |
| | | const plan = (records || []).find(p => String(p.id) === String(planId)) || {} |
| | | progressPlanNodes.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : [] |
| | | } else { |
| | | progressPlanNodes.value = [] |
| | | } |
| | | |
| | | progressDefaultPlanNodeId.value = computeDefaultPlanNodeId(info.stage, progressPlanNodes.value) |
| | | progressReportVisible.value = true |
| | | } catch (e) { |
| | | ElMessage.error('è·å项ç®è¯¦æ
失败') |
| | | } finally { |
| | | progressBtnLoadingId.value = null |
| | | } |
| | | } |
| | | |
| | | async function handleProgressSubmitted(payload) { |
| | | try { |
| | | const nodes = Array.isArray(progressPlanNodes.value) ? progressPlanNodes.value : [] |
| | | const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {} |
| | | const description = payload.remark |
| | | ? `${payload.reportDate || ''} ${payload.remark}`.trim() |
| | | : `${payload.reportDate || ''} è¿åº¦æ±æ¥`.trim() |
| | | const req = { |
| | | id: null, |
| | | projectManagementPlanNodeId: payload.planNodeId, |
| | | projectManagementInfoId: progressProjectId.value, |
| | | description, |
| | | actualLeaderId: userStore.id || progressProjectInfo.value?.managerId, |
| | | actualLeaderName: userStore.nickName || progressProjectInfo.value?.managerName, |
| | | estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, |
| | | planStartTime: payload.planStartTime || progressProjectInfo.value?.planStartTime, |
| | | planEndTime: payload.planEndTime || progressProjectInfo.value?.planEndTime, |
| | | actualStartTime: payload.actualStartTime || null, |
| | | actualEndTime: payload.actualEndTime || null, |
| | | progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0, |
| | | attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : [] |
| | | } |
| | | const res = await saveStage(req) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('æäº¤æå') |
| | | getList() |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'æäº¤å¤±è´¥') |
| | | } catch (e) { |
| | | ElMessage.error('æäº¤å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | function handleDetail(row) { |
| | | if (!row?.id) return |
| | | router.push(`/projectManagement/Management/detail/${row.id}`) |
| | | } |
| | | |
| | | function handleEdit(row) { |
| | | formDiaRef.value?.openDialog({ operationType: 'edit', row }) |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getList() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | .table-container { |
| | | background-color: #fff; |
| | | padding: 20px; |
| | | border-radius: 4px; |
| | | } |
| | | .table-actions { |
| | | margin-bottom: 15px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-card class="header-card" shadow="never"> |
| | | <div class="header-row"> |
| | | <div class="header-title">项ç®è¯¦æ
</div> |
| | | <div class="header-actions"> |
| | | <el-button style="color: white;background: #002FA7;" @click="openProgressReport">è¿åº¦æ±æ¥</el-button> |
| | | <!-- <el-button type="danger" @click="openDiscussProgress">æ´½è°è¿åº¦</el-button> --> |
| | | <el-button @click="goBack">è¿å</el-button> |
| | | </div> |
| | | </div> |
| | | <el-steps v-if="steps.length > 0" :active="activeStep" align-center finish-status="success"> |
| | | <el-step v-for="(s, idx) in steps" :key="idx" :title="s" /> |
| | | </el-steps> |
| | | </el-card> |
| | | |
| | | <el-card class="content-card" shadow="never" v-loading="loading"> |
| | | <el-tabs v-model="activeTab"> |
| | | <el-tab-pane label="åºç¡èµæ" name="base"> |
| | | <el-descriptions :column="4" border> |
| | | <el-descriptions-item label="项ç®ID">{{ info.id ?? '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åæ®ç¼å·">{{ info.no || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®åç§°">{{ info.title || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·åç§°">{{ info.clientName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="客æ·ID">{{ info.clientId ?? '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¶é¡¹ç®">{{ parentProjectLabel }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®ç±»å">{{ projectTypeLabel }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç«é¡¹æ¥æ">{{ info.establishTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¡¹ç®æ¥æº">{{ info.source || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç«é¡¹äºº">{{ info.managerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¸å¡å">{{ info.salesmanName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¨é¨">{{ info.departmentName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åæ®ç¶æ"> |
| | | <dict-tag :options="bill_status" :value="String(info.status ?? '')" /> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å®¡æ ¸ç¶æ"> |
| | | <dict-tag :options="project_management" :value="String(info.reviewStatus ?? '')" /> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="计åç¶æ"> |
| | | <dict-tag :options="plan_status" :value="String(info.stage ?? '')" /> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="é¢è®¡å·¥æ(天)">{{ estimatedDays }}</el-descriptions-item> |
| | | <el-descriptions-item label="计åå¼å§æ¥æ">{{ info.planStartTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="计åå®ææ¥æ">{{ info.planEndTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®é
å¼å§æ¥æ">{{ info.actualStartTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®é
å®ææ¥æ">{{ info.actualEndTime || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="è®¢åæ¥æ">{{ info.orderDate || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®éé¢">{{ info.orderAmount ?? '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="夿³¨" :span="4">{{ info.remark || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="attachment-block" v-if="attachments.length > 0"> |
| | | <div class="attachment-title">éä»¶</div> |
| | | <div class="attachment-list"> |
| | | <div v-for="(att, idx) in attachments" :key="att.id || att.url || idx" class="attachment-item"> |
| | | <el-icon><Document /></el-icon> |
| | | <span class="attachment-name">{{ att.name || att.fileName || att.url || 'éä»¶' }}</span> |
| | | <el-button link type="primary" size="small" @click="downloadAttachment(att)">ä¸è½½</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-divider content-position="left">产åä¿¡æ¯</el-divider> |
| | | <el-table :data="productRows" border show-summary :summary-method="summarizeProductTable"> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="产å大类" prop="productCategory" show-overflow-tooltip /> |
| | | <el-table-column label="è§æ ¼åå·" prop="specificationModel" show-overflow-tooltip /> |
| | | <el-table-column label="åä½" prop="unit" width="90" /> |
| | | <el-table-column label="æ°é" prop="quantity" width="90" /> |
| | | <el-table-column label="ç¨ç(%)" prop="taxRate" width="90" /> |
| | | <el-table-column label="å«ç¨åä»·(å
)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="å«ç¨æ»ä»·(å
)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="ä¸å«ç¨æ»ä»·(å
)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" /> |
| | | <el-table-column label="å票类å" prop="invoiceType" width="110" /> |
| | | </el-table> |
| | | |
| | | <el-divider content-position="left">项ç®å¢é</el-divider> |
| | | <el-table :data="teamRows" border> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="å§å" prop="userName" show-overflow-tooltip /> |
| | | <el-table-column label="项ç®ç»è§è²" prop="userRoleName" show-overflow-tooltip /> |
| | | <el-table-column label="è¿å
¥æ¥æ" prop="joinTime" width="140" /> |
| | | <el-table-column label="ç¦»å¼æ¥æ" prop="departTime" width="140" /> |
| | | <el-table-column label="èç³»æ¹å¼" prop="contact" show-overflow-tooltip /> |
| | | <el-table-column label="夿³¨" prop="remark" show-overflow-tooltip /> |
| | | </el-table> |
| | | |
| | | <el-divider content-position="left">æ¶è´§å°å</el-divider> |
| | | <el-descriptions :column="3" border> |
| | | <el-descriptions-item label="æ¶è´§äºº">{{ shippingAddress.consignee || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="èç³»æ¹å¼">{{ shippingAddress.contract || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å°å">{{ shippingAddress.address || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-divider content-position="left">è系信æ¯</el-divider> |
| | | <el-descriptions :column="4" border> |
| | | <el-descriptions-item label="è系人">{{ contractInfo.name || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ§å«">{{ contractInfo.sex || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="çæ¥">{{ contractInfo.birthday || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é¨é¨">{{ contractInfo.department || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="èå¡">{{ contractInfo.job || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ææºå·">{{ contractInfo.phoneNumber || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="é®ç®±">{{ contractInfo.email || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="QQ">{{ contractInfo.qq || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="åºå®çµè¯">{{ contractInfo.lineaFissa || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="微信">{{ contractInfo.wx || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç±è´¯">{{ contractInfo.origineEtnica || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ³äººä»£è¡¨">{{ contractInfo.rappresentanteLegale || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®ç±»å" name="plan"> |
| | | <el-descriptions :column="4" border> |
| | | <el-descriptions-item label="ç±»ååç§°">{{ projectPlan.name || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="夿³¨" :span="3">{{ projectPlan.description || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="attachment-block" v-if="planAttachments.length > 0"> |
| | | <div class="attachment-title">ç±»åéä»¶</div> |
| | | <div class="attachment-list"> |
| | | <div v-for="(att, idx) in planAttachments" :key="att.id || att.url || idx" class="attachment-item"> |
| | | <el-icon><Document /></el-icon> |
| | | <span class="attachment-name">{{ att.name || att.fileName || att.url || 'éä»¶' }}</span> |
| | | <el-button link type="primary" size="small" @click="downloadAttachment(att)">ä¸è½½</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-table :data="planNodeRows" border style="margin-top: 14px;"> |
| | | <el-table-column align="center" label="æ¥éª¤" type="index" width="80" /> |
| | | <el-table-column label="é¶æ®µåç§°" prop="name" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="è´è´£äºº" prop="leaderName" width="140" show-overflow-tooltip /> |
| | | <el-table-column label="é¢è®¡å·¥æ(天)" prop="estimatedDuration" width="140" /> |
| | | <el-table-column label="å·¥æ¶åä»·" prop="hourlyRate" width="120" /> |
| | | <el-table-column label="ä½ä¸å
容" prop="workContent" min-width="180" show-overflow-tooltip /> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="项ç®é¶æ®µ" name="stage"> |
| | | <el-table :data="stageNodeRows" border style="margin-top: 14px;"> |
| | | <el-table-column align="center" label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="é¶æ®µ" prop="stageName" min-width="160" show-overflow-tooltip /> |
| | | <el-table-column label="æè¿°" prop="description" min-width="220" show-overflow-tooltip /> |
| | | <el-table-column label="å®é
è´è´£äºº" prop="actualLeaderName" width="140" show-overflow-tooltip /> |
| | | <el-table-column label="è¿åº¦(%)" prop="progress" width="110" /> |
| | | <el-table-column label="计åå¼å§" prop="planStartTime" width="120" /> |
| | | <el-table-column label="计åç»æ" prop="planEndTime" width="120" /> |
| | | <el-table-column label="å®é
å¼å§" prop="actualStartTime" width="120" /> |
| | | <el-table-column label="å®é
ç»æ" prop="actualEndTime" width="120" /> |
| | | <el-table-column label="é¢è®¡å·¥æ(天)" prop="estimatedDuration" width="130" /> |
| | | <el-table-column label="éä»¶" width="90" align="center"> |
| | | <template #default="{ row }"> |
| | | <span>{{ row.attachmentCount }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="false" label="æä½" width="100" align="center" fixed="right" > |
| | | <template #default="{ row }"> |
| | | <el-button link type="danger" size="small" @click="handleDeleteStage(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <ProgressReportDialog |
| | | v-model="progressReportVisible" |
| | | :project-id="info.id" |
| | | :project-info="info" |
| | | :plan-nodes="planNodeRows" |
| | | :default-plan-node-id="defaultPlanNodeId" |
| | | @submitted="handleProgressSubmitted" |
| | | /> |
| | | <DiscussProgressDialog |
| | | v-model="discussProgressVisible" |
| | | :project-id="info.id" |
| | | :plan-nodes="planNodeRows" |
| | | :default-plan-node-id="defaultPlanNodeId" |
| | | @submitted="handleDiscussSubmitted" |
| | | /> |
| | | </template> |
| | | |
| | | <script setup name="ProjectManagementDetail"> |
| | | import { computed, getCurrentInstance, onMounted, ref } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import { Document } from '@element-plus/icons-vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { getProject, saveStage, listStage, deleteStage } from '@/api/projectManagement/project' |
| | | import { listPlan } from '@/api/projectManagement/projectType' |
| | | import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue' |
| | | import DiscussProgressDialog from '@/components/ProjectManagement/DiscussProgressDialog.vue' |
| | | import useUserStore from '@/store/modules/user' |
| | | |
| | | const { proxy } = getCurrentInstance() |
| | | const route = useRoute() |
| | | const router = useRouter() |
| | | const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status') |
| | | |
| | | const loading = ref(false) |
| | | const activeTab = ref('base') |
| | | const progressReportVisible = ref(false) |
| | | const discussProgressVisible = ref(false) |
| | | const userStore = useUserStore() |
| | | |
| | | const info = ref({}) |
| | | const shippingAddress = ref({}) |
| | | const contractInfo = ref({}) |
| | | const productRows = ref([]) |
| | | const teamRows = ref([]) |
| | | const attachments = ref([]) |
| | | const projectTypeMap = ref(new Map()) |
| | | const projectPlan = ref({}) |
| | | const planNodeRows = ref([]) |
| | | const planAttachments = ref([]) |
| | | const stageNodeRows = ref([]) |
| | | |
| | | const estimatedDays = computed(() => { |
| | | const raw = info.value?.estimatedDays |
| | | const n = Number(raw) |
| | | if (Number.isFinite(n) && n > 0) return n |
| | | const start = info.value?.planStartTime |
| | | const end = info.value?.planEndTime |
| | | if (!start || !end) return 0 |
| | | const startTime = new Date(`${start}T00:00:00`).getTime() |
| | | const endTime = new Date(`${end}T00:00:00`).getTime() |
| | | if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime < startTime) return 0 |
| | | return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1 |
| | | }) |
| | | |
| | | const projectTypeLabel = computed(() => { |
| | | const id = info.value?.projectManagementPlanId |
| | | if (id === undefined || id === null || id === '') return '-' |
| | | const p = projectTypeMap.value.get(Number(id)) |
| | | return p?.name || String(id) |
| | | }) |
| | | |
| | | const parentProjectLabel = computed(() => { |
| | | return ( |
| | | info.value?.parentTitle || |
| | | info.value?.projectManagementInfoParentName || |
| | | info.value?.projectManagementInfoParentId || |
| | | '-' |
| | | ) |
| | | }) |
| | | |
| | | const planStageEnum = computed(() => { |
| | | const list = Array.isArray(plan_status) ? plan_status : [] |
| | | return list.map(i => ({ value: String(i.value), label: i.label })) |
| | | }) |
| | | |
| | | const steps = computed(() => { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) |
| | | const labels = sorted.map(i => i.name || i.workContent).filter(Boolean) |
| | | return labels.length > 0 ? labels : planStageEnum.value.map(i => i.label) |
| | | }) |
| | | |
| | | const activeStep = computed(() => { |
| | | const statusOrStage = info.value?.stage ?? info.value?.status |
| | | const enumList = planStageEnum.value |
| | | // ä¼å
ä½¿ç¨ planStageEnum ç value å¹é
|
| | | const found = enumList.find(i => i.value === String(statusOrStage)) |
| | | const label = found?.label |
| | | // å¨é¡¹ç®ç±»åèç¹ä¸æ¥æ¾å¯¹åº label ç䏿 |
| | | const nodeLabels = steps.value |
| | | const idxByLabel = label ? nodeLabels.findIndex(l => String(l) === String(label)) : -1 |
| | | if (idxByLabel >= 0) return idxByLabel + 1 |
| | | // åéï¼å¦æ statusOrStage æ¯æ°åç´¢å¼ |
| | | const n = Number(statusOrStage) |
| | | if (Number.isFinite(n) && n > 0) return Math.min(n, nodeLabels.length) |
| | | return 0 |
| | | }) |
| | | |
| | | function goBack() { |
| | | router.back() |
| | | } |
| | | |
| | | function openProgressReport() { |
| | | progressReportVisible.value = true |
| | | } |
| | | |
| | | function openDiscussProgress() { |
| | | discussProgressVisible.value = true |
| | | } |
| | | |
| | | const defaultPlanNodeId = computed(() => { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | if (nodes.length === 0) return undefined |
| | | const stageVal = info.value?.stage |
| | | const direct = nodes.find(n => String(n.id) === String(stageVal)) |
| | | if (direct?.id) return direct.id |
| | | const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0)) |
| | | const idx = Number(stageVal) |
| | | if (Number.isFinite(idx)) { |
| | | const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0] |
| | | if (byIndex?.id) return byIndex.id |
| | | } |
| | | return sorted[0]?.id |
| | | }) |
| | | |
| | | async function handleProgressSubmitted(payload) { |
| | | try { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {} |
| | | const description = payload.remark |
| | | ? `${payload.reportDate || ''} ${payload.remark}`.trim() |
| | | : `${payload.reportDate || ''} è¿åº¦æ±æ¥`.trim() |
| | | const req = { |
| | | id: null, |
| | | projectManagementPlanNodeId: payload.planNodeId, |
| | | projectManagementInfoId: info.value?.id, |
| | | description, |
| | | actualLeaderId: userStore.id || info.value?.managerId, |
| | | actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName, |
| | | estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, |
| | | planStartTime: payload.planStartTime || info.value?.planStartTime, |
| | | planEndTime: payload.planEndTime || info.value?.planEndTime, |
| | | actualStartTime: payload.actualStartTime || null, |
| | | actualEndTime: payload.actualEndTime || null, |
| | | progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0, |
| | | attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : [] |
| | | } |
| | | const res = await saveStage(req) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('æäº¤æå') |
| | | await Promise.all([loadDetail(), loadStageList()]) |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'æäº¤å¤±è´¥') |
| | | } catch (e) { |
| | | ElMessage.error('æäº¤å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | async function handleDiscussSubmitted(payload) { |
| | | try { |
| | | const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {} |
| | | const req = { |
| | | id: null, |
| | | projectManagementPlanNodeId: payload.planNodeId, |
| | | projectManagementInfoId: info.value?.id, |
| | | description: payload.remark, |
| | | actualLeaderId: userStore.id || info.value?.managerId, |
| | | actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName, |
| | | estimatedDuration: Number(node.estimatedDuration ?? 0) || 0, |
| | | planStartTime: info.value?.planStartTime, |
| | | planEndTime: info.value?.planEndTime, |
| | | actualStartTime: info.value?.actualStartTime || null, |
| | | actualEndTime: info.value?.actualEndTime || null, |
| | | progress: Number(info.value?.progress ?? 0) || 0, |
| | | attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : [] |
| | | } |
| | | const res = await saveStage(req) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('æäº¤æå') |
| | | await Promise.all([loadDetail(), loadStageList()]) |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'æäº¤å¤±è´¥') |
| | | } catch (e) { |
| | | ElMessage.error('æäº¤å¤±è´¥') |
| | | } |
| | | } |
| | | |
| | | function downloadAttachment(att) { |
| | | if (att?.url) { |
| | | try { |
| | | proxy.$download.resource(att.url) |
| | | return |
| | | } catch (e) {} |
| | | } |
| | | if (att?.name) { |
| | | try { |
| | | proxy.$download.name(att.name, false) |
| | | return |
| | | } catch (e) {} |
| | | } |
| | | ElMessage.warning('éä»¶ææ ä¸è½½å°å') |
| | | } |
| | | |
| | | async function loadProjectTypeMap() { |
| | | try { |
| | | const res = await listPlan({ current: 1, size: 999 }) |
| | | const records = res?.data?.records || res?.records || res?.rows || [] |
| | | projectTypeMap.value = new Map((records || []).map(r => [Number(r.id), r])) |
| | | } catch { |
| | | projectTypeMap.value = new Map() |
| | | } |
| | | } |
| | | |
| | | function getPlanNodeName(planNodeId) { |
| | | const list = Array.isArray(planNodeRows.value) ? planNodeRows.value : [] |
| | | const node = list.find(n => String(n.id) === String(planNodeId)) |
| | | return node?.name || node?.workContent || String(planNodeId ?? '') |
| | | } |
| | | |
| | | async function loadStageList() { |
| | | const projectId = info.value?.id |
| | | if (!projectId) { |
| | | stageNodeRows.value = [] |
| | | return |
| | | } |
| | | try { |
| | | const res = await listStage(projectId) |
| | | const data = res?.data?.data ?? res?.data ?? res |
| | | const list = data?.records || data?.rows || data?.list || data || [] |
| | | const records = Array.isArray(list) ? list : [] |
| | | stageNodeRows.value = records.map(r => { |
| | | const attachmentList = Array.isArray(r.attachmentList) ? r.attachmentList : [] |
| | | const attachmentIds = Array.isArray(r.attachmentIds) ? r.attachmentIds : [] |
| | | return { |
| | | ...r, |
| | | stageName: getPlanNodeName(r.projectManagementPlanNodeId), |
| | | attachmentCount: attachmentList.length || attachmentIds.length || 0 |
| | | } |
| | | }) |
| | | } catch { |
| | | stageNodeRows.value = [] |
| | | } |
| | | } |
| | | |
| | | async function handleDeleteStage(row) { |
| | | const stageId = row?.id |
| | | if (!stageId) return |
| | | try { |
| | | await ElMessageBox.confirm('æ¯å¦ç¡®è®¤å é¤è¯¥é¡¹ç®é¶æ®µï¼', 'æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }) |
| | | const res = await deleteStage(stageId) |
| | | if (res?.code === 200) { |
| | | ElMessage.success('å 餿å') |
| | | await loadStageList() |
| | | return |
| | | } |
| | | ElMessage.error(res?.msg || 'å é¤å¤±è´¥') |
| | | } catch {} |
| | | } |
| | | |
| | | function syncProjectPlan() { |
| | | const id = info.value?.projectManagementPlanId |
| | | if (id === undefined || id === null || id === '') { |
| | | projectPlan.value = {} |
| | | planNodeRows.value = [] |
| | | planAttachments.value = [] |
| | | return |
| | | } |
| | | const plan = projectTypeMap.value.get(Number(id)) || {} |
| | | projectPlan.value = plan || {} |
| | | planNodeRows.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : [] |
| | | planAttachments.value = Array.isArray(plan?.attachmentList) ? plan.attachmentList : [] |
| | | } |
| | | |
| | | async function loadDetail() { |
| | | const id = route.params?.id |
| | | if (!id) return |
| | | loading.value = true |
| | | try { |
| | | const res = await getProject(id) |
| | | const detail = res?.data?.data ?? res?.data ?? res |
| | | info.value = detail?.info || {} |
| | | shippingAddress.value = detail?.shippingAddress || {} |
| | | contractInfo.value = detail?.contractInfo || {} |
| | | productRows.value = Array.isArray(detail?.salesLedgerProductList) ? detail.salesLedgerProductList : [] |
| | | teamRows.value = Array.isArray(detail?.info?.teamList) ? detail.info.teamList : [] |
| | | attachments.value = Array.isArray(detail?.info?.attachmentList) ? detail.info.attachmentList : [] |
| | | syncProjectPlan() |
| | | await loadStageList() |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await loadProjectTypeMap() |
| | | await loadDetail() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .section-bar { |
| | | width: 3px; |
| | | height: 14px; |
| | | background: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | .header-card { |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .header-row { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .header-title { |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .content-card { |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .attachment-block { |
| | | margin-top: 14px; |
| | | } |
| | | |
| | | .attachment-title { |
| | | font-weight: 600; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .attachment-list { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .attachment-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .attachment-name { |
| | | max-width: 520px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | </style> |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :title="title" |
| | | v-model="visible" |
| | | width="1000px" |
| | | append-to-body |
| | | @close="handleClose" |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="0"> |
| | | <!-- é¡¶é¨åºç¡ä¿¡æ¯ --> |
| | | <div class="base-info-row"> |
| | | <div class="info-item"> |
| | | <span class="item-label required">åç§°</span> |
| | | <el-input |
| | | v-model="form.name" |
| | | placeholder="请è¾å
¥åç§°" |
| | | maxlength="10" |
| | | show-word-limit |
| | | style="width: 220px" |
| | | /> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="item-label">夿³¨</span> |
| | | <el-input |
| | | v-model="form.description" |
| | | placeholder="请è¾å
¥å¤æ³¨" |
| | | maxlength="20" |
| | | show-word-limit |
| | | style="width: 220px" |
| | | /> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="item-label">éä»¶</span> |
| | | <el-upload |
| | | v-if="isEdit" |
| | | :action="uploadUrl" |
| | | :headers="uploadHeaders" |
| | | :on-success="handleUploadSuccess" |
| | | :on-remove="handleRemove" |
| | | v-model:file-list="uploadFileList" |
| | | :limit="3" |
| | | name="files" |
| | | multiple |
| | | > |
| | | <el-button type="primary">ä¸ä¼ éä»¶</el-button> |
| | | </el-upload> |
| | | <span v-else class="text-gray-400 text-sm">请å
ä¿åååä¸ä¼ éä»¶</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æ¥éª¤é
ç½®è¡¨æ ¼ --> |
| | | <p class="top-tip">请æç
§é¡ºåºé
置项ç®é¶æ®µï¼ææ½<b>æ¥éª¤</b>æåºå³å¯</p> |
| | | <div class="step-table-container"> |
| | | <el-table |
| | | :data="form.savePlanNodeList" |
| | | border |
| | | style="width: 100%" |
| | | row-key="id" |
| | | class="drag-table" |
| | | > |
| | | <el-table-column label="æ¥éª¤" width="80" align="center" class-name="drag-handle"> |
| | | <template #default="scope"> |
| | | <div class="step-index" style="cursor: move;"> |
| | | {{ scope.$index + 1 }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="é¶æ®µåç§°" min-width="150"> |
| | | <template #header> |
| | | <span class="required-star">*</span> é¶æ®µåç§° |
| | | </template> |
| | | <template #default="scope"> |
| | | <el-form-item |
| | | :prop="'savePlanNodeList.' + scope.$index + '.name'" |
| | | :rules="[{ required: true, message: '请è¾å
¥é¶æ®µåç§°', trigger: 'blur' }]" |
| | | > |
| | | <el-input v-model="scope.row.name" placeholder="请è¾å
¥" /> |
| | | </el-form-item> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="è´è´£äºº" width="180"> |
| | | <template #header> |
| | | <span class="required-star">*</span> è´è´£äºº |
| | | </template> |
| | | <template #default="scope"> |
| | | <el-form-item |
| | | :prop="'savePlanNodeList.' + scope.$index + '.leaderId'" |
| | | :rules="[{ required: true, message: 'è¯·éæ©è´è´£äºº', trigger: 'change' }]" |
| | | > |
| | | <el-select |
| | | v-model="scope.row.leaderId" |
| | | placeholder="æµè¯" |
| | | @change="(val) => handleLeaderChange(val, scope.row)" |
| | | > |
| | | <el-option |
| | | v-for="item in userOptions" |
| | | :key="item.userId" |
| | | :label="item.nickName" |
| | | :value="item.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="é¢è®¡å·¥æ (天)" width="150"> |
| | | <template #header> |
| | | é¢è®¡å·¥æ (天) |
| | | <el-tooltip content="å®æè¯¥é¶æ®µé¢è®¡éè¦ç天æ°" placement="top"> |
| | | <el-icon class="info-icon"><QuestionFilled /></el-icon> |
| | | </el-tooltip> |
| | | </template> |
| | | <template #default="scope"> |
| | | <el-input-number |
| | | v-model="scope.row.estimatedDuration" |
| | | :min="0" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="å·¥æ¶åä»·" width="120"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.hourlyRate" placeholder="请è¾å
¥" /> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="ä½ä¸å
容" min-width="150"> |
| | | <template #default="scope"> |
| | | <el-input v-model="scope.row.workContent" placeholder="请è¾å
¥" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" min-width="150"> |
| | | <template #default="scope"> |
| | | <el-button type="danger" size="mini" @click="removeStep(scope.$index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <div class="add-row-btn" @click="addStep"> |
| | | <el-icon><Plus /></el-icon> æ°å¢ä¸è¡ |
| | | </div> |
| | | </div> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="visible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitForm">æäº¤</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, watch, onMounted, nextTick } from 'vue'; |
| | | import { Plus, QuestionFilled } from '@element-plus/icons-vue'; |
| | | import { userListNoPageByTenantId } from '@/api/system/user'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import { getToken } from '@/utils/auth'; |
| | | import Sortable from 'sortablejs'; |
| | | |
| | | const props = defineProps({ |
| | | modelValue: Boolean, |
| | | title: String, |
| | | data: Object |
| | | }); |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'submit']); |
| | | |
| | | const visible = ref(false); |
| | | const formRef = ref(null); |
| | | const userOptions = ref([]); |
| | | const isEdit = ref(false); |
| | | const uploadHeaders = { Authorization: "Bearer " + getToken() }; |
| | | // ä¸ä¼ å°å |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload"; |
| | | let sortable = null; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | name: '', |
| | | description: '', |
| | | attachmentIds: [], |
| | | savePlanNodeList: [] |
| | | }); |
| | | const uploadFileList = ref([]); |
| | | |
| | | const rules = { |
| | | name: [{ required: true, message: '请è¾å
¥åç§°', trigger: 'blur' }] |
| | | }; |
| | | |
| | | // çå¬å¼¹çªæ¾ç¤º/éè |
| | | watch(() => props.modelValue, (val) => { |
| | | visible.value = val; |
| | | if (val) { |
| | | if (props.data) { |
| | | // ç¼è¾æ¨¡å¼ - åæ¾æ°æ® |
| | | isEdit.value = true; |
| | | form.value = { |
| | | id: props.data.id, |
| | | name: props.data.name, |
| | | description: props.data.description, |
| | | attachmentIds: Array.isArray(props.data.attachmentIds) |
| | | ? props.data.attachmentIds |
| | | : (props.data.attachmentList || []).map(f => f.id).filter(Boolean), |
| | | savePlanNodeList: [] |
| | | }; |
| | | |
| | | // åæ¾æ¥éª¤èç¹ |
| | | if (props.data.planNodeList && props.data.planNodeList.length > 0) { |
| | | form.value.savePlanNodeList = props.data.planNodeList.map(node => ({ |
| | | id: node.id, |
| | | projectManagementPlanId: node.projectManagementPlanId, |
| | | sort: node.sort, |
| | | name: node.name, |
| | | leaderId: node.leaderId, |
| | | leaderName: node.leaderName, |
| | | estimatedDuration: node.estimatedDuration, |
| | | hourlyRate: node.hourlyRate, |
| | | workContent: node.workContent |
| | | })); |
| | | } else { |
| | | form.value.savePlanNodeList = [createDefaultNode()]; |
| | | } |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ |
| | | isEdit.value = false; |
| | | resetForm(); |
| | | } |
| | | // åå§åææ½ |
| | | nextTick(() => { |
| | | initSortable(); |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | watch(visible, (val) => { |
| | | emit('update:modelValue', val); |
| | | }); |
| | | |
| | | /** åå§åææ½ */ |
| | | function initSortable() { |
| | | const el = document.querySelector('.drag-table .el-table__body-wrapper tbody'); |
| | | if (!el) return; |
| | | |
| | | if (sortable) { |
| | | sortable.destroy(); |
| | | } |
| | | |
| | | sortable = Sortable.create(el, { |
| | | handle: '.drag-handle', |
| | | animation: 150, |
| | | onEnd: ({ newIndex, oldIndex }) => { |
| | | const targetRow = form.value.savePlanNodeList.splice(oldIndex, 1)[0]; |
| | | form.value.savePlanNodeList.splice(newIndex, 0, targetRow); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** å建é»è®¤èç¹å¯¹è±¡ */ |
| | | function createDefaultNode() { |
| | | return { |
| | | name: '', |
| | | leaderId: null, |
| | | leaderName: null, |
| | | estimatedDuration: null, |
| | | hourlyRate: null, |
| | | workContent: null |
| | | }; |
| | | } |
| | | |
| | | /** é置表å */ |
| | | function resetForm() { |
| | | form.value = { |
| | | id: undefined, |
| | | name: '', |
| | | description: '', |
| | | attachmentIds: [], |
| | | savePlanNodeList: [createDefaultNode()] |
| | | }; |
| | | uploadFileList.value = []; |
| | | if (formRef.value) { |
| | | formRef.value.resetFields(); |
| | | } |
| | | } |
| | | |
| | | /** è·åç¨æ·å表 */ |
| | | async function getUserList() { |
| | | try { |
| | | const res = await userListNoPageByTenantId(); |
| | | if (res.code === 200) { |
| | | userOptions.value = res.data || []; |
| | | } |
| | | } catch (error) { |
| | | console.error('è·åç¨æ·å表失败:', error); |
| | | } |
| | | } |
| | | |
| | | /** å¤çè´è´£äººåå */ |
| | | function handleLeaderChange(val, row) { |
| | | const user = userOptions.value.find(u => u.userId === val); |
| | | if (user) { |
| | | row.leaderName = user.nickName; |
| | | } |
| | | } |
| | | |
| | | /** å¤çæä»¶ä¸ä¼ æå */ |
| | | function handleUploadSuccess(response, file, fileList) { |
| | | if (response.code === 200) { |
| | | const newFile = response.data; |
| | | const list = Array.isArray(newFile) ? newFile : [newFile]; |
| | | list.forEach(element => { |
| | | const id = element?.id; |
| | | if (id && !form.value.attachmentIds.includes(id)) { |
| | | form.value.attachmentIds.push(id); |
| | | } |
| | | }); |
| | | } else { |
| | | ElMessage.error(response.msg || 'ä¸ä¼ 失败'); |
| | | } |
| | | } |
| | | |
| | | /** å¤çæä»¶ç§»é¤ */ |
| | | function handleRemove(file) { |
| | | const removedId = file?.id || file?.response?.data?.id; |
| | | if (!removedId) return; |
| | | form.value.attachmentIds = form.value.attachmentIds.filter(id => id !== removedId); |
| | | } |
| | | |
| | | /** æ·»å æ¥éª¤ */ |
| | | function addStep() { |
| | | form.value.savePlanNodeList.push(createDefaultNode()); |
| | | } |
| | | |
| | | /** ç§»é¤æ¥éª¤ */ |
| | | function removeStep(index) { |
| | | if (form.value.savePlanNodeList.length <= 1) { |
| | | ElMessage.warning('è³å°ä¿çä¸ä¸ªæ¥éª¤'); |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm('æ¯å¦ç¡®è®¤å é¤è¯¥æ¥éª¤ï¼', 'ç³»ç»æç¤º', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }).then(() => { |
| | | form.value.savePlanNodeList.splice(index, 1); |
| | | }).catch(() => {}); |
| | | } |
| | | |
| | | /** ç§»å¨æ¥éª¤ */ |
| | | function moveStep(index, direction) { |
| | | const targetIndex = index + direction; |
| | | if (targetIndex < 0 || targetIndex >= form.value.savePlanNodeList.length) return; |
| | | |
| | | const list = form.value.savePlanNodeList; |
| | | const temp = list[index]; |
| | | list[index] = list[targetIndex]; |
| | | list[targetIndex] = temp; |
| | | } |
| | | |
| | | /** æäº¤è¡¨å */ |
| | | async function submitForm() { |
| | | if (!formRef.value) return; |
| | | |
| | | try { |
| | | const valid = await formRef.value.validate(); |
| | | if (valid) { |
| | | // æäº¤åèªå¨å¡«å
sort åæ®µï¼æå½åæ°ç»é¡ºåºæåº |
| | | form.value.savePlanNodeList.forEach((node, index) => { |
| | | node.sort = index; |
| | | }); |
| | | emit('submit', form.value); |
| | | } |
| | | } catch (error) { |
| | | console.error('表åéªè¯å¤±è´¥:', error); |
| | | } |
| | | } |
| | | |
| | | /** å
³éå¼¹çª */ |
| | | function handleClose() { |
| | | resetForm(); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getUserList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .base-info-row { |
| | | display: flex; |
| | | gap: 40px; |
| | | margin-bottom: 25px; |
| | | padding: 0 10px; |
| | | |
| | | .info-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | |
| | | .item-label { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | white-space: nowrap; |
| | | |
| | | &.required::before { |
| | | content: '*'; |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .step-table-container { |
| | | padding: 0 10px; |
| | | |
| | | :deep(.el-form-item) { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .required-star { |
| | | color: #f56c6c; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | .info-icon { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-left: 4px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .add-row-btn { |
| | | margin-top: 15px; |
| | | height: 40px; |
| | | border: 1px dashed #dcdfe6; |
| | | border-radius: 4px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | color: #409eff; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | transition: all 0.3s; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | background-color: #f0f7ff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 15px; |
| | | padding-top: 10px; |
| | | } |
| | | .top-tip { |
| | | |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin:0 0 10px 10px; |
| | | } |
| | | </style> |
| src/views/projectManagement/projectType/index.vue
src/views/projectManagement/roles/index.vue
src/views/qualityManagement/finalInspection/components/formDia.vue
src/views/qualityManagement/nonconformingManagement/components/formDia.vue
src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
src/views/qualityManagement/nonconformingManagement/index.vue
src/views/qualityManagement/processInspection/components/formDia.vue
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
src/views/reportAnalysis/productionAnalysis/components/center-center.vue
src/views/safeProduction/accidentReportingRecord/index.vue
src/views/safeProduction/hazardousMaterialsControl/index.vue
src/views/salesManagement/deliveryLedger/index.vue
src/views/salesManagement/indicatorStats/index.vue
src/views/salesManagement/invoiceRegistration/index.vue
src/views/salesManagement/receiptPayment/index.vue
src/views/salesManagement/returnOrder/components/formDia.vue
src/views/salesManagement/returnOrder/index.vue
src/views/salesManagement/salesLedger/index.vue
src/views/salesManagement/salesQuotation/index.vue
src/views/system/user/index.vue |