Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68904b02a5 | |||
| f3d69d7df0 | |||
| 11d33df46a | |||
| 06de9477e5 | |||
| f7ee9273c6 | |||
| 8388904916 | |||
| 24fec3c7d7 | |||
| b76ada9d6c | |||
| 2083754b16 | |||
| 3df09d7d45 | |||
| b878bc356e | |||
| d76977c07c | |||
| d95163663b | |||
| e95151ad51 | |||
| 5351911d50 | |||
| e602eecee8 | |||
| 70801096dd | |||
| 025cba317d | |||
| 5ce91ec5d5 | |||
| 26140aecb9 | |||
| 8d85311bb3 | |||
| 8b2fa09cb9 | |||
| 4d78466824 | |||
| fe4255c59b | |||
| c6c7c80d97 | |||
| 3397bd6a7b | |||
| 083d954765 | |||
| 4b060fc045 | |||
| 185a0f9aec | |||
| a528788409 | |||
| 72471f48de | |||
| 415e1fd58d | |||
| 698f808f0b | |||
| ea6ec13ea4 | |||
| 114c2fd2af | |||
| 6119c44394 | |||
| 9dab7f7495 | |||
| f5697e111a | |||
| 775307f5c0 | |||
| 4502881063 | |||
| 358add743e | |||
| f67a4036ed | |||
| 7ba6620f06 | |||
| 9a3652d767 | |||
| 450505715a | |||
| 9a603a3796 | |||
| 2b51619728 | |||
| 5f167aae50 | |||
| 0d8eb25253 | |||
| 74988c0b5e | |||
| e53852b05f | |||
| 8d3e87d710 | |||
| c7b07f0c95 | |||
| 4c117d87a5 | |||
| 59199397e0 | |||
| 70d49d022b | |||
| 7d16660bfc | |||
| d6bcb12c20 | |||
| e1d87f5788 | |||
| 56b8f68449 | |||
| 0ac17dea0b | |||
| 39e3115fbe | |||
| 9c0f7cb3f4 | |||
| 7b478bdbf2 | |||
| 87d6ff97a3 | |||
| e3bd44e8fc | |||
| 324daf9d4d | |||
| 37ac683fb4 | |||
| c402d68aea | |||
| e8f6311477 | |||
| fa78a68df6 | |||
| 320422856c | |||
| 0add4c5cdc | |||
| f6621392b1 | |||
| 2a5eca8393 | |||
| e4ae1d3c77 | |||
| 96783fcd7b | |||
| 372e8826a2 | |||
| 8d9fe998c0 | |||
| 92a8d4aab1 | |||
| 59a1ca8a0d | |||
| b9de34aa0e | |||
| 5544cfb3ca | |||
| fcd5ef7df9 | |||
| 489b406214 | |||
| 353148eb7f | |||
| 293b4fc6ce | |||
| 53b588063e | |||
| 0889f29c59 | |||
| f1c004813e | |||
| d67cf55491 | |||
| d7969c191a | |||
| 2f64abfc83 | |||
| a657b9d0ef | |||
| 9558426da1 | |||
| 6384e80758 | |||
| 3f019c9ac5 | |||
| e7c76c803f | |||
| 90edc4b864 | |||
| d05f8fd636 | |||
| d968c418a8 | |||
| d100e7a271 | |||
| 447b2d9d76 | |||
| 603038d5ff | |||
| 673c47771b | |||
| 3168d365d0 | |||
| d3af455374 | |||
| 0c4bb7cd82 | |||
| 1622015724 | |||
| f7d8fbf16b | |||
| 847f09b14d | |||
| 2ae817ec38 | |||
| 6d80ffde6c | |||
| 44809ad303 | |||
| 91cb3ee8c2 | |||
| b2a6d73bac | |||
| fb91768585 | |||
| 411fdd2425 | |||
| 7ea96bcdc0 | |||
| b7af496a43 | |||
| 9e5f53702f | |||
| 8640238cbe | |||
| ef1a43f947 |
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
pnpm-lock.yaml
|
||||
/dist
|
||||
/dist.zip
|
||||
|
||||
/tests/e2e/videos/
|
||||
/tests/e2e/screenshots/
|
||||
|
||||
+5
-4
@@ -4,10 +4,11 @@
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"prod": "vite --mode production",
|
||||
"build": "vite build",
|
||||
"build:prod": "vite build --mode production",
|
||||
"build": "vite build && node scripts/zip-dist.mjs",
|
||||
"build:prod": "vite build --mode production && node scripts/zip-dist.mjs",
|
||||
"zip:dist": "node scripts/zip-dist.mjs",
|
||||
"serve": "vite preview --host",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
"typecheck": "vue-tsc -p jsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
@@ -48,7 +49,7 @@
|
||||
"prettier": "^2.8.7",
|
||||
"sass": "^1.85.1",
|
||||
"terser": "^5.31.1",
|
||||
"typescript": "^7.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"unplugin-auto-import": "^0.11.2",
|
||||
"vite": "^5.4.19",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
|
||||
Generated
+94
-295
@@ -13,31 +13,31 @@ importers:
|
||||
dependencies:
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.2(vue@3.5.34(typescript@7.0.2))
|
||||
version: 2.3.2(vue@3.5.34(typescript@5.9.3))
|
||||
'@open-file-viewer/core':
|
||||
specifier: ^0.1.27
|
||||
version: 0.1.27(pdfjs-dist@6.1.200)
|
||||
'@open-file-viewer/vue':
|
||||
specifier: ^0.1.27
|
||||
version: 0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34(typescript@7.0.2))
|
||||
version: 0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34(typescript@5.9.3))
|
||||
'@saber/nf-design-base-elp':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
'@saber/nf-form-design-elp':
|
||||
specifier: ^1.6.1
|
||||
version: 1.6.1(vue@3.5.34(typescript@7.0.2))
|
||||
version: 1.6.1(vue@3.5.34(typescript@5.9.3))
|
||||
'@saber/nf-form-elp':
|
||||
specifier: ^1.6.3
|
||||
version: 1.7.0
|
||||
'@smallwei/avue':
|
||||
specifier: ~3.9.2
|
||||
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@7.0.2)))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34(typescript@7.0.2)))(vue@3.5.34(typescript@7.0.2))
|
||||
version: 3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@5.9.3)))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34(typescript@5.9.3)))(vue@3.5.34(typescript@5.9.3))
|
||||
animate.css:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
avue-plugin-ueditor:
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34(typescript@7.0.2))
|
||||
version: 1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34(typescript@5.9.3))
|
||||
axios:
|
||||
specifier: ^1.13.5
|
||||
version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
@@ -58,7 +58,7 @@ importers:
|
||||
version: 6.1.0
|
||||
element-plus:
|
||||
specifier: ^2.14.2
|
||||
version: 2.14.2(vue@3.5.34(typescript@7.0.2))
|
||||
version: 2.14.2(vue@3.5.34(typescript@5.9.3))
|
||||
exceljs:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
@@ -85,29 +85,29 @@ importers:
|
||||
version: 0.4.0
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.34(typescript@7.0.2)
|
||||
version: 3.5.34(typescript@5.9.3)
|
||||
vue-i18n:
|
||||
specifier: ^11.4.2
|
||||
version: 11.4.2(vue@3.5.34(typescript@7.0.2))
|
||||
version: 11.4.2(vue@3.5.34(typescript@5.9.3))
|
||||
vue-pdf-embed:
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6(vue@3.5.34(typescript@7.0.2))
|
||||
version: 2.1.6(vue@3.5.34(typescript@5.9.3))
|
||||
vue-router:
|
||||
specifier: ^4.3.2
|
||||
version: 4.6.4(vue@3.5.34(typescript@7.0.2))
|
||||
version: 4.6.4(vue@3.5.34(typescript@5.9.3))
|
||||
vue3-clipboard:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(vue@3.5.34(typescript@7.0.2))
|
||||
version: 1.0.0(vue@3.5.34(typescript@5.9.3))
|
||||
vuex:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(vue@3.5.34(typescript@7.0.2))
|
||||
version: 4.1.0(vue@3.5.34(typescript@5.9.3))
|
||||
xlsx:
|
||||
specifier: 0.18.5
|
||||
version: 0.18.5
|
||||
devDependencies:
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^5.2.4
|
||||
version: 5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34(typescript@7.0.2))
|
||||
version: 5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34(typescript@5.9.3))
|
||||
'@vue/compiler-sfc':
|
||||
specifier: ^3.4.27
|
||||
version: 3.5.34
|
||||
@@ -121,11 +121,11 @@ importers:
|
||||
specifier: ^5.31.1
|
||||
version: 5.46.2
|
||||
typescript:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
unplugin-auto-import:
|
||||
specifier: ^0.11.2
|
||||
version: 0.11.5(@vueuse/core@14.3.0(vue@3.5.34(typescript@7.0.2)))(rollup@4.60.3)
|
||||
version: 0.11.5(@vueuse/core@14.3.0(vue@3.5.34(typescript@5.9.3)))(rollup@4.60.3)
|
||||
vite:
|
||||
specifier: ^5.4.19
|
||||
version: 5.4.21(sass@1.99.0)(terser@5.46.2)
|
||||
@@ -137,7 +137,7 @@ importers:
|
||||
version: 0.4.0(vite@5.4.21(sass@1.99.0)(terser@5.46.2))
|
||||
vue-tsc:
|
||||
specifier: ^3.3.11
|
||||
version: 3.3.13(typescript@7.0.2)
|
||||
version: 3.3.13(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -771,126 +771,6 @@ packages:
|
||||
'@types/web-bluetooth@0.0.21':
|
||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||
|
||||
'@typescript/typescript-aix-ppc64@7.0.2':
|
||||
resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.0.2':
|
||||
resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-arm@7.0.2':
|
||||
resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.0.2':
|
||||
resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.0.2':
|
||||
resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.0.2':
|
||||
resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.0.2':
|
||||
resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.0.2':
|
||||
resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-linux-x64@7.0.2':
|
||||
resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.0.2':
|
||||
resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.0.2':
|
||||
resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.0.2':
|
||||
resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@typescript/typescript-win32-x64@7.0.2':
|
||||
resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
|
||||
|
||||
@@ -2194,9 +2074,9 @@ packages:
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
|
||||
typescript@7.0.2:
|
||||
resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==}
|
||||
engines: {node: '>=16.20.0'}
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
ufo@1.6.4:
|
||||
@@ -2408,9 +2288,9 @@ snapshots:
|
||||
|
||||
'@ctrl/tinycolor@4.2.0': {}
|
||||
|
||||
'@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@7.0.2))':
|
||||
'@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
@@ -2651,10 +2531,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- geotiff
|
||||
|
||||
'@open-file-viewer/vue@0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34(typescript@7.0.2))':
|
||||
'@open-file-viewer/vue@0.1.27(@open-file-viewer/core@0.1.27(pdfjs-dist@6.1.200))(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@open-file-viewer/core': 0.1.27(pdfjs-dist@6.1.200)
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.6':
|
||||
optional: true
|
||||
@@ -2806,9 +2686,9 @@ snapshots:
|
||||
codemirror: 5.65.21
|
||||
randomcolor: 0.6.2
|
||||
|
||||
'@saber/nf-form-design-elp@1.6.1(vue@3.5.34(typescript@7.0.2))':
|
||||
'@saber/nf-form-design-elp@1.6.1(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
vuedraggable: 4.1.0(vue@3.5.34(typescript@7.0.2))
|
||||
vuedraggable: 4.1.0(vue@3.5.34(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
@@ -2820,16 +2700,16 @@ snapshots:
|
||||
countup.js: 2.10.0
|
||||
signature_pad: 5.1.3
|
||||
|
||||
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@7.0.2)))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34(typescript@7.0.2)))(vue@3.5.34(typescript@7.0.2))':
|
||||
'@smallwei/avue@3.9.2(patch_hash=5ba15ad41b2c301250bf24adf39da2ed526b8375143da2c5a495361b29e862db)(@element-plus/icons-vue@2.3.2(vue@3.5.34(typescript@5.9.3)))(debug@4.4.3(supports-color@7.2.0))(element-plus@2.14.2(vue@3.5.34(typescript@5.9.3)))(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@7.0.2))
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@5.9.3))
|
||||
axios: 0.21.4(debug@4.4.3(supports-color@7.2.0))
|
||||
countup.js: 1.9.3
|
||||
dayjs: 1.11.20
|
||||
element-plus: 2.14.2(vue@3.5.34(typescript@7.0.2))
|
||||
element-plus: 2.14.2(vue@3.5.34(typescript@5.9.3))
|
||||
lodash: 4.18.1
|
||||
lodash-es: 4.18.1
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
@@ -2856,66 +2736,6 @@ snapshots:
|
||||
|
||||
'@types/web-bluetooth@0.0.21': {}
|
||||
|
||||
'@typescript/typescript-aix-ppc64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-darwin-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-freebsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-arm@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-loong64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-mips64el@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-ppc64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-riscv64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-s390x@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-linux-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-netbsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-openbsd-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-sunos-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-arm64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@typescript/typescript-win32-x64@7.0.2':
|
||||
optional: true
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
dependencies:
|
||||
'@uppy/utils': 4.1.3
|
||||
@@ -2945,10 +2765,10 @@ snapshots:
|
||||
'@uppy/utils': 4.1.3
|
||||
nanoid: 3.3.12
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34(typescript@7.0.2))':
|
||||
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(sass@1.99.0)(terser@5.46.2))(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
vite: 5.4.21(sass@1.99.0)(terser@5.46.2)
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@volar/language-core@2.4.28':
|
||||
dependencies:
|
||||
@@ -2956,13 +2776,13 @@ snapshots:
|
||||
|
||||
'@volar/source-map@2.4.28': {}
|
||||
|
||||
'@volar/typescript@2.4.28(typescript@7.0.2)':
|
||||
'@volar/typescript@2.4.28(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.28
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.2.0
|
||||
optionalDependencies:
|
||||
typescript: 7.0.2
|
||||
typescript: 5.9.3
|
||||
|
||||
'@vue/compiler-core@3.5.34':
|
||||
dependencies:
|
||||
@@ -3022,51 +2842,51 @@ snapshots:
|
||||
'@vue/shared': 3.5.34
|
||||
csstype: 3.2.3
|
||||
|
||||
'@vue/server-renderer@3.5.34(vue@3.5.34(typescript@7.0.2))':
|
||||
'@vue/server-renderer@3.5.34(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@vue/compiler-ssr': 3.5.34
|
||||
'@vue/shared': 3.5.34
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@vue/shared@3.5.34': {}
|
||||
|
||||
'@vueuse/core@14.3.0(vue@3.5.34(typescript@7.0.2))':
|
||||
'@vueuse/core@14.3.0(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 14.3.0
|
||||
'@vueuse/shared': 14.3.0(vue@3.5.34(typescript@7.0.2))
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
'@vueuse/shared': 14.3.0(vue@3.5.34(typescript@5.9.3))
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@vueuse/metadata@14.3.0': {}
|
||||
|
||||
'@vueuse/shared@14.3.0(vue@3.5.34(typescript@7.0.2))':
|
||||
'@vueuse/shared@14.3.0(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 3.0.0
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
is-url: 1.2.4
|
||||
lodash.throttle: 4.1.1
|
||||
nanoid: 5.1.11
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/code-highlight@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
prismjs: 1.30.0
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@types/event-emitter': 0.3.5
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
dom7: 3.0.0
|
||||
dom7: 4.0.6
|
||||
event-emitter: 0.3.5
|
||||
html-void-elements: 3.0.0
|
||||
i18next: 23.16.8
|
||||
@@ -3087,13 +2907,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/code-highlight': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/list-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/table-module': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/upload-image-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/video-module': 2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
is-hotkey: 0.2.0
|
||||
lodash.camelcase: 4.3.0
|
||||
@@ -3106,16 +2926,16 @@ snapshots:
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/list-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/table-module@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
lodash.debounce: 4.0.8
|
||||
lodash.throttle: 4.1.1
|
||||
@@ -3123,22 +2943,22 @@ snapshots:
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/upload-image-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/basic-modules': 2.0.0(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
lodash.foreach: 4.5.0
|
||||
slate: 0.123.0
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
'@wangeditor-next/video-module@2.0.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3))(dom7@4.0.6)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
'@wangeditor-next/core': 1.8.0(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.11)(slate@0.123.0)(snabbdom@3.6.3)
|
||||
dom7: 4.0.6
|
||||
nanoid: 5.1.11
|
||||
slate: 0.123.0
|
||||
@@ -3185,10 +3005,10 @@ snapshots:
|
||||
slate-history: 0.66.0(slate@0.72.8)
|
||||
snabbdom: 3.6.3
|
||||
|
||||
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@7.0.2))':
|
||||
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@wangeditor/editor': 5.1.23
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
'@wangeditor/editor@5.1.23':
|
||||
dependencies:
|
||||
@@ -3317,12 +3137,12 @@ snapshots:
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34(typescript@7.0.2)):
|
||||
avue-plugin-ueditor@1.0.4(axios@1.16.0(debug@4.4.3(supports-color@7.2.0)))(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@wangeditor/editor': 5.1.23
|
||||
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@7.0.2))
|
||||
'@wangeditor/editor-for-vue': 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34(typescript@5.9.3))
|
||||
axios: 1.16.0(debug@4.4.3(supports-color@7.2.0))
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
axios@0.21.4(debug@4.4.3(supports-color@7.2.0)):
|
||||
dependencies:
|
||||
@@ -3575,15 +3395,15 @@ snapshots:
|
||||
tslib: 2.3.0
|
||||
zrender: 6.1.0
|
||||
|
||||
element-plus@2.14.2(vue@3.5.34(typescript@7.0.2)):
|
||||
element-plus@2.14.2(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 4.2.0
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@7.0.2))
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@5.9.3))
|
||||
'@floating-ui/dom': 1.7.6
|
||||
'@popperjs/core': '@sxzz/popperjs-es@2.11.8'
|
||||
'@types/lodash': 4.17.24
|
||||
'@types/lodash-es': 4.17.12
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@7.0.2))
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@5.9.3))
|
||||
async-validator: 4.2.5
|
||||
dayjs: 1.11.20
|
||||
lodash: 4.18.1
|
||||
@@ -3591,7 +3411,7 @@ snapshots:
|
||||
lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.18.1)(lodash@4.18.1)
|
||||
memoize-one: 6.0.0
|
||||
normalize-wheel-es: 1.2.0
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
vue-component-type-helpers: 3.3.6
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
@@ -4352,28 +4172,7 @@ snapshots:
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typescript@7.0.2:
|
||||
optionalDependencies:
|
||||
'@typescript/typescript-aix-ppc64': 7.0.2
|
||||
'@typescript/typescript-darwin-arm64': 7.0.2
|
||||
'@typescript/typescript-darwin-x64': 7.0.2
|
||||
'@typescript/typescript-freebsd-arm64': 7.0.2
|
||||
'@typescript/typescript-freebsd-x64': 7.0.2
|
||||
'@typescript/typescript-linux-arm': 7.0.2
|
||||
'@typescript/typescript-linux-arm64': 7.0.2
|
||||
'@typescript/typescript-linux-loong64': 7.0.2
|
||||
'@typescript/typescript-linux-mips64el': 7.0.2
|
||||
'@typescript/typescript-linux-ppc64': 7.0.2
|
||||
'@typescript/typescript-linux-riscv64': 7.0.2
|
||||
'@typescript/typescript-linux-s390x': 7.0.2
|
||||
'@typescript/typescript-linux-x64': 7.0.2
|
||||
'@typescript/typescript-netbsd-arm64': 7.0.2
|
||||
'@typescript/typescript-netbsd-x64': 7.0.2
|
||||
'@typescript/typescript-openbsd-arm64': 7.0.2
|
||||
'@typescript/typescript-openbsd-x64': 7.0.2
|
||||
'@typescript/typescript-sunos-x64': 7.0.2
|
||||
'@typescript/typescript-win32-arm64': 7.0.2
|
||||
'@typescript/typescript-win32-x64': 7.0.2
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.4: {}
|
||||
|
||||
@@ -4397,7 +4196,7 @@ snapshots:
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unplugin-auto-import@0.11.5(@vueuse/core@14.3.0(vue@3.5.34(typescript@7.0.2)))(rollup@4.60.3):
|
||||
unplugin-auto-import@0.11.5(@vueuse/core@14.3.0(vue@3.5.34(typescript@5.9.3)))(rollup@4.60.3):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.3)
|
||||
@@ -4406,7 +4205,7 @@ snapshots:
|
||||
unimport: 1.3.0(rollup@4.60.3)
|
||||
unplugin: 1.16.1
|
||||
optionalDependencies:
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@7.0.2))
|
||||
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
@@ -4465,54 +4264,54 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.3.6: {}
|
||||
|
||||
vue-i18n@11.4.2(vue@3.5.34(typescript@7.0.2)):
|
||||
vue-i18n@11.4.2(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 11.4.2
|
||||
'@intlify/devtools-types': 11.4.2
|
||||
'@intlify/shared': 11.4.2
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
vue-pdf-embed@2.1.6(vue@3.5.34(typescript@7.0.2)):
|
||||
vue-pdf-embed@2.1.6(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
pdfjs-dist: 6.2.108
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
vue-router@4.6.4(vue@3.5.34(typescript@7.0.2)):
|
||||
vue-router@4.6.4(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
vue-tsc@3.3.13(typescript@7.0.2):
|
||||
vue-tsc@3.3.13(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.28(typescript@7.0.2)
|
||||
'@volar/typescript': 2.4.28(typescript@5.9.3)
|
||||
'@vue/language-core': 3.3.13
|
||||
typescript: 7.0.2
|
||||
typescript: 5.9.3
|
||||
|
||||
vue3-clipboard@1.0.0(vue@3.5.34(typescript@7.0.2)):
|
||||
vue3-clipboard@1.0.0(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
clipboard: 2.0.11
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
vue@3.5.34(typescript@7.0.2):
|
||||
vue@3.5.34(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.34
|
||||
'@vue/compiler-sfc': 3.5.34
|
||||
'@vue/runtime-dom': 3.5.34
|
||||
'@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@7.0.2))
|
||||
'@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@5.9.3))
|
||||
'@vue/shared': 3.5.34
|
||||
optionalDependencies:
|
||||
typescript: 7.0.2
|
||||
typescript: 5.9.3
|
||||
|
||||
vuedraggable@4.1.0(vue@3.5.34(typescript@7.0.2)):
|
||||
vuedraggable@4.1.0(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
sortablejs: 1.14.0
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
vuex@4.1.0(vue@3.5.34(typescript@7.0.2)):
|
||||
vuex@4.1.0(vue@3.5.34(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.34(typescript@7.0.2)
|
||||
vue: 3.5.34(typescript@5.9.3)
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, rmSync, statSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const root = process.cwd();
|
||||
const distDir = resolve(root, 'dist');
|
||||
const zipPath = resolve(root, 'dist.zip');
|
||||
|
||||
if (!existsSync(distDir) || !statSync(distDir).isDirectory()) {
|
||||
console.error('[zip-dist] dist 目录不存在,请先完成构建');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (existsSync(zipPath)) {
|
||||
rmSync(zipPath);
|
||||
}
|
||||
|
||||
// 压缩 dist 内文件到根级(解压即 index.html / assets,便于直接上传部署)
|
||||
execFileSync('zip', ['-qr', zipPath, '.', '-x', '*.DS_Store', '-x', '__MACOSX/*'], {
|
||||
cwd: distDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
const sizeMb = (statSync(zipPath).size / (1024 * 1024)).toFixed(2);
|
||||
console.log(`[zip-dist] 已生成 ${zipPath}(${sizeMb} MB)`);
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createCrudApi } from './common';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/contract-change';
|
||||
const api = createCrudApi(baseUrl);
|
||||
|
||||
export const getList = api.getList;
|
||||
export const getDetail = api.getDetail;
|
||||
export const submit = api.submit;
|
||||
export const remove = api.remove;
|
||||
|
||||
export const createFromContract = (contractId, changeType) =>
|
||||
request({
|
||||
url: `${baseUrl}/create-from-contract`,
|
||||
method: 'post',
|
||||
params: { contractId, changeType },
|
||||
});
|
||||
|
||||
export const saveDraft = row =>
|
||||
request({
|
||||
url: `${baseUrl}/save-draft`,
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
|
||||
export const submitApproval = id =>
|
||||
request({
|
||||
url: `${baseUrl}/submit-approval`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const approve = id =>
|
||||
request({
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
timeout: APPROVE_TIMEOUT,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
request({
|
||||
url: `${baseUrl}/reject`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const withdraw = id =>
|
||||
request({
|
||||
url: `${baseUrl}/withdraw`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const markSyncStatus = (ids, syncStatus) =>
|
||||
request({
|
||||
url: `${baseUrl}/mark-sync-status`,
|
||||
method: 'post',
|
||||
params: { ids, syncStatus },
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createCrudApi } from './common';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/contract-manage';
|
||||
@@ -36,6 +37,7 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
timeout: APPROVE_TIMEOUT,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/hcs';
|
||||
|
||||
/** 进程内缓存,避免列表页重复请求推送开关 */
|
||||
let immediateEnabledCache = null;
|
||||
let immediateEnabledPromise = null;
|
||||
|
||||
/**
|
||||
* 查询 HCS 即时推送开关(thirdParty.hcs.push.immediate-enabled)。
|
||||
* 关闭时项目管理/合同管理隐藏「推送合规」「批量推送合规」。
|
||||
*/
|
||||
export const getPushConfig = () =>
|
||||
request({
|
||||
url: `${baseUrl}/push-config`,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
export const loadHcsImmediateEnabled = () => {
|
||||
if (immediateEnabledCache !== null) {
|
||||
return Promise.resolve(immediateEnabledCache);
|
||||
}
|
||||
if (immediateEnabledPromise) {
|
||||
return immediateEnabledPromise;
|
||||
}
|
||||
immediateEnabledPromise = getPushConfig()
|
||||
.then(res => {
|
||||
const data = res?.data?.data ?? res?.data ?? {};
|
||||
immediateEnabledCache = data.immediateEnabled === true || data.immediateEnabled === 'true';
|
||||
return immediateEnabledCache;
|
||||
})
|
||||
.catch(() => {
|
||||
immediateEnabledCache = false;
|
||||
return false;
|
||||
})
|
||||
.finally(() => {
|
||||
immediateEnabledPromise = null;
|
||||
});
|
||||
return immediateEnabledPromise;
|
||||
};
|
||||
|
||||
// 合规推送涉及 HCS 远程多次调用(风险校验+附件转传+起草),可达 20s+,覆盖全局 10s 超时
|
||||
export const repush = (bizType, id) =>
|
||||
request({
|
||||
url: `${baseUrl}/sync/${bizType}/${id}`,
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
// 批量推送为逐条同步推送,耗时随勾选行数线性增长,进一步放宽超时
|
||||
export const repushBatch = (bizType, ids) =>
|
||||
request({
|
||||
url: `${baseUrl}/sync/${bizType}/batch`,
|
||||
method: 'post',
|
||||
data: ids,
|
||||
timeout: 300000,
|
||||
});
|
||||
|
||||
export const getTemplates = () =>
|
||||
request({
|
||||
url: `${baseUrl}/master-data/template`,
|
||||
method: 'get',
|
||||
});
|
||||
|
||||
export const refreshMasterData = () =>
|
||||
request({
|
||||
url: `${baseUrl}/master-data/refresh`,
|
||||
method: 'post',
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const getRisk = (customerCode, creditCode, customerName) =>
|
||||
request({
|
||||
url: `${baseUrl}/risk`,
|
||||
method: 'get',
|
||||
params: { customerCode, creditCode, customerName },
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// 客商风险批量查询(客商列表页展示用):入参编号数组,后端逐个查 C101-1,单个失败跳过
|
||||
export const getRiskBatch = customerCodes =>
|
||||
request({
|
||||
url: `${baseUrl}/risk/batch`,
|
||||
method: 'get',
|
||||
params: { customerCodes: (customerCodes || []).join(',') },
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
// 客商风险按名称查询(客商档案表单展示用):名称反查编号后口径同编号查询
|
||||
export const getRiskByName = customerName =>
|
||||
request({
|
||||
url: `${baseUrl}/risk/by-name`,
|
||||
method: 'get',
|
||||
params: { customerName },
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
export const uploadAttachment = formData =>
|
||||
request({
|
||||
url: `${baseUrl}/attachment/upload`,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const downloadAttachment = (fdId, name) =>
|
||||
request({
|
||||
url: `${baseUrl}/attachment/download`,
|
||||
method: 'get',
|
||||
params: { fdId, name },
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
export const applyContractSpec = data =>
|
||||
request({
|
||||
url: `${baseUrl}/contract/spec-apply`,
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const confirmContractSpec = (id, result) =>
|
||||
request({
|
||||
url: `${baseUrl}/contract/spec/confirm`,
|
||||
method: 'post',
|
||||
params: { id, result },
|
||||
});
|
||||
|
||||
export const unwrapData = response => {
|
||||
const data = response?.data?.data ?? response?.data;
|
||||
return data && typeof data === 'object' && 'data' in data ? data.data : data;
|
||||
};
|
||||
@@ -24,11 +24,16 @@ export const getDetail = id =>
|
||||
},
|
||||
});
|
||||
|
||||
export const getCarrierContracts = projectIds =>
|
||||
export const getCarrierContracts = (projectIds, waybillIds = []) =>
|
||||
request({
|
||||
url: `${baseUrl}/carrier-contracts`,
|
||||
method: 'get',
|
||||
params: { projectIds: (projectIds || []).join(',') },
|
||||
params: {
|
||||
projectIds: (projectIds || []).join(','),
|
||||
...(waybillIds || []).filter(Boolean).length
|
||||
? { waybillIds: (waybillIds || []).filter(Boolean).join(',') }
|
||||
: {},
|
||||
},
|
||||
});
|
||||
|
||||
export const saveDraft = row =>
|
||||
|
||||
@@ -43,6 +43,8 @@ export const approve = id =>
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
// 审批通过后同步推送合规(远程多次调用),覆盖全局 10s 超时
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { createCrudApi } from './common';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
import request from '@/axios';
|
||||
|
||||
const baseUrl = '/blade-transport/project-change';
|
||||
const api = createCrudApi(baseUrl);
|
||||
|
||||
export const getList = api.getList;
|
||||
export const getDetail = api.getDetail;
|
||||
export const submit = api.submit;
|
||||
export const remove = api.remove;
|
||||
|
||||
export const createFromProject = (projectId, changeType) =>
|
||||
request({
|
||||
url: `${baseUrl}/create-from-project`,
|
||||
method: 'post',
|
||||
params: { projectId, changeType },
|
||||
});
|
||||
|
||||
export const saveDraft = row =>
|
||||
request({
|
||||
url: `${baseUrl}/save-draft`,
|
||||
method: 'post',
|
||||
data: row,
|
||||
});
|
||||
|
||||
export const submitApproval = id =>
|
||||
request({
|
||||
url: `${baseUrl}/submit-approval`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const approve = id =>
|
||||
request({
|
||||
url: `${baseUrl}/approve`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
timeout: APPROVE_TIMEOUT,
|
||||
});
|
||||
|
||||
export const reject = id =>
|
||||
request({
|
||||
url: `${baseUrl}/reject`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
|
||||
export const withdraw = id =>
|
||||
request({
|
||||
url: `${baseUrl}/withdraw`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
@@ -14,6 +14,13 @@ export const cancel = api.cancel;
|
||||
export const reassign = api.reassign;
|
||||
export const complete = api.complete;
|
||||
export const batchComplete = api.batchComplete;
|
||||
/** 确认回单:回单节点配成「需业务员确认」时用;后端校验司机打卡节点齐全且无未了结异常后才置完成 */
|
||||
export const confirmReturn = id =>
|
||||
request({
|
||||
url: `${baseUrl}/confirm-return`,
|
||||
method: 'post',
|
||||
params: { id },
|
||||
});
|
||||
export const changeRoute = data =>
|
||||
request({
|
||||
url: `${baseUrl}/change-route`,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import request from '@/axios';
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
/** 审核按钮统一超时:审批通过后可能同步推送金蝶等耗时操作,覆盖全局 10s */
|
||||
export const APPROVE_TIMEOUT = 60 * 1000;
|
||||
|
||||
/**
|
||||
* 文件流返回
|
||||
* @param url 接口地址
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/invoice-application';
|
||||
|
||||
@@ -18,7 +19,8 @@ export const getReceiverInformation = settlementIds =>
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const syncKingdee = id =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/invoice-receipt';
|
||||
|
||||
@@ -31,7 +32,8 @@ export const getReferenceInformation = settlementIds =>
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
export const pushKingdee = ids =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/payment-application';
|
||||
export const getList = (current, size, params) =>
|
||||
@@ -20,7 +21,8 @@ export const getProjectAdvanceWriteOffOptions = params =>
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
// 同步金蝶涉及多次金蝶远程接口调用(A103-2 预检 + A103-1/A103-3 推送),单笔可达 20s+,覆盖全局 10s 超时
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/formal-settlement';
|
||||
|
||||
@@ -27,7 +28,8 @@ export const getCandidateDetails = (current, size, params) =>
|
||||
export const save = data => request({ url: `${baseUrl}/save`, method: 'post', data });
|
||||
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
export const voidBill = data => request({ url: `${baseUrl}/void`, method: 'post', data });
|
||||
// 同步金蝶涉及多次金蝶远程接口调用(A102-4 预检 + A102-2 保存),可达 20s+,覆盖全局 10s 超时
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/axios';
|
||||
import { APPROVE_TIMEOUT } from '@/api/common';
|
||||
|
||||
const baseUrl = '/blade-transport/pre-settlement';
|
||||
|
||||
@@ -42,7 +43,8 @@ export const removeDetail = (id, detailId) =>
|
||||
|
||||
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||
|
||||
export const approve = data => request({ url: `${baseUrl}/approve`, method: 'post', data });
|
||||
export const approve = data =>
|
||||
request({ url: `${baseUrl}/approve`, method: 'post', data, timeout: APPROVE_TIMEOUT });
|
||||
|
||||
export const returnBill = data => request({ url: `${baseUrl}/return`, method: 'post', data });
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import request from '@/axios';
|
||||
|
||||
/** 提交审核流(MK)统一超时:客商/项目/合同/预结算/正式结算/付款/开票/收票/汇票付款 */
|
||||
export const MK_SUBMIT_TIMEOUT = 30000;
|
||||
|
||||
/**
|
||||
* 调用 MK processSubmit 提交审核流
|
||||
* @param {Object} data
|
||||
@@ -15,6 +18,7 @@ export const processSubmit = data => {
|
||||
url: '/blade-system/businessProcess/processSubmit',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: MK_SUBMIT_TIMEOUT,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -28,6 +32,7 @@ export const processDelete = data => {
|
||||
url: '/blade-system/businessProcess/processDelete',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: MK_SUBMIT_TIMEOUT,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -139,3 +139,10 @@ export const getPlatformCompanySelect = () => {
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const getBusinessDeptSelect = () => {
|
||||
return request({
|
||||
url: '/blade-system/dept/business-dept-select',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import request from '@/axios';
|
||||
|
||||
export const getRoleMenuDataScopeTree = roleId => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/tree',
|
||||
method: 'get',
|
||||
params: {
|
||||
roleId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const submitRoleMenuDataScope = data => {
|
||||
return request({
|
||||
url: '/blade-system/role-menu-data-scope/submit',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
@@ -163,6 +163,42 @@ export const queryKingdeeCustomer = creditCode => {
|
||||
});
|
||||
};
|
||||
|
||||
// 金蝶供应商编码查询(A901-1):新增客商填写信用代码后即时回显
|
||||
export const querySupplierCode = creditCode => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/query-supplier-code',
|
||||
method: 'get',
|
||||
timeout: 30000,
|
||||
params: {
|
||||
creditCode,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 金蝶开户行查询(A905-3):收款信息填写开户行失焦后精确匹配带出联行号
|
||||
export const queryKingdeeBank = bankName => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/query-bank',
|
||||
method: 'get',
|
||||
timeout: 30000,
|
||||
params: {
|
||||
bankName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 金蝶联行号校验(A905-3):收款信息保存前按联行号精确匹配,查不到不允许保存
|
||||
export const queryKingdeeBankByNumber = cnapsCode => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/query-bank-by-number',
|
||||
method: 'get',
|
||||
timeout: 30000,
|
||||
params: {
|
||||
cnapsCode,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const syncKingdee = id => {
|
||||
return request({
|
||||
url: '/blade-transport/customer-archive/sync-kingdee',
|
||||
|
||||
+31
-10
@@ -105,12 +105,27 @@ const isJsonContentType = res => {
|
||||
return contentType.includes('application/json') || contentType.includes('text/json');
|
||||
};
|
||||
|
||||
/**
|
||||
* 是否为可按文本读取的 Blob / Blob-like(含跨 realm)。
|
||||
* 禁止对普通业务 JSON 对象调用 .text()。
|
||||
*/
|
||||
const isBlobLikeData = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.text === 'function';
|
||||
|
||||
/**
|
||||
* 判断响应体是否已是解析好的对象(无需再解析)。
|
||||
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
|
||||
*/
|
||||
const isPlainObject = value =>
|
||||
value !== null && typeof value === 'object' && !(value instanceof Blob);
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
!(value instanceof Blob) &&
|
||||
!isBlobLikeData(value) &&
|
||||
!(value instanceof ArrayBuffer);
|
||||
//跨域请求,允许保存cookie
|
||||
axios.defaults.withCredentials = true;
|
||||
// NProgress Configuration
|
||||
@@ -190,15 +205,20 @@ axios.interceptors.response.use(
|
||||
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
|
||||
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
|
||||
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
|
||||
// 仅对真正的 Blob/Blob-like 调 .text(),避免业务 JSON 已解析后出现
|
||||
// 「e.data.text is not a function」。
|
||||
if (isJsonContentType(res) && !isPlainObject(res.data)) {
|
||||
try {
|
||||
let text = '';
|
||||
if (typeof res.data?.text === 'function') {
|
||||
text = await res.data.text();
|
||||
} else if (typeof res.data === 'string') {
|
||||
text = res.data;
|
||||
} else if (typeof res.data?.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await res.data.arrayBuffer());
|
||||
const raw = res.data;
|
||||
if (isBlobLikeData(raw) || (typeof Blob !== 'undefined' && raw instanceof Blob)) {
|
||||
text = await raw.text();
|
||||
} else if (typeof raw === 'string') {
|
||||
text = raw;
|
||||
} else if (raw && typeof raw.arrayBuffer === 'function') {
|
||||
text = new TextDecoder().decode(await raw.arrayBuffer());
|
||||
} else if (raw instanceof ArrayBuffer) {
|
||||
text = new TextDecoder().decode(raw);
|
||||
}
|
||||
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
@@ -208,10 +228,11 @@ axios.interceptors.response.use(
|
||||
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
|
||||
}
|
||||
}
|
||||
//获取状态信息
|
||||
const status = res.data.error_code || res.data.code || res.status;
|
||||
// 获取状态信息:文件流没有 code/msg,必须退回 HTTP status,禁止读 Blob 上的幽灵字段
|
||||
const payload = isPlainObject(res.data) ? res.data : {};
|
||||
const status = Number(payload.error_code || payload.code || res.status);
|
||||
const statusWhiteList = website.statusWhiteList || [];
|
||||
const message = res.data.msg || res.data.error_description || '系统错误';
|
||||
const message = payload.msg || payload.error_description || '系统错误';
|
||||
//如果在白名单里则自行catch逻辑处理
|
||||
if (statusWhiteList.includes(status)) return Promise.reject(res);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import { h } from 'vue';
|
||||
* 1. 混入本 mixin:mixins: [organizationSearch]
|
||||
* 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可)
|
||||
* 3. 在 created / 初始化时调用 this.loadOrganizationOptions()
|
||||
* 4. 需要「未分配组织」筛选项的页面在 data 里置 enableUnassignedOrgFilter: true;
|
||||
* 该项只追加进搜索树 organizationSearchTreeOptions,表单级联 organizationTreeOptions 不含哨兵
|
||||
*
|
||||
* 说明:
|
||||
* - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
|
||||
@@ -26,6 +28,12 @@ import { h } from 'vue';
|
||||
*/
|
||||
const ORG_PROPS = ['organizationName', 'deptName'];
|
||||
|
||||
// 「未分配组织」筛选项的哨兵值:不是真实组织名,选中后提交给后端的就是这串字面量。
|
||||
// 后端 TransportBusiConstant.UNASSIGNED_ORG_FILTER、DriverMapper.xml / TransportVehicleMapper.xml
|
||||
// 的 organizationName 分支以及司机、车辆导出查询都按它改判为 organization_name = '';
|
||||
// 后端换成空串会让整段组织条件不生效,筛选静默失效。前端与后端这几处必须同步修改。
|
||||
const UNASSIGNED_ORG_FILTER = '__UNASSIGNED_ORG__';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -34,9 +42,18 @@ export default {
|
||||
organizationTreeOptions: [],
|
||||
organizationTreeFlatOptions: [],
|
||||
organizationLoading: false,
|
||||
// 组织筛选是否追加「未分配组织」一项;页面在 data 里覆盖为 true 才生效
|
||||
enableUnassignedOrgFilter: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 搜索栏级联的选项:开关打开时在真实组织最前面追加「未分配组织」。
|
||||
// 不能直接改 organizationTreeOptions——司机/车辆页的编辑表单也用这棵树选组织。
|
||||
organizationSearchTreeOptions() {
|
||||
return this.enableUnassignedOrgFilter
|
||||
? [this.unassignedOrgOption(), ...this.organizationTreeOptions]
|
||||
: this.organizationTreeOptions;
|
||||
},
|
||||
// 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级
|
||||
organizationCascaderProps() {
|
||||
return {
|
||||
@@ -61,7 +78,7 @@ export default {
|
||||
? this.excludeExternalOrganization(source)
|
||||
: source;
|
||||
this.organizationTreeOptions = this.normalizeOrgTree(tree);
|
||||
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationTreeOptions);
|
||||
this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationSearchTreeOptions);
|
||||
const option = this.tableOption || this.option;
|
||||
if (!option) return;
|
||||
ORG_PROPS.forEach(prop => {
|
||||
@@ -100,12 +117,20 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
// 哨兵节点的 id 与 rawLabel 都取哨兵值,resolveOrgName 取到的仍是哨兵而不是某个真实组织名
|
||||
unassignedOrgOption() {
|
||||
return { id: UNASSIGNED_ORG_FILTER, label: '未分配组织', rawLabel: UNASSIGNED_ORG_FILTER };
|
||||
},
|
||||
flattenOrgTree(tree = []) {
|
||||
return (tree || []).flatMap(item => [
|
||||
{ id: String(item.id), label: item.label, rawLabel: item.rawLabel },
|
||||
...this.flattenOrgTree(item.children || []),
|
||||
]);
|
||||
},
|
||||
// 组织列渲染用:空串或纯空白即「未分配组织」(组织列 NOT NULL,无组织存空串)
|
||||
isBlankOrganizationName(value) {
|
||||
return String(value ?? '').trim() === '';
|
||||
},
|
||||
// 兼容两种入参:级联路径数组(取末级)或单个部门 id
|
||||
findOrgById(id) {
|
||||
const value = Array.isArray(id) ? id.at(-1) : id;
|
||||
@@ -155,7 +180,7 @@ export default {
|
||||
// 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中
|
||||
if (scope.row) scope.row[prop] = Array.isArray(value) ? value : [];
|
||||
},
|
||||
options: this.organizationTreeOptions,
|
||||
options: this.organizationSearchTreeOptions,
|
||||
props: {
|
||||
label: 'label',
|
||||
value: 'id',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const transportTypeOptions = [
|
||||
export const transportTypeOptions = [
|
||||
{ label: '公路运输', value: '公路运输' },
|
||||
{ label: '铁路运输', value: '铁路运输' },
|
||||
{ label: '水路运输', value: '水路运输' },
|
||||
@@ -42,6 +42,7 @@ export const waybillStatusOptions = [
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '待执行', value: 'pending' },
|
||||
{ label: '进行中', value: 'running' },
|
||||
{ label: '待回单确认', value: 'pending_return_confirm' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
];
|
||||
@@ -143,6 +144,17 @@ export const contractArchiveStatusOptions = [
|
||||
{ label: '已归档', value: '已归档' },
|
||||
];
|
||||
|
||||
export const hcsSyncStatusOptions = [
|
||||
{ label: '待推送', value: 'unsynced' },
|
||||
{ label: '已推送', value: 'synced' },
|
||||
{ label: '推送失败', value: 'failed' },
|
||||
];
|
||||
|
||||
export const yesNoOptions = [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
];
|
||||
|
||||
export const nodeOptions = [
|
||||
{ label: '接单', value: '接单' },
|
||||
{ label: '到场', value: '到场' },
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
approvalStatusOptions,
|
||||
contractCategoryOptions,
|
||||
createCrudOption,
|
||||
hcsSyncStatusOptions,
|
||||
signTypeOptions,
|
||||
} from './common';
|
||||
|
||||
const listDicFormatter = res => {
|
||||
const data = res?.data || res;
|
||||
if (Array.isArray(data)) return data;
|
||||
if (Array.isArray(data?.records)) return data.records;
|
||||
if (Array.isArray(data?.data)) return data.data;
|
||||
if (Array.isArray(data?.data?.records)) return data.data.records;
|
||||
return [];
|
||||
};
|
||||
|
||||
const projectNameDicFormatter = res => {
|
||||
const list = listDicFormatter(res);
|
||||
return list.filter(item => item && item.projectName && String(item.projectName).trim());
|
||||
};
|
||||
|
||||
export const changeTypeOptions = [
|
||||
{ label: '合同信息变更', value: '合同信息变更' },
|
||||
{ label: '结算规则变更', value: '结算规则变更' },
|
||||
];
|
||||
|
||||
export const config = {
|
||||
title: '合同变更',
|
||||
permission: 'contract_change',
|
||||
statusProp: 'approvalStatus',
|
||||
statusTextProp: 'approvalStatusName',
|
||||
deleteStatus: ['draft', 'withdrawn', 'rejected'],
|
||||
editStatus: ['draft', 'withdrawn', 'rejected'],
|
||||
viewStatus: ['approved'],
|
||||
operations: [
|
||||
{
|
||||
action: 'submitApproval',
|
||||
label: '提交',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['draft'],
|
||||
permission: 'contract_change_submit',
|
||||
},
|
||||
{
|
||||
action: 'withdraw',
|
||||
label: '撤回',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'contract_change_withdraw',
|
||||
},
|
||||
{
|
||||
action: 'mkAudit',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'rejected', 'approved'],
|
||||
permission: 'contract_change_approve',
|
||||
mkOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'approve',
|
||||
label: '通过',
|
||||
type: 'success',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'contract_change_approve',
|
||||
localOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
label: '驳回',
|
||||
type: 'warning',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'contract_change_reject',
|
||||
localOnly: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const option = createCrudOption([
|
||||
{
|
||||
label: '合同编号',
|
||||
prop: 'contractNo',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 20,
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '合同名称',
|
||||
prop: 'contractName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 30,
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
label: '所属项目',
|
||||
prop: 'projectName',
|
||||
search: true,
|
||||
searchType: 'select',
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 60,
|
||||
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999&contractSelectable=true',
|
||||
dicFormatter: projectNameDicFormatter,
|
||||
props: {
|
||||
label: 'projectName',
|
||||
value: 'projectName',
|
||||
},
|
||||
filterable: true,
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'organizationName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 10,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
label: '合同类别',
|
||||
prop: 'contractCategory',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 70,
|
||||
dicData: contractCategoryOptions,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '签约类型',
|
||||
prop: 'signType',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 50,
|
||||
dicData: signTypeOptions,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '甲方',
|
||||
prop: 'partyA',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '乙方',
|
||||
prop: 'partyB',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '开始日期',
|
||||
prop: 'startDate',
|
||||
type: 'date',
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '结束日期',
|
||||
prop: 'endDate',
|
||||
type: 'date',
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '临时效力起',
|
||||
prop: 'temporaryStartDate',
|
||||
type: 'date',
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '临时效力止',
|
||||
prop: 'temporaryEndDate',
|
||||
type: 'date',
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '经办人',
|
||||
prop: 'handlerUserName',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
type: 'datetime',
|
||||
minWidth: 170,
|
||||
},
|
||||
{
|
||||
label: '审核状态',
|
||||
prop: 'approvalStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 40,
|
||||
dicData: approvalStatusOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
prop: 'currentNode',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '当前处理人',
|
||||
prop: 'currentProcessor',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '同步状态',
|
||||
prop: 'syncStatus',
|
||||
type: 'select',
|
||||
dicData: hcsSyncStatusOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
]);
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
createCrudOption,
|
||||
effectiveTypeOptions,
|
||||
nonNegativeRule,
|
||||
hcsSyncStatusOptions,
|
||||
selectRule,
|
||||
signTypeOptions,
|
||||
textRule,
|
||||
yesNoOptions,
|
||||
} from './common';
|
||||
|
||||
const contractSettlementModeOptions = [
|
||||
@@ -606,6 +608,59 @@ export const option = createCrudOption([
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '合规状态',
|
||||
prop: 'hcsSyncStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchOrder: 6,
|
||||
searchPlaceholder: '请选择',
|
||||
slot: true,
|
||||
dicData: hcsSyncStatusOptions,
|
||||
minWidth: 110,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '客商风险',
|
||||
prop: 'customerRisk',
|
||||
slot: true,
|
||||
order: 189,
|
||||
minWidth: 150,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '金蝶合同ID',
|
||||
prop: 'kingdeeId',
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步金蝶',
|
||||
prop: 'kingdeeSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 7,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步合规',
|
||||
prop: 'hcsSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 8,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
prop: 'currentNode',
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
businessModeOptions,
|
||||
businessTypeOptions,
|
||||
createCrudOption,
|
||||
hcsSyncStatusOptions,
|
||||
nonNegativeRule,
|
||||
projectApprovalStatusOptions,
|
||||
projectSourceOptions,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
selectRule,
|
||||
settlementModeOptions,
|
||||
textRule,
|
||||
yesNoOptions,
|
||||
} from './common';
|
||||
|
||||
const projectEffectiveTypeOptions = [
|
||||
@@ -406,6 +408,57 @@ export const option = createCrudOption([
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '合规状态',
|
||||
prop: 'hcsSyncStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 1,
|
||||
slot: true,
|
||||
dicData: hcsSyncStatusOptions,
|
||||
minWidth: 110,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '金蝶项目ID',
|
||||
prop: 'kingdeeId',
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '金蝶项目编号',
|
||||
prop: 'kingdeeNumber',
|
||||
minWidth: 130,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步金蝶',
|
||||
prop: 'kingdeeSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 2,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '是否同步合规',
|
||||
prop: 'hcsSynced',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 3,
|
||||
dicData: yesNoOptions,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '生效类型',
|
||||
prop: 'effectiveType',
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
approvalStatusOptions,
|
||||
createCrudOption,
|
||||
hcsSyncStatusOptions,
|
||||
projectTypeOptions,
|
||||
} from './common';
|
||||
|
||||
export const changeTypeOptions = [
|
||||
{ label: '项目变更', value: '项目变更' },
|
||||
{ label: '项目备案调整', value: '项目备案调整' },
|
||||
];
|
||||
|
||||
export const config = {
|
||||
title: '项目变更',
|
||||
permission: 'project_change',
|
||||
statusProp: 'approvalStatus',
|
||||
statusTextProp: 'approvalStatusName',
|
||||
deleteStatus: ['draft', 'withdrawn', 'rejected'],
|
||||
editStatus: ['draft', 'withdrawn', 'rejected'],
|
||||
viewStatus: ['approved', 'rejected', 'withdrawn'],
|
||||
operations: [
|
||||
{
|
||||
action: 'submitApproval',
|
||||
label: '提交',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['draft'],
|
||||
permission: 'project_change_submit',
|
||||
},
|
||||
{
|
||||
action: 'withdraw',
|
||||
label: '撤回',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_withdraw',
|
||||
},
|
||||
{
|
||||
action: 'mkAudit',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_approve',
|
||||
mkOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'approve',
|
||||
label: '通过',
|
||||
type: 'success',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_approve',
|
||||
localOnly: true,
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
label: '驳回',
|
||||
type: 'warning',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing'],
|
||||
permission: 'project_change_reject',
|
||||
localOnly: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const option = createCrudOption([
|
||||
{
|
||||
label: '项目编号',
|
||||
prop: 'projectCode',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 70,
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '项目名称',
|
||||
prop: 'projectName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 60,
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
label: '变更类型',
|
||||
prop: 'changeType',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 10,
|
||||
dicData: changeTypeOptions,
|
||||
minWidth: 130,
|
||||
},
|
||||
{
|
||||
label: '项目类型',
|
||||
prop: 'projectType',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 20,
|
||||
dicData: projectTypeOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '客户名称',
|
||||
prop: 'customerNames',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '下游承运商',
|
||||
prop: 'carrierNames',
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
label: '承办部门',
|
||||
prop: 'undertakeDeptName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 50,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
label: '项目负责人',
|
||||
prop: 'principalUserName',
|
||||
search: true,
|
||||
searchPlaceholder: '请输入',
|
||||
searchOrder: 40,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '审批状态',
|
||||
prop: 'approvalStatus',
|
||||
type: 'select',
|
||||
search: true,
|
||||
searchPlaceholder: '请选择',
|
||||
searchOrder: 30,
|
||||
dicData: approvalStatusOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
{
|
||||
label: '当前节点',
|
||||
prop: 'currentNode',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '当前处理人',
|
||||
prop: 'currentProcessor',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '审核通过时间',
|
||||
prop: 'approvedTime',
|
||||
type: 'datetime',
|
||||
minWidth: 170,
|
||||
},
|
||||
{
|
||||
label: '同步状态',
|
||||
prop: 'syncStatus',
|
||||
type: 'select',
|
||||
dicData: hcsSyncStatusOptions,
|
||||
minWidth: 110,
|
||||
},
|
||||
]);
|
||||
@@ -471,7 +471,7 @@ export const config = {
|
||||
endDateRange: ['endDateStart', 'endDateEnd'],
|
||||
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
||||
},
|
||||
actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete'],
|
||||
actions: ['copy', 'cancel', 'reassign', 'complete', 'batchComplete', 'confirmReturn'],
|
||||
detailButton: true,
|
||||
statusProp: 'businessStatus',
|
||||
statusTextProp: 'businessStatusName',
|
||||
@@ -483,6 +483,8 @@ export const config = {
|
||||
row.requireAccept === true ||
|
||||
row.requireAccept === 1 ||
|
||||
requiresDriverAcceptConfirmation(row);
|
||||
// 待回单确认由末位打卡产生,司机必然已接单
|
||||
if (status === 'pending_return_confirm') return '待回单确认';
|
||||
// 需司机接单且尚未接单 → 待执行(含库中仍为 running 的历史数据展示校正)
|
||||
if (needAccept && !hasDriverAcceptRecord(row) && !terminalStatuses.includes(status)) {
|
||||
return '待执行';
|
||||
@@ -504,7 +506,7 @@ export const config = {
|
||||
return !isDriverRejectedWaybill(row);
|
||||
},
|
||||
canCancel(row) {
|
||||
// 进行中的运单不允许取消
|
||||
// 进行中的运单不允许取消;待回单确认仍可取消(与页面 canCancel 一致)
|
||||
return !isInProgressStatus(row, row.businessStatusName);
|
||||
},
|
||||
deleteStatus: ['draft'],
|
||||
|
||||
@@ -169,7 +169,7 @@ export const paymentApplicationOption = ({
|
||||
{ label: '当前节点', prop: 'currentNode', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 120, formatter: textFormatter },
|
||||
{ label: '金蝶单据号', prop: 'kingdeeBillNo', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', minWidth: 130, formatter: textFormatter },
|
||||
{ label: '金蝶单据状态', prop: 'kingdeeStatusName', slot: true, minWidth: 130 },
|
||||
{ label: '金蝶锁状态', prop: 'kingdeeLockStatus', slot: true, minWidth: 150 },
|
||||
{ label: '附件同步', prop: 'attachmentSyncStatusName', minWidth: 100, formatter: textFormatter },
|
||||
],
|
||||
|
||||
@@ -99,7 +99,8 @@ const buildTableColumns = () =>
|
||||
}
|
||||
// link / status 标记的列统一走单元格插槽
|
||||
if (column.link || column.status) extra.slot = true;
|
||||
if (!extra.formatter) extra.formatter = textFormatter;
|
||||
// 插槽列不挂兜底 formatter:avue 中 formatter 会覆盖单元格插槽渲染
|
||||
if (!extra.slot && !extra.formatter) extra.formatter = textFormatter;
|
||||
return { ...column, ...extra };
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ export const formalSettlementFormFields = [
|
||||
{ label: '收款方', prop: 'payeeName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementTypeName', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '业务模式', prop: 'businessModel', type: 'select', optionsKey: 'businessModelOptions' },
|
||||
{ label: '运输方式', prop: 'transportType', type: 'select', optionsKey: 'transportTypeOptions' },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date', required: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', required: true },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
@@ -25,6 +27,8 @@ export const createFormalSettlementForm = () => ({
|
||||
settlementType: '',
|
||||
sourcePreSettlementIds: [],
|
||||
sourceDetailIds: [],
|
||||
businessModel: '',
|
||||
transportType: '',
|
||||
exchangeRateDate: '',
|
||||
exchangeRate: 1,
|
||||
createUserName: '',
|
||||
|
||||
@@ -8,6 +8,8 @@ export const formalSettlementTableColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 150 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 170 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 145, money: true },
|
||||
{ label: '业务模式', prop: 'businessModelName', minWidth: 120 },
|
||||
{ label: '运输方式', prop: 'transportTypeName', minWidth: 120 },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 155, money: true },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110 },
|
||||
{ label: '申请付款金额(含预付)', prop: 'appliedPaymentAmount', minWidth: 185, money: true },
|
||||
|
||||
@@ -12,6 +12,8 @@ export const preSettlementFormFields = [
|
||||
{ label: '付款方', prop: 'payerName', readonly: true },
|
||||
{ label: '结算类型', prop: 'settlementType', readonly: true },
|
||||
{ label: '结算金额', prop: 'settlementAmount', readonly: true, money: true },
|
||||
{ label: '业务模式', prop: 'businessModel', type: 'select', optionsKey: 'businessModelOptions' },
|
||||
{ label: '运输方式', prop: 'transportType', type: 'select', optionsKey: 'transportTypeOptions' },
|
||||
{ label: '汇率日期', prop: 'exchangeRateDate', type: 'date' },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', type: 'number', class: 'exchange-rate-input' },
|
||||
{ label: '本位币合计', prop: 'localSettlementAmount', readonly: true, money: true },
|
||||
@@ -64,6 +66,8 @@ export const emptyPreSettlementForm = () => ({
|
||||
settlementType: 'payable',
|
||||
currency: 'RMB',
|
||||
settlementAmount: 0,
|
||||
businessModel: '',
|
||||
transportType: '',
|
||||
localCurrency: 'RMB',
|
||||
localSettlementAmount: 0,
|
||||
exchangeRateDate: '',
|
||||
|
||||
@@ -8,6 +8,8 @@ export const preSettlementTableColumns = [
|
||||
{ label: '合同编号', prop: 'contractNo', minWidth: 140 },
|
||||
{ label: '合同名称', prop: 'contractName', minWidth: 160 },
|
||||
{ label: '原币结算金额', prop: 'settlementAmount', minWidth: 140, money: true, precision: 2 },
|
||||
{ label: '业务模式', prop: 'businessModelName', minWidth: 120 },
|
||||
{ label: '运输方式', prop: 'transportTypeName', minWidth: 120 },
|
||||
{ label: '本位币结算金额', prop: 'localSettlementAmount', minWidth: 150, money: true, precision: 2 },
|
||||
{ label: '结算汇率', prop: 'exchangeRate', minWidth: 110, precision: 2 },
|
||||
{
|
||||
|
||||
@@ -14,6 +14,11 @@ const validatePhone = (rule, value, callback) => {
|
||||
callback(new Error('请输入手机号'));
|
||||
return;
|
||||
}
|
||||
// 详情回显为脱敏手机号(含 *)时放行,未修改则提交时剔除该字段
|
||||
if (phone.includes('*')) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!/^1\d{10}$/.test(phone)) {
|
||||
callback(new Error('请输入正确的手机号'));
|
||||
return;
|
||||
@@ -121,6 +126,62 @@ export default {
|
||||
order: 60,
|
||||
rules: [{ required: true, message: '请选择小程序身份', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptId',
|
||||
formslot: true,
|
||||
hide: true,
|
||||
span: 24,
|
||||
order: 55,
|
||||
// 所属组织可为空:为空表示未分配组织,账号的数据权限为空集
|
||||
},
|
||||
{
|
||||
label: '所属组织',
|
||||
prop: 'deptName',
|
||||
minWidth: 180,
|
||||
overHidden: true,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
viewDisplay: false,
|
||||
order: 54,
|
||||
},
|
||||
{
|
||||
label: '数据权限范围',
|
||||
prop: 'dataScopeRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 53,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '仅所属组织', value: 1 },
|
||||
{ label: '全部组织', value: 2 },
|
||||
{ label: '自定义', value: 3 },
|
||||
],
|
||||
rules: [{ required: true, message: '请选择数据权限范围', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据层级范围',
|
||||
prop: 'dataLevelRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 52,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '包含下级', value: 1 },
|
||||
{ label: '仅本级', value: 2 },
|
||||
],
|
||||
rules: [{ required: true, message: '请选择数据层级范围', trigger: 'change' }],
|
||||
},
|
||||
{
|
||||
label: '数据权限',
|
||||
prop: 'dataScopeDeptIds',
|
||||
formslot: true,
|
||||
span: 24,
|
||||
hide: true,
|
||||
order: 51,
|
||||
},
|
||||
{
|
||||
label: '性别',
|
||||
prop: 'sex',
|
||||
@@ -142,10 +203,9 @@ export default {
|
||||
overHidden: true,
|
||||
hide: true,
|
||||
addDisplay: false,
|
||||
editDisplay: true,
|
||||
editDisplay: false,
|
||||
viewDisplay: true,
|
||||
order: 40,
|
||||
placeholder: '可清空以解绑微信',
|
||||
},
|
||||
{
|
||||
label: '状态',
|
||||
|
||||
@@ -315,29 +315,6 @@ export const userOption = safe => {
|
||||
{ label: '承运商', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据权限范围',
|
||||
prop: 'dataScopeRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '仅所属组织', value: 1 },
|
||||
{ label: '全部组织', value: 2 },
|
||||
{ label: '自定义', value: 3 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据层级范围',
|
||||
prop: 'dataLevelRange',
|
||||
type: 'radio',
|
||||
span: 24,
|
||||
value: 1,
|
||||
dicData: [
|
||||
{ label: '包含下级', value: 1 },
|
||||
{ label: '仅本级', value: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
prop: 'remark',
|
||||
|
||||
@@ -48,6 +48,7 @@ export const option = {
|
||||
searchOrder: 1,
|
||||
searchslot: true,
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
@@ -55,6 +56,8 @@ export const option = {
|
||||
prop: 'useDepartment',
|
||||
search: true,
|
||||
searchOrder: 3,
|
||||
slot: true,
|
||||
overHidden: true,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -95,6 +95,17 @@ export default [
|
||||
bizType: 'project-apply',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/project-change/public-view',
|
||||
name: '查看项目变更信息',
|
||||
component: () => import('@/views/business/project-apply-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'project-change',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/contract-manage/public-view',
|
||||
name: '查看合同信息',
|
||||
@@ -106,6 +117,17 @@ export default [
|
||||
bizType: 'contract-manage',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/business/contract-change/public-view',
|
||||
name: '查看合同变更信息',
|
||||
component: () => import('@/views/business/contract-change-public-view.vue'),
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
isTab: false,
|
||||
isAuth: false,
|
||||
bizType: 'contract-change',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/settlement/pre-settlement/public-view',
|
||||
name: '查看预付结算单信息',
|
||||
|
||||
@@ -104,6 +104,22 @@ export default [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// 兼容旧菜单路径:按 detailNo 解析应付/应收后跳转到拆分后的列表页
|
||||
path: '/settlement/formal-settlement',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '正式结算单兼容跳转',
|
||||
meta: {
|
||||
keepAlive: false,
|
||||
activeMenu: '/settlement/payment-settlement',
|
||||
},
|
||||
component: loadView(() => import('@/views/settlement/formal-settlement-redirect.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/settlement/formal-settlement/form',
|
||||
component: Layout,
|
||||
@@ -198,6 +214,18 @@ export default [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/project-change/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '项目变更表单',
|
||||
meta: { keepAlive: false, activeMenu: '/business/project-change' },
|
||||
component: loadView(() => import('@/views/business/project-apply.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/waybill-manage/form',
|
||||
component: Layout,
|
||||
@@ -342,6 +370,18 @@ export default [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/business/contract-change/form',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: '合同变更表单',
|
||||
meta: { keepAlive: false, activeMenu: '/business/contract-change' },
|
||||
component: loadView(() => import('@/views/business/contract-manage-change.vue')),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/wel',
|
||||
component: () =>
|
||||
|
||||
@@ -622,7 +622,7 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
// 合同管理新增/编辑弹窗(business-crud-page 封装 → avue-crud,弹窗带 avue-crud__dialog class)
|
||||
// 合同管理新增/编辑弹窗(avue-crud,弹窗带 avue-crud__dialog class)
|
||||
// 复刻 waybill-manage 方案:接近全屏、压过库内置 .avue-dialog 的 calc(100% - 200px),
|
||||
// 并解开 body 的 200px 上限、抵消全局 footer 的 -30px 上移
|
||||
.contract-manage-dialog.el-dialog {
|
||||
|
||||
@@ -26,6 +26,35 @@ const uniqueOrgNames = (values = []) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
// 候选司机取数上限:超出后只能靠关键字缩小范围
|
||||
const DRIVER_CANDIDATE_MAX_SIZE = 200;
|
||||
|
||||
/**
|
||||
* 派单候选司机:租户内的司机档案,按岗位等条件过滤。
|
||||
*
|
||||
* 与组织、承运商都无关——司机档案与组织树、承运商之间没有外键,`organization_name`
|
||||
* 只是自由文本,用它做匹配会让小程序自动建档的司机永远进不了候选。
|
||||
*/
|
||||
export const fetchDriverCandidates = async (query = {}) => {
|
||||
const params = { ...query };
|
||||
const size = Math.min(Math.max(Number(params.size) || 50, 50), DRIVER_CANDIDATE_MAX_SIZE);
|
||||
delete params.size;
|
||||
const res = await getDriverList(1, size, params);
|
||||
return extractRecords(res).slice(0, size);
|
||||
};
|
||||
|
||||
/**
|
||||
* 司机候选项的展示文本:姓名(手机尾号后 4 位)。
|
||||
*
|
||||
* 只用于下拉渲染。写回表单的仍是姓名本身,别把带尾号的文本当值用。
|
||||
*/
|
||||
export const driverOptionText = (item = {}) => {
|
||||
const name = normalizeOrgName(item.driverName || item.name);
|
||||
const mobile = String(item.mobile || item.phone || item.driverPhone || '').trim();
|
||||
if (!name) return '';
|
||||
return mobile.length >= 4 ? `${name}(${mobile.slice(-4)})` : name;
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
|
||||
*/
|
||||
@@ -72,13 +101,6 @@ export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName }
|
||||
return orgNames;
|
||||
};
|
||||
|
||||
export const clearCarrierOrganizationCache = (carrier = {}) => {
|
||||
const id = String(carrier.carrierId || '').trim();
|
||||
const name = normalizeOrgName(carrier.carrierName);
|
||||
if (id) orgNameCache.delete(id);
|
||||
if (name) orgNameCache.delete(name);
|
||||
};
|
||||
|
||||
export const matchOrganizationName = (value, orgNames = []) => {
|
||||
const text = normalizeOrgName(value);
|
||||
if (!text || !orgNames.length) return false;
|
||||
@@ -90,34 +112,9 @@ const filterByOrganizations = (records = [], orgNames = []) => {
|
||||
return records.filter(item => matchOrganizationName(item.organizationName, orgNames));
|
||||
};
|
||||
|
||||
/**
|
||||
* 按承运商联系人所属组织筛选司机。
|
||||
* 未选择承运商时返回空列表(需先选承运商)。
|
||||
*/
|
||||
export const fetchDriversByCarrierOrganizations = async (
|
||||
query = {},
|
||||
{ carrierId, carrierName } = {}
|
||||
) => {
|
||||
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
|
||||
return [];
|
||||
}
|
||||
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
|
||||
if (!orgNames.length) return [];
|
||||
|
||||
const size = Math.max(Number(query.size) || 50, 50);
|
||||
const params = { ...query };
|
||||
delete params.size;
|
||||
// 单组织时用后端模糊条件缩小范围;多组织再前端精确过滤
|
||||
if (orgNames.length === 1) {
|
||||
params.organizationName = orgNames[0];
|
||||
}
|
||||
const res = await getDriverList(1, Math.min(size * 5, 200), params);
|
||||
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
|
||||
};
|
||||
|
||||
/**
|
||||
* 按承运商联系人所属组织筛选车辆。
|
||||
* 未选择承运商时返回空列表。
|
||||
* 未选择承运商时返回空列表(需先选承运商)。
|
||||
*/
|
||||
export const fetchVehiclesByCarrierOrganizations = async (
|
||||
query = {},
|
||||
|
||||
@@ -106,6 +106,9 @@ const CERT_SNAPSHOT_GROUPS = {
|
||||
fields: [
|
||||
{ key: 'realName', label: '真实姓名' },
|
||||
{ key: 'idCardNo', label: '身份证号' },
|
||||
{ key: 'birthday', label: '出生年月', type: TYPE_DATE },
|
||||
{ key: 'addressRegion', label: '住址' },
|
||||
{ key: 'address', label: '详细地址' },
|
||||
{ key: 'idCardFrontUrl', label: '身份证正面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardBackUrl', label: '身份证反面照', type: TYPE_IMAGE },
|
||||
{ key: 'idCardValidStart', label: '身份证有效期起', type: TYPE_DATE },
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'nprogress/nprogress.css';
|
||||
const excelPattern = /\.(xls|xlsx)$/;
|
||||
|
||||
/** 是否为浏览器 Blob(含跨 realm 的 Blob-like) */
|
||||
const isBlobLike = value =>
|
||||
export const isBlobLike = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.size === 'number' &&
|
||||
@@ -15,14 +15,14 @@ const isBlobLike = value =>
|
||||
(typeof value.text === 'function' || typeof value.arrayBuffer === 'function');
|
||||
|
||||
/** axios 拦截器已把 JSON Blob 解析成的业务对象 */
|
||||
const isBusinessJson = value =>
|
||||
export const isBusinessJson = value =>
|
||||
value != null &&
|
||||
typeof value === 'object' &&
|
||||
!isBlobLike(value) &&
|
||||
!(value instanceof ArrayBuffer) &&
|
||||
('code' in value || 'success' in value || 'msg' in value);
|
||||
|
||||
const isExcelResponse = res => {
|
||||
export const isExcelResponse = res => {
|
||||
// 已是业务 JSON(导入成功/失败提示)时,绝不能当失败明细 Excel 处理
|
||||
if (isBusinessJson(res?.data)) {
|
||||
return false;
|
||||
@@ -54,7 +54,7 @@ const isExcelResponse = res => {
|
||||
* 提前解析成普通对象;此处需同时兼容 Blob / 字符串 / 已解析对象,避免再调
|
||||
* `blob.text()` 时出现「blob.text is not a function」。
|
||||
*/
|
||||
const parseBlobJson = async data => {
|
||||
export const parseBlobJson = async data => {
|
||||
if (data == null) {
|
||||
return {};
|
||||
}
|
||||
@@ -88,7 +88,7 @@ const parseBlobJson = async data => {
|
||||
*
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
export const resolveImportErrorMessage = async error => {
|
||||
const payload = error?.response?.data ?? error?.data;
|
||||
if (isBusinessJson(payload) && payload.msg) {
|
||||
return payload.msg;
|
||||
@@ -109,6 +109,26 @@ const resolveErrorMessage = async error => {
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一消费导入接口响应:成功 JSON / 失败 Excel 流。
|
||||
* 兼容 axios 已把 JSON Blob 解析成对象的情况,禁止再对普通对象调 .text()。
|
||||
*/
|
||||
export const consumeImportResponse = async (res, options = {}) => {
|
||||
const { businessName = '导入', dayjsFormat } = options;
|
||||
if (isExcelResponse(res)) {
|
||||
return {
|
||||
kind: 'excel',
|
||||
blob: res?.data,
|
||||
fileName: `${businessName}导入失败明细${dayjsFormat || ''}.xlsx`,
|
||||
};
|
||||
}
|
||||
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
|
||||
if (Number(result?.code) !== 200) {
|
||||
throw new Error(result?.msg || '导入失败');
|
||||
}
|
||||
return { kind: 'success', result };
|
||||
};
|
||||
|
||||
/**
|
||||
* 加工后端返回的导入失败明细 Excel。
|
||||
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
|
||||
@@ -235,7 +255,7 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
vm.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
notifyUploadError(option, error);
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 金蝶单据状态公共映射
|
||||
*/
|
||||
|
||||
/** 金蝶单据状态 → el-tag 颜色类型(failed 状态列表页悬浮展示失败原因) */
|
||||
export const KINGDEE_STATUS_TAG_TYPES = {
|
||||
synced: 'success',
|
||||
paid: 'success',
|
||||
invoiced: 'success',
|
||||
paying: 'warning',
|
||||
failed: 'danger',
|
||||
};
|
||||
|
||||
/** 金蝶单据状态 → el-tag 颜色类型,未映射状态返回 info */
|
||||
export const kingdeeStatusType = status => KINGDEE_STATUS_TAG_TYPES[status] || 'info';
|
||||
@@ -15,12 +15,24 @@ export const MK_BIZ = {
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
approved: ['approved', 'change_approved'],
|
||||
},
|
||||
'project-change': {
|
||||
dictName: '提交项目变更审核流',
|
||||
subjectPrefix: '项目变更审批',
|
||||
rejected: ['rejected', 'withdrawn'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'contract-manage': {
|
||||
dictName: '提交合同审核流',
|
||||
subjectPrefix: '合同审批',
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
approved: ['approved', 'change_approved'],
|
||||
},
|
||||
'contract-change': {
|
||||
dictName: '提交合同变更审核流',
|
||||
subjectPrefix: '合同变更审批',
|
||||
rejected: ['rejected', 'withdrawn'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'pre-settlement': {
|
||||
dictName: '提交预结算审核流',
|
||||
subjectPrefix: '预付结算审批',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 客商/合同风险标签公共展示逻辑(黑名单=金蝶黑名单 A801-5 命中,与后端 HcsCreditCheckSupport 口径一致):
|
||||
* 等级与黑名单组合展示、等级在前;内部组织客商默认无风险
|
||||
*/
|
||||
|
||||
/** 金蝶黑名单命中判定(客商档案行:失信类别/列入事由任一非空) */
|
||||
export function isKingdeeBlack(category, reason) {
|
||||
return !!(category || reason);
|
||||
}
|
||||
|
||||
/** 合同行黑名单判定(推送链路按金蝶命中口径落库 kingdeeBlackStatus=yes) */
|
||||
export function isContractBlack(blackStatus) {
|
||||
return String(blackStatus || '').toLowerCase() === 'yes';
|
||||
}
|
||||
|
||||
/** 风险标签文案:riskLevel=C101-1 等级(0/1/2);isBlack=金蝶黑名单命中;customerKind=internal 显示"无风险" */
|
||||
export function resolveRiskTagName(riskLevel, isBlack, customerKind) {
|
||||
const levelName = { 2: '高风险', 1: '中风险', 0: '低风险' }[riskLevel] || '';
|
||||
if (isBlack) return levelName ? `${levelName}、黑名单` : '黑名单';
|
||||
if (levelName) return levelName;
|
||||
if (customerKind === 'internal') return '无风险';
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 是否红色风险标签(黑名单命中或高风险) */
|
||||
export function isRiskyTag(riskLevel, isBlack) {
|
||||
return isBlack || riskLevel === '2';
|
||||
}
|
||||
@@ -3,10 +3,27 @@ export function isFreightFeeName(name) {
|
||||
return text.includes('运费') || text.includes('运输费');
|
||||
}
|
||||
|
||||
function parseBillingRules(row = {}) {
|
||||
const rule = row.billingRule || {};
|
||||
const nested = Array.isArray(rule.rules) ? rule.rules : [];
|
||||
if (nested.length) return nested;
|
||||
const raw = row.billingRulesJson ?? row.billingRules;
|
||||
if (Array.isArray(raw)) return raw.filter(item => item && typeof item === 'object');
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter(item => item && typeof item === 'object') : [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** 运输单价对应的运费计费类型。 */
|
||||
export function freightBillingType(row = {}) {
|
||||
const rule = row.billingRule || {};
|
||||
const rules = Array.isArray(rule.rules) ? rule.rules : [];
|
||||
const rules = parseBillingRules(row);
|
||||
const freightRule = rules.find(
|
||||
item => isFreightFeeName(item?.feeItem) || isFreightFeeName(item?.feeType)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/** 审批通过、驳回等审批流转类变更内容,不在结算单变更记录中展示。 */
|
||||
export function isApprovalSettlementChange(content) {
|
||||
const text = String(content || '').trim();
|
||||
if (!text) return false;
|
||||
return text.includes('审批通过') || text.includes('审批驳回') || text === '已驳回';
|
||||
}
|
||||
+190
-100
@@ -50,7 +50,7 @@
|
||||
title="角色权限配置"
|
||||
append-to-body
|
||||
v-model="box"
|
||||
width="345px"
|
||||
width="960px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-tabs type="border-card">
|
||||
@@ -100,41 +100,26 @@
|
||||
align="middle"
|
||||
style="margin-bottom: 12px; background: #f5f7fa; padding: 6px 10px; border-radius: 4px"
|
||||
>
|
||||
<span style="display: inline-flex; align-items: center">
|
||||
<el-switch
|
||||
v-model="dataScopeLinked"
|
||||
active-text="节点联动"
|
||||
size="small"
|
||||
@change="handleLinkedChange('treeDataScope')"
|
||||
/>
|
||||
<el-tooltip content="开启后勾选父节点会自动勾选所有子节点,关闭则可独立勾选任意节点" placement="top">
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleSelectAll('treeDataScope', dataScopeGrantList)"
|
||||
>全选</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
plain
|
||||
@click="handleInvertSelect('treeDataScope', dataScopeGrantList, dataScopeLinked)"
|
||||
>反选</el-button
|
||||
>
|
||||
</el-button-group>
|
||||
<span style="color: #606266; font-size: 13px"
|
||||
>勾选多个菜单后点击「设置」,可批量配置数据权限;设置后菜单旁展示当前范围</span
|
||||
>
|
||||
<el-button type="primary" size="small" @click="openDataScopeSetting">设置</el-button>
|
||||
</el-row>
|
||||
<el-tree
|
||||
:data="dataScopeGrantList"
|
||||
:data="roleDataScopeMenuTree"
|
||||
show-checkbox
|
||||
:check-strictly="!dataScopeLinked"
|
||||
node-key="id"
|
||||
ref="treeDataScope"
|
||||
:default-checked-keys="dataScopeTreeObj"
|
||||
ref="treeRoleDataScope"
|
||||
:props="props"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span class="role-data-scope-node">
|
||||
<span>{{ data.title }}</span>
|
||||
<span v-if="getMenuScopeLabel(data.id)" class="role-data-scope-tag"
|
||||
>【{{ getMenuScopeLabel(data.id) }}】</span
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="接口权限">
|
||||
@@ -188,6 +173,45 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
title="数据权限设置"
|
||||
append-to-body
|
||||
v-model="dataScopeSettingBox"
|
||||
width="560px"
|
||||
class="role-data-scope-setting-dialog"
|
||||
>
|
||||
<el-form label-position="right" label-width="auto">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="dataScopeSetting.roleName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据权限">
|
||||
<el-radio-group v-model="dataScopeSetting.scopeType">
|
||||
<el-radio :value="1">所在部门及下级</el-radio>
|
||||
<el-radio :value="2">全部组织</el-radio>
|
||||
<el-radio :value="3">本人创建</el-radio>
|
||||
<el-radio :value="4">自定义组织</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="Number(dataScopeSetting.scopeType) === 4" label="组织">
|
||||
<el-tree
|
||||
ref="treeCustomDept"
|
||||
:data="deptTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:props="{ label: 'title', children: 'children' }"
|
||||
style="width: 100%; max-height: 320px; overflow: auto; border: 1px solid #ebeef5; padding: 8px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dataScopeSettingBox = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitDataScopeSetting">保存</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -196,37 +220,44 @@ import {
|
||||
add,
|
||||
getList,
|
||||
getRole,
|
||||
getRoleAlias,
|
||||
getRoleTreeById,
|
||||
grant,
|
||||
grantTree,
|
||||
remove,
|
||||
update,
|
||||
} from '@/api/system/role';
|
||||
import { getRoleMenuDataScopeTree, submitRoleMenuDataScope } from '@/api/system/roleDataScope';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { mapGetters } from 'vuex';
|
||||
import website from '@/config/website';
|
||||
import { validatenull } from '@/utils/validate';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
box: false,
|
||||
dataScopeSettingBox: false,
|
||||
props: {
|
||||
label: 'title',
|
||||
value: 'key',
|
||||
},
|
||||
menuGrantList: [],
|
||||
dataScopeGrantList: [],
|
||||
apiScopeGrantList: [],
|
||||
apiGrantList: [],
|
||||
menuTreeObj: [],
|
||||
dataScopeTreeObj: [],
|
||||
apiScopeTreeObj: [],
|
||||
menuLinked: false,
|
||||
dataScopeLinked: false,
|
||||
apiScopeLinked: false,
|
||||
currentRoleIds: [],
|
||||
currentRoleName: '',
|
||||
roleDataScopeMenuTree: [],
|
||||
roleDataScopeMap: {},
|
||||
deptTree: [],
|
||||
dataScopeSetting: {
|
||||
roleName: '',
|
||||
scopeType: 2,
|
||||
menuIds: [],
|
||||
},
|
||||
selectionList: [],
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -309,32 +340,19 @@ export default {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '角色别名',
|
||||
labelTip: '用于权限校验与标注的属性,可手动录入也可点击现有别名快速录入',
|
||||
label: '角色标识',
|
||||
labelTip: '用于权限校验与标注的属性',
|
||||
prop: 'roleAlias',
|
||||
search: true,
|
||||
span: 24,
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入角色别名',
|
||||
message: '请输入角色标识',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '现有别名',
|
||||
labelTip: '当前系统已存在的角色别名,点击可快速录入',
|
||||
prop: 'currentAlias',
|
||||
type: 'radio',
|
||||
dicData: [],
|
||||
props: {
|
||||
label: 'roleName',
|
||||
value: 'roleAlias',
|
||||
},
|
||||
hide: true,
|
||||
span: 24,
|
||||
},
|
||||
{
|
||||
label: '角色排序',
|
||||
prop: 'sort',
|
||||
@@ -378,14 +396,6 @@ export default {
|
||||
return ids;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'form.currentAlias'() {
|
||||
const alias = this.form.currentAlias;
|
||||
if (!validatenull(alias)) {
|
||||
this.form.roleAlias = alias;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initData(roleId) {
|
||||
getRoleTreeById(roleId).then(res => {
|
||||
@@ -452,9 +462,9 @@ export default {
|
||||
},
|
||||
submit() {
|
||||
const menuList = this.$refs.treeMenu.getCheckedKeys();
|
||||
const dataScopeList = this.$refs.treeDataScope.getCheckedKeys();
|
||||
const apiScopeList = this.$refs.treeApiScope.getCheckedKeys();
|
||||
grant(this.currentRoleIds, menuList, dataScopeList, apiScopeList).then(() => {
|
||||
// 数据权限已改为角色菜单配置;传 null 跳过原生 dataScope 更新,避免误清历史配置
|
||||
grant(this.currentRoleIds, menuList, null, apiScopeList).then(() => {
|
||||
this.box = false;
|
||||
this.$message({
|
||||
type: 'success',
|
||||
@@ -465,6 +475,84 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
});
|
||||
},
|
||||
getMenuScopeLabel(menuId) {
|
||||
const scope = this.roleDataScopeMap[String(menuId)];
|
||||
return scope ? scope.scopeTypeName : '';
|
||||
},
|
||||
loadRoleDataScopeTree(roleId) {
|
||||
return getRoleMenuDataScopeTree(roleId).then(res => {
|
||||
const data = res.data.data || {};
|
||||
this.currentRoleName = data.roleName || '';
|
||||
this.roleDataScopeMenuTree = data.menuTree || [];
|
||||
const scopeMap = {};
|
||||
(data.scopes || []).forEach(item => {
|
||||
scopeMap[String(item.menuId)] = item;
|
||||
});
|
||||
this.roleDataScopeMap = scopeMap;
|
||||
});
|
||||
},
|
||||
openDataScopeSetting() {
|
||||
const tree = this.$refs.treeRoleDataScope;
|
||||
if (!tree) {
|
||||
return;
|
||||
}
|
||||
const menuIds = tree.getCheckedKeys();
|
||||
if (!menuIds || menuIds.length === 0) {
|
||||
this.$message.warning('请先勾选要设置的菜单');
|
||||
return;
|
||||
}
|
||||
this.dataScopeSetting = {
|
||||
roleName: this.currentRoleName,
|
||||
scopeType: 2,
|
||||
menuIds,
|
||||
};
|
||||
const loadDept =
|
||||
this.deptTree && this.deptTree.length
|
||||
? Promise.resolve()
|
||||
: getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptTree = res.data.data || [];
|
||||
});
|
||||
loadDept.then(() => {
|
||||
this.dataScopeSettingBox = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.treeCustomDept) {
|
||||
this.$refs.treeCustomDept.setCheckedKeys([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
submitDataScopeSetting() {
|
||||
const roleId = this.currentRoleIds && this.currentRoleIds[0];
|
||||
if (!roleId) {
|
||||
this.$message.warning('角色信息异常,请重新打开权限配置');
|
||||
return;
|
||||
}
|
||||
const scopeType = Number(this.dataScopeSetting.scopeType || 2);
|
||||
let deptIds = [];
|
||||
if (scopeType === 4) {
|
||||
deptIds = this.$refs.treeCustomDept ? this.$refs.treeCustomDept.getCheckedKeys() : [];
|
||||
if (!deptIds.length) {
|
||||
this.$message.warning('自定义组织时请勾选组织');
|
||||
return;
|
||||
}
|
||||
}
|
||||
submitRoleMenuDataScope({
|
||||
roleId,
|
||||
menuIds: this.dataScopeSetting.menuIds,
|
||||
scopeType,
|
||||
deptIds,
|
||||
}).then(() => {
|
||||
this.$message.success('数据权限设置成功');
|
||||
this.dataScopeSettingBox = false;
|
||||
this.loadRoleDataScopeTree(roleId).then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.treeRoleDataScope) {
|
||||
this.$refs.treeRoleDataScope.setCheckedKeys([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
add(this.withTenantId(row)).then(
|
||||
() => {
|
||||
@@ -547,10 +635,6 @@ export default {
|
||||
this.form.parentId = '';
|
||||
}
|
||||
}
|
||||
getRoleAlias().then(res => {
|
||||
const column = this.findColumn(this.option.column, 'currentAlias');
|
||||
column.dicData = res.data.data;
|
||||
});
|
||||
done();
|
||||
},
|
||||
handleRole() {
|
||||
@@ -563,58 +647,52 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
// 保存当前要配置权限的角色ID
|
||||
this.currentRoleIds = this.idsArray;
|
||||
grantTree().then(res => {
|
||||
this.menuGrantList = res.data.data.menu;
|
||||
this.dataScopeGrantList = res.data.data.dataScope;
|
||||
this.apiScopeGrantList = res.data.data.apiScope;
|
||||
getRole(this.ids).then(res => {
|
||||
this.menuTreeObj = res.data.data.menu;
|
||||
this.dataScopeTreeObj = res.data.data.dataScope;
|
||||
this.apiScopeTreeObj = res.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
this.currentRoleName = (this.selectionList[0] && this.selectionList[0].roleName) || '';
|
||||
const roleId = this.currentRoleIds[0];
|
||||
Promise.all([grantTree(), getRole(this.ids), this.loadRoleDataScopeTree(roleId)]).then(results => {
|
||||
const grantRes = results[0];
|
||||
const roleRes = results[1];
|
||||
this.menuGrantList = grantRes.data.data.menu;
|
||||
this.apiScopeGrantList = grantRes.data.data.apiScope;
|
||||
this.menuTreeObj = roleRes.data.data.menu;
|
||||
this.apiScopeTreeObj = roleRes.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleRowRole(row) {
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
// 保存当前要配置权限的角色ID(单个行操作)
|
||||
this.currentRoleIds = [row.id];
|
||||
grantTree().then(res => {
|
||||
this.menuGrantList = res.data.data.menu;
|
||||
this.dataScopeGrantList = res.data.data.dataScope;
|
||||
this.apiScopeGrantList = res.data.data.apiScope;
|
||||
getRole(row.id).then(res => {
|
||||
this.menuTreeObj = res.data.data.menu;
|
||||
this.dataScopeTreeObj = res.data.data.dataScope;
|
||||
this.apiScopeTreeObj = res.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeDataScope', this.dataScopeTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
this.currentRoleName = row.roleName || '';
|
||||
Promise.all([grantTree(), getRole(row.id), this.loadRoleDataScopeTree(row.id)]).then(results => {
|
||||
const grantRes = results[0];
|
||||
const roleRes = results[1];
|
||||
this.menuGrantList = grantRes.data.data.menu;
|
||||
this.apiScopeGrantList = grantRes.data.data.apiScope;
|
||||
this.menuTreeObj = roleRes.data.data.menu;
|
||||
this.apiScopeTreeObj = roleRes.data.data.apiScope;
|
||||
this.box = true;
|
||||
this.$nextTick(() => {
|
||||
this.applyCheckedKeys('treeMenu', this.menuTreeObj);
|
||||
this.applyCheckedKeys('treeApiScope', this.apiScopeTreeObj);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleDialogClose() {
|
||||
// 对话框关闭时清空选择状态和权限树数据
|
||||
this.menuTreeObj = [];
|
||||
this.dataScopeTreeObj = [];
|
||||
this.apiScopeTreeObj = [];
|
||||
this.menuLinked = false;
|
||||
this.dataScopeLinked = false;
|
||||
this.apiScopeLinked = false;
|
||||
this.roleDataScopeMenuTree = [];
|
||||
this.roleDataScopeMap = {};
|
||||
this.currentRoleName = '';
|
||||
this.dataScopeSettingBox = false;
|
||||
this.selectionClear();
|
||||
},
|
||||
handleDelete() {
|
||||
@@ -659,3 +737,15 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-data-scope-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.role-data-scope-tag {
|
||||
color: #409eff;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -280,6 +280,23 @@ export default {
|
||||
.filter(Boolean)[0] || '';
|
||||
},
|
||||
applyDefaultDeptSearch() {
|
||||
// 超管默认不限组织,避免预填当前组织后看不到其他组织/无组织数据。
|
||||
if (this.isAdmin) {
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
if (deptColumn) {
|
||||
deptColumn.searchValue = undefined;
|
||||
}
|
||||
const nextQuery = { ...this.query };
|
||||
delete nextQuery.deptId;
|
||||
delete nextQuery.searchDeptId;
|
||||
this.query = nextQuery;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.crud?.searchForm) {
|
||||
this.$refs.crud.searchForm.searchDeptId = undefined;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
const deptId = this.defaultDeptId();
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
if (!deptId || !deptColumn) return;
|
||||
@@ -298,7 +315,8 @@ export default {
|
||||
if (deptColumn) {
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
}
|
||||
// dicData 异步到位后再回填,避免下拉选项为空时清空已选组织,导致请求丢失 deptId。
|
||||
// 超管不回填默认组织;普通用户在 dicData 到位后再回填,避免下拉清空已选组织。
|
||||
if (this.isAdmin) return;
|
||||
const deptId = String(this.query.deptId || this.defaultDeptId() || '');
|
||||
if (!deptId) return;
|
||||
if (deptColumn) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -205,7 +205,9 @@ export const defaultContractAttachmentTypeOptions = [
|
||||
{ label: '双章归档文件', value: '双章归档文件' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
// 特批附件归口附件(佐证材料等)区上传:高风险/黑名单客商合同必传,后端按 fileType=特批附件 校验并挂特批面板(与后端 HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION 对齐)
|
||||
export const defaultContractSupportingAttachmentTypeOptions = [
|
||||
{ label: '特批附件', value: '特批附件' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
|
||||
@@ -299,6 +301,8 @@ export const resolveContractAttachmentType = (
|
||||
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
|
||||
);
|
||||
if (matchedByLabel) return matchedByLabel.value;
|
||||
// 行已带类型但字典未命中(如历史保存值、字典缺失)时保留原值,避免重开页面时被降级为其他文件
|
||||
if (rawFileType) return aliasedFileType;
|
||||
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||
if (!fileName) return otherType;
|
||||
const matched = list
|
||||
@@ -641,7 +645,7 @@ export default {
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
} else {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
this.uploadDialogType = this.uploadDialogType || this.defaultAttachmentType;
|
||||
}
|
||||
this.$message.success('上传成功');
|
||||
},
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<section class="contract-manage-form__section contract-manage-form__section--panel hcs-contract-panel">
|
||||
<div class="contract-manage-form__attachment-head">
|
||||
<div class="dialog-section-title">合规信息</div>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="canApplySpec"
|
||||
type="warning"
|
||||
plain
|
||||
:loading="specApplying"
|
||||
@click="openSpecDialog"
|
||||
>
|
||||
发起特批
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isSpecProcessing"
|
||||
type="primary"
|
||||
plain
|
||||
@click="confirmSpecResult"
|
||||
>
|
||||
确认特批结果
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
|
||||
<el-descriptions-item label="合规同步状态">
|
||||
<el-tooltip
|
||||
v-if="hcsSyncMsg"
|
||||
placement="top"
|
||||
:content="hcsSyncMsg"
|
||||
>
|
||||
<el-tag :type="syncTagType" class="status-text">{{ syncLabel }}</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else :type="syncTagType" class="status-text">{{ syncLabel }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="合规合同ID">{{
|
||||
contract.hcsFdId || '-'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="客商风险">
|
||||
<template v-if="contract.hcsRiskLevel || contract.kingdeeBlackStatus">
|
||||
<el-tag
|
||||
v-if="isBlacklist"
|
||||
type="danger"
|
||||
class="status-text"
|
||||
>黑名单</el-tag
|
||||
>
|
||||
<span v-if="contract.hcsRiskLevel" class="hcs-contract-panel__risk">{{
|
||||
`风险等级:${contract.hcsRiskLevel}`
|
||||
}}</span>
|
||||
<span v-if="!contract.hcsRiskLevel && !isBlacklist">-</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="特批状态">
|
||||
<el-tag v-if="specLabel" :type="specTagType" class="status-text">{{ specLabel }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="特批说明" :span="2">{{
|
||||
contract.hcsSpecMsg || '-'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-dialog
|
||||
v-model="specDialogVisible"
|
||||
title="发起合同特批"
|
||||
width="640px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="hcs-contract-panel__spec-dialog"
|
||||
@closed="resetSpecDialog"
|
||||
>
|
||||
<el-alert
|
||||
title="高风险客商合同需经集团特批后方可签约推送;请上传特批申请材料及内部审批流程附件。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="hcs-contract-panel__spec-alert"
|
||||
/>
|
||||
<el-upload
|
||||
drag
|
||||
multiple
|
||||
:show-file-list="false"
|
||||
:http-request="uploadMaterial"
|
||||
:on-error="() => uploadFailed()"
|
||||
class="hcs-contract-panel__spec-upload"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">将特批材料拖到此处,或<em>点击上传到合同系统</em></div>
|
||||
</el-upload>
|
||||
<el-table :data="materials" border class="hcs-contract-panel__spec-table">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column prop="name" label="文件名" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="fdId" label="合规附件ID" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link type="danger" @click="materials.splice($index, 1)">删除</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button type="primary" :loading="specApplying" @click="submitSpec">确认发起</el-button>
|
||||
<el-button @click="specDialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { UploadFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
applyContractSpec,
|
||||
confirmContractSpec,
|
||||
uploadAttachment,
|
||||
} from '@/api/business/hcs';
|
||||
|
||||
const SYNC_STATUS_OPTIONS = [
|
||||
{ label: '待推送', value: 'unsynced', type: 'info' },
|
||||
{ label: '已推送', value: 'synced', type: 'success' },
|
||||
{ label: '推送失败', value: 'failed', type: 'danger' },
|
||||
];
|
||||
|
||||
const SPEC_STATUS_OPTIONS = [
|
||||
{ label: '待特批', value: 'need', type: 'warning' },
|
||||
{ label: '特批中', value: 'processing', type: 'warning' },
|
||||
{ label: '特批通过', value: 'approved', type: 'success' },
|
||||
{ label: '特批驳回', value: 'rejected', type: 'danger' },
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'HcsContractPanel',
|
||||
components: { UploadFilled },
|
||||
props: {
|
||||
// 合同详情行(须含 id/hcsSyncStatus/hcsFdId/hcsRiskLevel/kingdeeBlackStatus/hcsSpecStatus 等)
|
||||
contract: { type: Object, required: true },
|
||||
},
|
||||
emits: ['refresh'],
|
||||
data() {
|
||||
return {
|
||||
specDialogVisible: false,
|
||||
specApplying: false,
|
||||
materials: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
syncLabel() {
|
||||
return SYNC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSyncStatus)?.label || '-';
|
||||
},
|
||||
syncTagType() {
|
||||
return SYNC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSyncStatus)?.type || 'info';
|
||||
},
|
||||
hcsSyncMsg() {
|
||||
return this.contract.hcsSyncMsg || '';
|
||||
},
|
||||
isBlacklist() {
|
||||
return String(this.contract.kingdeeBlackStatus || '').toLowerCase() === 'yes';
|
||||
},
|
||||
specLabel() {
|
||||
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.label || '';
|
||||
},
|
||||
specTagType() {
|
||||
return SPEC_STATUS_OPTIONS.find(item => item.value === this.contract.hcsSpecStatus)?.type || 'info';
|
||||
},
|
||||
isSpecProcessing() {
|
||||
return this.contract.hcsSpecStatus === 'processing';
|
||||
},
|
||||
canApplySpec() {
|
||||
return ['need', 'rejected'].includes(this.contract.hcsSpecStatus);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openSpecDialog() {
|
||||
this.specDialogVisible = true;
|
||||
},
|
||||
resetSpecDialog() {
|
||||
this.materials = [];
|
||||
},
|
||||
// 自定义上传:直传合同合规系统(C103-2),成功记录合规附件ID
|
||||
uploadMaterial(options) {
|
||||
const file = options.file;
|
||||
if (file.size / 1024 / 1024 > 50) {
|
||||
this.$message.warning('单文件大小不能超过 50MB');
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('fdFile', file);
|
||||
uploadAttachment(formData)
|
||||
.then(response => {
|
||||
const fdId = response?.data?.data || response?.data;
|
||||
if (!fdId || typeof fdId !== 'string') {
|
||||
this.$message.error(response?.data?.msg || '上传合同系统失败');
|
||||
return;
|
||||
}
|
||||
this.materials.push({ fdId, name: file.name });
|
||||
this.$message.success(`已上传合同系统:${file.name}`);
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error(error?.response?.data?.msg || '上传合同系统失败');
|
||||
});
|
||||
},
|
||||
uploadFailed() {
|
||||
this.$message.error('上传合同系统失败');
|
||||
},
|
||||
submitSpec() {
|
||||
if (!this.materials.length) {
|
||||
this.$message.warning('请先上传特批申请材料');
|
||||
return;
|
||||
}
|
||||
this.specApplying = true;
|
||||
applyContractSpec({
|
||||
contractId: this.contract.id,
|
||||
attachments: this.materials.map(item => ({ fdId: item.fdId, name: item.name })),
|
||||
})
|
||||
.then(response => {
|
||||
if (response?.data?.success === false) {
|
||||
this.$message.error(response?.data?.msg || '特批发起失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(response?.data?.data || '特批发起成功,等待集团审批');
|
||||
this.specDialogVisible = false;
|
||||
this.$emit('refresh');
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error(error?.response?.data?.msg || '特批发起失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.specApplying = false;
|
||||
});
|
||||
},
|
||||
confirmSpecResult() {
|
||||
this.$prompt('请输入集团特批结果(approved=通过 / rejected=驳回)', '确认特批结果', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^(approved|rejected)$/,
|
||||
inputErrorMessage: '请输入 approved 或 rejected',
|
||||
}).then(({ value }) => {
|
||||
confirmContractSpec(this.contract.id, value).then(() => {
|
||||
this.$message.success('特批结果已确认');
|
||||
this.$emit('refresh');
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hcs-contract-panel__risk {
|
||||
margin-left: 8px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.hcs-contract-panel__spec-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hcs-contract-panel__spec-upload {
|
||||
margin-bottom: 12px;
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.hcs-contract-panel__spec-table {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -294,8 +294,8 @@ export default {
|
||||
query: { id: String(customerId), name: '查看客商档案', view: '1' },
|
||||
});
|
||||
},
|
||||
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
waybillStatusName(value) { return ({ pending: '待执行', waiting: '待执行', running: '进行中', processing: '进行中', pending_return_confirm: '待回单确认', completed: '已完成', cancelled: '已取消' })[value] || value || '-'; },
|
||||
waybillStatusType(value) { return ({ pending: 'info', waiting: 'info', running: 'warning', processing: 'warning', pending_return_confirm: 'warning', completed: 'success', cancelled: 'danger' })[value] || 'info'; },
|
||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||
},
|
||||
|
||||
@@ -210,7 +210,11 @@
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleDriverSuggestionSelect"
|
||||
/></el-form-item
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item
|
||||
@@ -239,7 +243,11 @@
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleEscortSuggestionSelect"
|
||||
/></el-form-item
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete></el-form-item
|
||||
></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"
|
||||
><el-form-item label="押运人手机号"
|
||||
@@ -316,7 +324,7 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
currencyUnit,
|
||||
@@ -403,6 +411,7 @@ export default {
|
||||
this.applySelfOperatedCarrierDefaults();
|
||||
},
|
||||
methods: {
|
||||
driverOptionText,
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
@@ -944,12 +953,6 @@ export default {
|
||||
route.escortPhone = '';
|
||||
this.driverOptions = [];
|
||||
},
|
||||
getRouteCarrierFilter(route = {}) {
|
||||
return {
|
||||
carrierId: route.carrierId || '',
|
||||
carrierName: route.carrierName || '',
|
||||
};
|
||||
},
|
||||
carrierOptionValue(item = {}) {
|
||||
return item.contractId || item.id || item.value || item.carrierName || '';
|
||||
},
|
||||
@@ -1026,36 +1029,22 @@ export default {
|
||||
this.activeSuggestRoute = route;
|
||||
},
|
||||
async loadDriverOptions(route = {}, keyword = '') {
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
this.driverOptions = [];
|
||||
return [];
|
||||
}
|
||||
this.driverLoading = true;
|
||||
try {
|
||||
this.driverOptions = await fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
);
|
||||
this.driverOptions = await fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '司机',
|
||||
});
|
||||
return this.driverOptions;
|
||||
} finally {
|
||||
this.driverLoading = false;
|
||||
}
|
||||
},
|
||||
fetchDriverSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
const keyword = String(queryString || '').trim();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.driverOptions = records;
|
||||
callback(
|
||||
@@ -1080,19 +1069,13 @@ export default {
|
||||
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
||||
},
|
||||
fetchEscortSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.escortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
callback(
|
||||
records.map(item => ({
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
|
||||
@@ -111,7 +111,7 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"
|
||||
><el-input v-model="form.arrivalContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
@@ -352,7 +352,12 @@
|
||||
@opened="loadAddressList"
|
||||
>
|
||||
<div class="route-dialog__search common-address-dialog__search">
|
||||
<el-form :model="addressQuery" label-position="right" label-width="auto">
|
||||
<el-form
|
||||
class="dialog-form-label-fixed"
|
||||
:model="addressQuery"
|
||||
label-position="right"
|
||||
label-width="96px"
|
||||
>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="地址名称">
|
||||
@@ -446,7 +451,12 @@
|
||||
@opened="loadStationList"
|
||||
>
|
||||
<div class="route-dialog__search">
|
||||
<el-form :model="stationQuery" label-position="right" label-width="auto">
|
||||
<el-form
|
||||
class="dialog-form-label-fixed"
|
||||
:model="stationQuery"
|
||||
label-position="right"
|
||||
label-width="96px"
|
||||
>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8"><el-form-item label="站点名称"><el-input v-model="stationQuery.name" clearable placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="详细地址"><el-input v-model="stationQuery.detailAddress" clearable placeholder="请输入" /></el-form-item></el-col>
|
||||
@@ -652,6 +662,8 @@ export default {
|
||||
addressQuery: {},
|
||||
addressPage: { current: 1, size: 10, total: 0 },
|
||||
addressRegionPaths: { departure: [], arrival: [] },
|
||||
// 程序回填省市区时抑制 cascader @change,避免空值回调把已填地址清空
|
||||
suppressAddressRegionChange: false,
|
||||
regionOptions: [],
|
||||
regionLoading: false,
|
||||
regionRequest: null,
|
||||
@@ -714,7 +726,7 @@ export default {
|
||||
'msg',
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
transports: ['铁路运输', '水路运输', '航空运输', '公路运输'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
@@ -798,6 +810,7 @@ export default {
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
if (this.quantityUnitOptions.includes('吨')) return '吨';
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
@@ -888,26 +901,63 @@ export default {
|
||||
findRegionPathByName(options = [], regionName = '', parents = []) {
|
||||
const target = String(regionName || '').replace(/\s+/g, '');
|
||||
if (!target) return [];
|
||||
let suffixMatch = [];
|
||||
for (const item of options) {
|
||||
const nextParents = [...parents, item];
|
||||
const name = nextParents.map(region => region.title || region.name || '').join('');
|
||||
if (name === target) return nextParents.map(region => region.id);
|
||||
// 常用地址可能缺省名,允许「市+区」后缀命中完整省市区路径
|
||||
if (!suffixMatch.length && name.endsWith(target) && target.length >= 2) {
|
||||
suffixMatch = nextParents.map(region => region.id);
|
||||
}
|
||||
const path = this.findRegionPathByName(item.children, target, nextParents);
|
||||
if (path.length) return path;
|
||||
}
|
||||
return [];
|
||||
return suffixMatch;
|
||||
},
|
||||
findRegionPathByCode(options = [], regionCode = '', parents = []) {
|
||||
const target = String(regionCode || '').trim();
|
||||
if (!target) return [];
|
||||
for (const item of options) {
|
||||
const nextParents = [...parents, item];
|
||||
if (String(item.id) === target) return nextParents.map(region => region.id);
|
||||
if (String(item.id) === target || String(item.value || '') === target) {
|
||||
return nextParents.map(region => region.id);
|
||||
}
|
||||
const path = this.findRegionPathByCode(item.children, target, nextParents);
|
||||
if (path.length) return path;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
resolveRegionPath(field) {
|
||||
if (!field) return [];
|
||||
const codePath = this.findRegionPathByCode(
|
||||
this.regionOptions,
|
||||
field.model[field.regionCode]
|
||||
);
|
||||
if (codePath.length) return codePath;
|
||||
const namePath = this.findRegionPathByName(this.regionOptions, field.model[field.name]);
|
||||
if (namePath.length) return namePath;
|
||||
const composed = [field.model[field.provinceName], field.model[field.cityName], field.model[field.districtName]]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
return this.findRegionPathByName(this.regionOptions, composed);
|
||||
},
|
||||
setAddressRegionPath(target, path = []) {
|
||||
const nextPath = Array.isArray(path) ? path.map(item => String(item)) : [];
|
||||
const current = this.addressRegionPaths[target] || [];
|
||||
if (
|
||||
current.length === nextPath.length &&
|
||||
current.every((item, index) => String(item) === String(nextPath[index]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.suppressAddressRegionChange = true;
|
||||
// Vue3 直接写动态 key,避免整体替换对象导致其它 cascader 重建并触发空 @change
|
||||
this.addressRegionPaths[target] = nextPath;
|
||||
this.$nextTick(() => {
|
||||
this.suppressAddressRegionChange = false;
|
||||
});
|
||||
},
|
||||
regionLabels(path = []) {
|
||||
let options = this.regionOptions;
|
||||
const labels = [];
|
||||
@@ -946,10 +996,22 @@ export default {
|
||||
this.setAddressRegionNames(field, [provinceMatch?.[0], cityMatch?.[0], districtMatch?.[0]]);
|
||||
},
|
||||
handleAddressRegionChange(target, value) {
|
||||
if (this.suppressAddressRegionChange) return;
|
||||
const field = this.addressModel(target);
|
||||
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||||
this.setAddressRegionPath(target, path);
|
||||
if (!field) return;
|
||||
// 程序回填/重建过程中 cascader 可能短暂抛出空值,不能据此清空已选常用地址
|
||||
if (!path.length) {
|
||||
const hasExisting =
|
||||
String(field.model[field.name] || '').trim() ||
|
||||
String(field.model[field.address] || '').trim() ||
|
||||
String(field.model[field.regionCode] || '').trim();
|
||||
if (hasExisting) {
|
||||
this.$nextTick(() => this.syncAddressRegionPath(target));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const labels = this.regionLabels(path);
|
||||
field.model[field.name] = labels.join('');
|
||||
field.model[field.regionCode] = path.at(-1) || '';
|
||||
@@ -958,19 +1020,29 @@ export default {
|
||||
},
|
||||
syncAddressRegionPath(target) {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
const codePath = this.findRegionPathByCode(this.regionOptions, field.model[field.regionCode]);
|
||||
const path = codePath.length
|
||||
? codePath
|
||||
: this.findRegionPathByName(this.regionOptions, field.model[field.name]);
|
||||
this.addressRegionPaths = { ...this.addressRegionPaths, [target]: path };
|
||||
if (!field || !this.regionOptions.length) return;
|
||||
const path = this.resolveRegionPath(field);
|
||||
this.setAddressRegionPath(target, path);
|
||||
if (path.length) this.setAddressRegionNames(field, this.regionLabels(path));
|
||||
},
|
||||
syncAddressRegionPaths() {
|
||||
if (!this.regionOptions.length) return;
|
||||
['departure', 'arrival', ...this.form.routes.map((route, index) => `route-${index}`)].forEach(target => {
|
||||
this.syncAddressRegionPath(target);
|
||||
});
|
||||
this.suppressAddressRegionChange = true;
|
||||
try {
|
||||
['departure', 'arrival', ...this.form.routes.map((route, index) => `route-${index}`)].forEach(
|
||||
target => {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
const path = this.resolveRegionPath(field);
|
||||
this.addressRegionPaths[target] = path;
|
||||
if (path.length) this.setAddressRegionNames(field, this.regionLabels(path));
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
this.$nextTick(() => {
|
||||
this.suppressAddressRegionChange = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
async loadDetail(id) {
|
||||
const res = await api.getDetail(id);
|
||||
@@ -1144,13 +1216,15 @@ export default {
|
||||
};
|
||||
},
|
||||
addressTransportType(target) {
|
||||
if (target.startsWith('route-')) {
|
||||
return this.form.routes[Number(target.slice(6))]?.transportType || '';
|
||||
const key = String(target || '');
|
||||
if (key.startsWith('route-')) {
|
||||
return String(this.form.routes[Number(key.slice(6))]?.transportType || '');
|
||||
}
|
||||
return target === 'arrival' ? this.form.finalTransportType : this.form.routes[0]?.transportType || '';
|
||||
if (key === 'arrival') return String(this.form.finalTransportType || '');
|
||||
return String(this.form.routes[0]?.transportType || '');
|
||||
},
|
||||
addressTransportMode(target) {
|
||||
const type = this.addressTransportType(target);
|
||||
const type = String(this.addressTransportType(target) || '');
|
||||
if (type.includes('铁路')) return 'rail';
|
||||
if (type.includes('水路') || type.includes('水运') || type.includes('海运')) return 'water';
|
||||
if (type.includes('航空') || type.includes('空运')) return 'air';
|
||||
@@ -1164,8 +1238,7 @@ export default {
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return false;
|
||||
const siteCode = String(field.model[field.siteCode] || '').trim();
|
||||
// 地址展示模式由实际选择的数据决定:有站点编码表示非公路站点名称,
|
||||
// 没有站点编码则保留省市区级联,避免切换运输方式时改写已有地址。
|
||||
// 展示模式由已选数据决定:有站点编码则显示站点名,否则省市区;暂不按运输方式强制站点限制。
|
||||
return Boolean(siteCode && siteCode !== '/');
|
||||
},
|
||||
resolveAddressRegionCode(row = {}) {
|
||||
@@ -1174,12 +1247,7 @@ export default {
|
||||
openCommonAddress(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.stationQuery = {};
|
||||
this.stationPage.current = 1;
|
||||
this.stationDialogVisible = true;
|
||||
return;
|
||||
}
|
||||
// 暂不按运输方式限制站点:统一走常用地址选择。
|
||||
this.addressQuery = {};
|
||||
this.addressPage.current = 1;
|
||||
this.addressDialogVisible = true;
|
||||
@@ -1224,8 +1292,9 @@ export default {
|
||||
this.stationPage.current = 1;
|
||||
this.loadStationList();
|
||||
},
|
||||
selectStation(row) {
|
||||
const target = this.addressModel(this.addressTarget);
|
||||
async selectStation(row) {
|
||||
const targetKey = this.addressTarget;
|
||||
const target = this.addressModel(targetKey);
|
||||
if (!target) return;
|
||||
const station = this.normalizeStation(row);
|
||||
target.model[target.name] = station.stationName || station.regionName;
|
||||
@@ -1236,7 +1305,9 @@ export default {
|
||||
target.model[target.siteCode] = station.stationCode;
|
||||
this.setAddressRegionNamesFromRow(target, station);
|
||||
this.stationDialogVisible = false;
|
||||
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||||
await this.loadRegionOptions();
|
||||
await this.$nextTick();
|
||||
this.syncAddressRegionPath(targetKey);
|
||||
},
|
||||
async loadAddressList() {
|
||||
this.addressLoading = true;
|
||||
@@ -1262,8 +1333,9 @@ export default {
|
||||
this.addressPage.current = 1;
|
||||
this.loadAddressList();
|
||||
},
|
||||
selectAddress(row) {
|
||||
const target = this.addressModel(this.addressTarget);
|
||||
async selectAddress(row) {
|
||||
const targetKey = this.addressTarget;
|
||||
const target = this.addressModel(targetKey);
|
||||
if (!target) return;
|
||||
target.model[target.name] = row.regionName || row.addressName || '';
|
||||
target.model[target.address] = row.detailAddress || row.address || '';
|
||||
@@ -1275,22 +1347,17 @@ export default {
|
||||
target.model[target.siteCode] = '';
|
||||
this.setAddressRegionNamesFromRow(target, row);
|
||||
this.addressDialogVisible = false;
|
||||
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||||
await this.loadRegionOptions();
|
||||
// nextTick 确保途经点动态 key 已挂载 cascader 后再回填路径
|
||||
await this.$nextTick();
|
||||
this.syncAddressRegionPath(targetKey);
|
||||
},
|
||||
handleAddressDetailClick(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.openCommonAddress(target);
|
||||
return;
|
||||
}
|
||||
this.openAddressMap(target);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
}
|
||||
const field = this.addressModel(target);
|
||||
if (!field) return;
|
||||
this.mapTarget = target;
|
||||
@@ -1690,7 +1757,11 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
// 常用货物单位若为空或「个」,新建总单统一默认「吨」
|
||||
quantityUnit:
|
||||
!row.quantityUnit || String(row.quantityUnit).trim() === '个'
|
||||
? this.defaultQuantityUnit()
|
||||
: row.quantityUnit,
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1775,17 +1846,21 @@ export default {
|
||||
return formatUpdateUserName(row);
|
||||
},
|
||||
selectRoute(route) {
|
||||
// 先写入运输方式(会触发地点清空),再回填线路地址,避免被清空逻辑覆盖。
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
if (this.form.routes[0]) this.form.routes[0].transportType = '公路运输';
|
||||
this.form.departureName = route.departureName || '';
|
||||
this.form.departureAddress = route.departureAddress || '';
|
||||
this.form.departureContact = route.departureContact || '';
|
||||
this.form.departurePhone = route.departurePhone || '';
|
||||
this.form.departureSiteCode = '';
|
||||
this.form.arrivalName = route.arrivalName || '';
|
||||
this.form.arrivalAddress = route.arrivalAddress || '';
|
||||
this.form.arrivalContact = route.arrivalContact || '';
|
||||
this.form.arrivalPhone = route.arrivalPhone || '';
|
||||
this.form.arrivalSiteCode = '';
|
||||
this.syncAddressRegionPaths();
|
||||
this.form.finalTransportType = '公路运输';
|
||||
if (!this.form.routes.length) this.addRoute();
|
||||
this.routeDialogVisible = false;
|
||||
},
|
||||
createRoute(index = this.form.routes.length) {
|
||||
@@ -1793,12 +1868,23 @@ export default {
|
||||
},
|
||||
addRoute() {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.form.routes.push(this.createRoute());
|
||||
const index = this.form.routes.length;
|
||||
this.form.routes.push(this.createRoute(index));
|
||||
this.addressRegionPaths[`route-${index}`] = [];
|
||||
},
|
||||
removeRoute(index) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.form.routes.splice(index, 1);
|
||||
this.syncAddressRegionPaths();
|
||||
const nextPaths = { departure: this.addressRegionPaths.departure || [], arrival: this.addressRegionPaths.arrival || [] };
|
||||
this.form.routes.forEach((route, routeIndex) => {
|
||||
nextPaths[`route-${routeIndex}`] = this.addressRegionPaths[`route-${routeIndex >= index ? routeIndex + 1 : routeIndex}`] || [];
|
||||
});
|
||||
this.suppressAddressRegionChange = true;
|
||||
this.addressRegionPaths = nextPaths;
|
||||
this.$nextTick(() => {
|
||||
this.suppressAddressRegionChange = false;
|
||||
this.syncAddressRegionPaths();
|
||||
});
|
||||
},
|
||||
goodsKey(row = {}) {
|
||||
return `${row.cargoName || ''}\u0000${row.cargoType || ''}`;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:class="{ 'punch-record-photos__grid--edit': editable }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, groupIndex) in group.items"
|
||||
v-for="item in group.items"
|
||||
:key="`${item.photo.url}-${item.index}`"
|
||||
class="punch-record-photos__item"
|
||||
@click="handlePreview(item.index)"
|
||||
@@ -23,8 +23,6 @@
|
||||
:lazy="lazy"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
|
||||
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
|
||||
<el-link
|
||||
v-if="editable"
|
||||
class="punch-record-photos__remove"
|
||||
@@ -59,7 +57,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
|
||||
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
|
||||
// 它与各凭证类型各自最多 9 张。
|
||||
const PUNCH_PHOTO_TYPE = '打卡照片';
|
||||
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
|
||||
const PHOTO_TYPE_FALLBACK = '凭证';
|
||||
@@ -252,18 +250,6 @@ const countText = group =>
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px 0 4px 0;
|
||||
background: rgba(64, 158, 255, 0.9);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
align-self: center;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1870,7 +1870,11 @@
|
||||
:fetch-suggestions="fetchTaskDriverSuggestions"
|
||||
:loading="taskDriverLoading"
|
||||
@select="item => handleTaskDriverSelect('dispatch', item)"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="dispatchItemForm.carrierType === '承运商' ? '手机号' : '司机手机号'"
|
||||
@@ -1893,7 +1897,11 @@
|
||||
:fetch-suggestions="fetchTaskEscortSuggestions"
|
||||
:loading="taskEscortLoading"
|
||||
@select="handleTaskEscortSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dispatchItemForm.carrierType !== '承运商'" label="押运人手机号">
|
||||
<el-input v-model="dispatchItemForm.escortPhone" placeholder="请输入" />
|
||||
@@ -2760,7 +2768,7 @@ import {
|
||||
getList as getProjectList,
|
||||
} from '@/api/business/project-apply';
|
||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
@@ -3809,6 +3817,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
driverOptionText,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -4178,6 +4187,7 @@ export default {
|
||||
waiting: ['待执行', 'is-pending'],
|
||||
processing: ['进行中', 'is-processing'],
|
||||
running: ['进行中', 'is-processing'],
|
||||
pending_return_confirm: ['待回单确认', 'is-pending'],
|
||||
completed: ['已完成', 'is-completed'],
|
||||
cancelled: ['已取消', 'is-cancelled'],
|
||||
};
|
||||
@@ -4988,16 +4998,8 @@ export default {
|
||||
},
|
||||
fetchTaskDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDispatchCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskDriverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
callback(
|
||||
@@ -5017,16 +5019,12 @@ export default {
|
||||
},
|
||||
fetchTaskEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDispatchCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskEscortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
this.taskEscortOptions = records;
|
||||
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
|
||||
@@ -8044,12 +8042,6 @@ export default {
|
||||
this.taskDriverOptions = [];
|
||||
this.taskEscortOptions = [];
|
||||
},
|
||||
getDispatchCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.dispatchItemForm.carrierId || '',
|
||||
carrierName: this.dispatchItemForm.carrierName || '',
|
||||
};
|
||||
},
|
||||
handleDispatchCargoQuantityInput(row, value) {
|
||||
const text = String(value || '').replace(/[^\d.]/g, '');
|
||||
const parts = text.split('.');
|
||||
|
||||
@@ -1381,11 +1381,23 @@ const formatOptionalNumber = value => {
|
||||
const normalized = normalizeOptionalImportNumber(value);
|
||||
return normalized === null || normalized === undefined ? '' : normalized;
|
||||
};
|
||||
// 明细接口返回运单实体字段 startDate/endDate/estimatedStartTime/estimatedEndTime,
|
||||
// 而明细表与编辑回填按导入列字段名 actualStartDate 等读取,重载后在此对齐,否则实际/预计时间列为空。
|
||||
const detailDateAliases = {
|
||||
actualStartDate: 'startDate',
|
||||
actualEndDate: 'endDate',
|
||||
planStartDate: 'estimatedStartTime',
|
||||
planEndDate: 'estimatedEndTime',
|
||||
};
|
||||
const normalizeDetailRow = row => {
|
||||
const next = { ...(row || {}) };
|
||||
OPTIONAL_NUMBER_PROPS.forEach(prop => {
|
||||
next[prop] = formatOptionalNumber(next[prop]);
|
||||
});
|
||||
Object.entries(detailDateAliases).forEach(([prop, source]) => {
|
||||
if (String(next[prop] ?? '').trim()) return;
|
||||
if (String(next[source] ?? '').trim()) next[prop] = next[source];
|
||||
});
|
||||
return next;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
|
||||
@@ -1200,7 +1200,11 @@
|
||||
:loading="taskDriverLoading"
|
||||
:disabled="dialogReadonly"
|
||||
@select="handleTaskDriverSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="isRoadTransport"
|
||||
@@ -1279,7 +1283,11 @@
|
||||
:loading="taskEscortLoading"
|
||||
:disabled="dialogReadonly"
|
||||
@select="handleTaskEscortSelect"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="showRoadEscortFields" label="押运人手机号">
|
||||
<el-input
|
||||
@@ -1594,6 +1602,13 @@
|
||||
<el-link type="primary" v-if="canComplete(row)" @click="handleAction('complete', row)">
|
||||
{{ completeActionLabel }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="canConfirmReturn(row)"
|
||||
@click="handleAction('confirmReturn', row)"
|
||||
>
|
||||
{{ completeActionLabel }}
|
||||
</el-link>
|
||||
<el-link type="primary" v-if="canMaintainMileage(row)" @click="openMileageDialog(row)">
|
||||
维护里程
|
||||
</el-link>
|
||||
@@ -1653,7 +1668,11 @@
|
||||
style="width: 100%"
|
||||
@select="handleReassignDriverSelect"
|
||||
@change="handleReassignDriverChange"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="手机号"
|
||||
@@ -2252,6 +2271,13 @@
|
||||
<span v-if="record.volume">体积 {{ record.volume }}方</span>
|
||||
<span v-if="record.quantity">数量 {{ record.quantity }}</span>
|
||||
</div>
|
||||
<!-- 里程只挂在末位节点上(needMileage 由后端下发),显示的是运单里程 -->
|
||||
<div
|
||||
v-if="record.needMileage && Number(record.mileage) > 0"
|
||||
class="waybill-manage-page__punch-record-line"
|
||||
>
|
||||
里程 {{ record.mileage }} 公里
|
||||
</div>
|
||||
<div
|
||||
v-if="record.remark"
|
||||
class="waybill-manage-page__punch-record-line"
|
||||
@@ -2848,6 +2874,20 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<!-- 里程只在末位打卡节点出现:后端按「末位 + 计费规则含里程」下发 needMileage,前端不自己推 -->
|
||||
<el-col v-if="waybillPunchEditMileageVisible" :span="8">
|
||||
<el-form-item label="里程">
|
||||
<el-input
|
||||
v-model="waybillPunchEditForm.mileage"
|
||||
maxlength="10"
|
||||
:disabled="waybillPunchEditMileageLocked"
|
||||
placeholder="请输入"
|
||||
@input="handleWaybillPunchMileageInput"
|
||||
>
|
||||
<template #append>公里</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
@@ -3683,6 +3723,7 @@ import {
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import {
|
||||
getList as getWaybillList,
|
||||
getPunchRecords as getWaybillPunchRecords,
|
||||
locateVehicle,
|
||||
savePunchRecord,
|
||||
@@ -3692,9 +3733,7 @@ import VoucherExecutionDetail from './voucher-execution-detail.vue';
|
||||
import PunchRecordPhotos from './punch-record-photos.vue';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import {
|
||||
fetchDriversByCarrierOrganizations,
|
||||
} from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
@@ -4011,6 +4050,7 @@ export default {
|
||||
weight: '',
|
||||
volume: '',
|
||||
quantity: '',
|
||||
mileage: '',
|
||||
remark: '',
|
||||
photos: [],
|
||||
},
|
||||
@@ -4370,9 +4410,15 @@ export default {
|
||||
addBtn: this.canCreate,
|
||||
};
|
||||
},
|
||||
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水
|
||||
// 打卡记录编辑入口与运单编辑同权限:能改运单的人才能订正它的打卡流水。
|
||||
// 时间轴的「编辑打卡 / 补卡」链接是本页唯一写入入口,提交固定 punched=true。
|
||||
// 取消打卡不在本页;待回单确认的运单不显示该链接。
|
||||
canEditWaybillPunchRecord() {
|
||||
return this.hasPermission(`${this.config.permission}_edit`) && !this.readonlyScope;
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
!this.readonlyScope &&
|
||||
this.statusValue(this.detailRow) !== 'pending_return_confirm'
|
||||
);
|
||||
},
|
||||
waybillPunchEditNode() {
|
||||
return this.findWaybillPunchEditNodeConfig(this.waybillPunchEditRecord);
|
||||
@@ -4407,6 +4453,15 @@ export default {
|
||||
waybillPunchEditPunchDisabled() {
|
||||
return this.waybillPunchEditNode?.punch === false;
|
||||
},
|
||||
// 里程:后端下发 needMileage 才出现(末位打卡节点 + 计费规则含里程);在途打卡不采集里程
|
||||
waybillPunchEditMileageVisible() {
|
||||
const record = this.waybillPunchEditRecord || {};
|
||||
return record.needMileage === true && record.type !== 'enroute';
|
||||
},
|
||||
// 已生成结算单的运单里程冻结:输入框置灰,改动由后端拒绝
|
||||
waybillPunchEditMileageLocked() {
|
||||
return this.waybillPunchEditRecord?.mileageLocked === true;
|
||||
},
|
||||
waybillPunchEditRules() {
|
||||
return { punchTime: [{ required: true, message: '请选择打卡时间', trigger: 'change' }] };
|
||||
},
|
||||
@@ -4800,6 +4855,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
driverOptionText,
|
||||
parseRouteChangeContent,
|
||||
// 页面被 keep-alive 缓存/卸载时调用,关闭本页所有 append-to-body 的弹窗,
|
||||
// 避免 Teleport 出去的 DOM 残留在 body 上盖住后续页面(详见 data 里的说明)。
|
||||
@@ -5060,11 +5116,16 @@ export default {
|
||||
);
|
||||
},
|
||||
canCopy(row) {
|
||||
if (!row) return false;
|
||||
// 归属本账号可见范围的运单允许复制。跨组织但项目归属本账号的记录会被标成只读,
|
||||
// 只读仅拦截编辑/删除,不拦截复制。具备查看、新增、编辑或复制权限即可使用。
|
||||
return (
|
||||
this.hasAction('copy') &&
|
||||
this.hasPermission(`${this.config.permission}_copy`) &&
|
||||
!this.readonlyScope &&
|
||||
!row.readonly
|
||||
(this.hasPermission(`${this.config.permission}_copy`) ||
|
||||
this.hasPermission(`${this.config.permission}_add`) ||
|
||||
this.hasPermission(`${this.config.permission}_edit`) ||
|
||||
this.hasPermission(`${this.config.permission}_view`))
|
||||
);
|
||||
},
|
||||
canEnable(row) {
|
||||
@@ -5099,7 +5160,8 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const status = this.statusValue(row);
|
||||
return status === 'pending';
|
||||
// 待回单确认可取消,取消不生成应收应付
|
||||
return status === 'pending' || status === 'pending_return_confirm';
|
||||
},
|
||||
canComplete(row) {
|
||||
if (
|
||||
@@ -5111,8 +5173,22 @@ export default {
|
||||
return false;
|
||||
}
|
||||
const status = this.statusValue(row);
|
||||
// 待回单确认的「完成」由确认回单入口承担(走打卡节点与未了结异常校验);
|
||||
// 只有确认回单不可用时才退回这里的普通完成
|
||||
if (status === 'pending_return_confirm') {
|
||||
return !this.canConfirmReturn(row);
|
||||
}
|
||||
return status === 'processing' || status === 'running';
|
||||
},
|
||||
canConfirmReturn(row) {
|
||||
return (
|
||||
this.hasAction('confirmReturn') &&
|
||||
this.hasPermission(`${this.config.permission}_complete`) &&
|
||||
!this.readonlyScope &&
|
||||
!row.readonly &&
|
||||
this.statusValue(row) === 'pending_return_confirm'
|
||||
);
|
||||
},
|
||||
canMaintainMileage(row) {
|
||||
return (
|
||||
this.config.permission === 'waybill_manage' &&
|
||||
@@ -5926,6 +6002,8 @@ export default {
|
||||
weight: record.weight || '',
|
||||
volume: record.volume || '',
|
||||
quantity: record.quantity || '',
|
||||
// 里程只有正整数口径,后端若仍下发 "200.00" 会过不了提交时的正整数校验
|
||||
mileage: Number(record.mileage) > 0 ? String(Number(record.mileage)) : '',
|
||||
remark: record.remark || '',
|
||||
photos: (record.photos || [])
|
||||
.filter(photo => photo?.url)
|
||||
@@ -6001,10 +6079,22 @@ export default {
|
||||
photos.splice(index, 1);
|
||||
this.waybillPunchEditForm.photos = photos;
|
||||
},
|
||||
// 里程只收整数:非数字直接丢掉,长度与「维护里程」一致(≤10 位)
|
||||
handleWaybillPunchMileageInput(value) {
|
||||
this.waybillPunchEditForm.mileage = String(value || '')
|
||||
.replace(/\D/g, '')
|
||||
.slice(0, 10);
|
||||
},
|
||||
async submitWaybillPunchRecordEdit() {
|
||||
await this.$refs.waybillPunchRecordEditFormRef.validate();
|
||||
const record = this.waybillPunchEditRecord;
|
||||
const form = this.waybillPunchEditForm;
|
||||
// 管理端里程选填:留空表示不改动运单里程,所以只校验填了值的情况
|
||||
const mileageText = String(form.mileage || '').trim();
|
||||
if (this.waybillPunchEditMileageVisible && mileageText && !/^[1-9]\d{0,9}$/.test(mileageText)) {
|
||||
this.$message.warning('里程必须为不超过10位的正整数');
|
||||
return;
|
||||
}
|
||||
const submit =
|
||||
typeof this.api?.savePunchRecord === 'function' ? this.api.savePunchRecord : savePunchRecord;
|
||||
this.waybillPunchRecordEditDialog.submitting = true;
|
||||
@@ -6023,6 +6113,8 @@ export default {
|
||||
weight: form.weight,
|
||||
volume: form.volume,
|
||||
quantity: form.quantity,
|
||||
// 不传代表不改;后端按 null 处理
|
||||
mileage: mileageText ? Number(mileageText) : undefined,
|
||||
remark: form.remark,
|
||||
photos: form.photos.map(photo => ({ url: photo.url, type: photo.type })),
|
||||
});
|
||||
@@ -6543,6 +6635,9 @@ export default {
|
||||
if (status === 'completed') {
|
||||
return 'primary';
|
||||
}
|
||||
if (status === 'pending_return_confirm') {
|
||||
return 'warning';
|
||||
}
|
||||
return 'warning';
|
||||
},
|
||||
normalizeRow(row, saveAsDraft = false) {
|
||||
@@ -7942,19 +8037,10 @@ export default {
|
||||
this.taskDriverOptions = [];
|
||||
this.taskEscortOptions = [];
|
||||
},
|
||||
getTaskCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.form.carrierId || '',
|
||||
carrierName: this.form.carrierName || '',
|
||||
};
|
||||
},
|
||||
loadTaskDriverOptions() {
|
||||
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
|
||||
this.taskDriverLoading = true;
|
||||
return fetchDriversByCarrierOrganizations(
|
||||
{ size: 9999, posts: '司机' },
|
||||
this.getTaskCarrierFilter()
|
||||
)
|
||||
return fetchDriverCandidates({ size: 9999, posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
return this.taskDriverOptions;
|
||||
@@ -7965,16 +8051,8 @@ export default {
|
||||
},
|
||||
fetchTaskDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getTaskCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskDriverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.taskDriverOptions = records;
|
||||
callback(
|
||||
@@ -7994,16 +8072,12 @@ export default {
|
||||
},
|
||||
fetchTaskEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getTaskCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.taskEscortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
this.taskEscortOptions = records;
|
||||
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
|
||||
@@ -10150,8 +10224,14 @@ export default {
|
||||
cancel: '取消',
|
||||
complete: this.completeActionLabel,
|
||||
reassign: '重新派单',
|
||||
confirmReturn: '完成',
|
||||
}[action];
|
||||
this.$confirm(`确定${actionName}该${this.config.title}?`, '提示', {
|
||||
// 回单确认是终态动作(本轮不做撤销),把后果写在提示里,别让人以为还能退回
|
||||
const confirmText =
|
||||
action === 'confirmReturn'
|
||||
? '回单确认后该运单将置为已完成,且不可撤销。是否继续?'
|
||||
: `确定${actionName}该${this.config.title}?`;
|
||||
this.$confirm(confirmText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -10204,20 +10284,8 @@ export default {
|
||||
},
|
||||
fetchReassignDriverSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const row = this.reassignDrawer.row || {};
|
||||
const carrier = {
|
||||
carrierId: row.carrierId || '',
|
||||
carrierName: row.carrierName || '',
|
||||
};
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.reassignDrawer.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
|
||||
.then(records => {
|
||||
this.reassignDriverOptions = records;
|
||||
callback(
|
||||
@@ -10356,7 +10424,14 @@ export default {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定完成选中的运单?', '提示', {
|
||||
const pendingReturnCount = this.selectionList.filter(
|
||||
row => this.statusValue(row) === 'pending_return_confirm'
|
||||
).length;
|
||||
const tip =
|
||||
pendingReturnCount > 0
|
||||
? `确定完成选中的运单?其中 ${pendingReturnCount} 条为待回单确认,确认后将转为已完成。`
|
||||
: '确定完成选中的运单?';
|
||||
this.$confirm(tip, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@@ -10370,25 +10445,38 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
},
|
||||
handleRoadLoading() {
|
||||
async handleRoadLoading() {
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const nonRoadWaybills = this.selectionList.filter(row => !this.waybillIsRoadTransport(row));
|
||||
if (nonRoadWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持运输方式为“公路运输”的运单');
|
||||
const selectedIds = this.selectionList.map(row => row.id).filter(Boolean);
|
||||
if (!selectedIds.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const unavailableWaybills = this.selectionList.filter(
|
||||
row =>
|
||||
!['processing', 'running'].includes(this.statusValue(row)) ||
|
||||
row.loadingId ||
|
||||
row.loadingManageId ||
|
||||
row.loadingNo
|
||||
);
|
||||
if (unavailableWaybills.length) {
|
||||
this.$message.warning('公路配载仅支持进行中且未配载的运单');
|
||||
// 可配载与否只看后端查询结果
|
||||
let loadableIds;
|
||||
try {
|
||||
const res = await getWaybillList(1, selectedIds.length, {
|
||||
loadableOnly: 1,
|
||||
ids: selectedIds.join(','),
|
||||
});
|
||||
loadableIds = new Set(extractRecords(res).map(row => String(row.id)));
|
||||
} catch (error) {
|
||||
this.$message.error('校验运单可配载状态失败,请重试');
|
||||
return;
|
||||
}
|
||||
const rejectedRows = this.selectionList.filter(row => !loadableIds.has(String(row.id)));
|
||||
if (rejectedRows.length) {
|
||||
const rejectedText = rejectedRows
|
||||
.map(row => {
|
||||
const reason = this.roadLoadingRejectReasonText(row);
|
||||
const no = row.waybillNo || row.id || '';
|
||||
return `${no}(${reason})`;
|
||||
})
|
||||
.join('、');
|
||||
this.$message.warning(`${rejectedText};公路配载仅支持公路、待执行且未配载的运单`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -10411,6 +10499,13 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
// 只给后端判定为不可配载的运单生成可读原因,本身不参与准入判定
|
||||
roadLoadingRejectReasonText(row = {}) {
|
||||
if (!this.waybillIsRoadTransport(row)) return '非公路运输';
|
||||
if (String(row.businessStatus || '') !== 'pending') return '状态不是待执行';
|
||||
if (row.loadingNo) return '已关联配载单';
|
||||
return '运单数据可能已变化,请刷新后重试';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<mk-public-shell biz-type="contract-change" :get-form="getForm">
|
||||
<contract-manage-change ref="page" />
|
||||
</mk-public-shell>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
|
||||
import ContractManageChange from '@/views/business/contract-manage-change.vue';
|
||||
|
||||
export default {
|
||||
name: 'ContractChangePublicView',
|
||||
components: { MkPublicShell, ContractManageChange },
|
||||
methods: {
|
||||
getForm() {
|
||||
const row = this.$refs.page?.form || {};
|
||||
return { ...row, subject: row.contractName || row.contractNo || '' };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<basic-container class="contract-change-page">
|
||||
<avue-crud
|
||||
:option="tableOption"
|
||||
:table-loading="loading"
|
||||
:data="data"
|
||||
v-model:page="page"
|
||||
v-model="tableForm"
|
||||
ref="crud"
|
||||
:permission="permissionList"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@selection-change="selectionChange"
|
||||
@current-change="currentChange"
|
||||
@size-change="sizeChange"
|
||||
@refresh-change="refreshChange"
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button type="primary" plain :loading="hcsRepushingId === 'batch'" @click="handleHcsBatchRepush">
|
||||
批量同步
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
v-if="hasPermission(`${config.permission}_delete`)"
|
||||
@click="handleDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #approvalStatus="{ row }">
|
||||
{{ row.approvalStatusName || statusLabel(row.approvalStatus) }}
|
||||
</template>
|
||||
|
||||
<template #syncStatus="{ row }">
|
||||
<el-tag v-if="row.syncStatus" :type="syncTagType(row.syncStatus)" class="status-text">
|
||||
{{ syncLabel(row.syncStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link type="primary" v-if="canView(row)" @click="openForm('view', row)"> 查看 </el-link>
|
||||
<el-link type="primary" v-if="canEdit(row)" @click="openForm('edit', row)"> 编辑 </el-link>
|
||||
<el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
|
||||
<el-link
|
||||
v-for="operation in customOperations(row)"
|
||||
:key="operation.action"
|
||||
:type="
|
||||
operation.type === 'warning'
|
||||
? 'warning'
|
||||
: operation.type === 'success'
|
||||
? 'success'
|
||||
: 'primary'
|
||||
"
|
||||
@click="handleCustomOperation(operation, row)"
|
||||
>
|
||||
{{ operation.label }}
|
||||
</el-link>
|
||||
</template>
|
||||
</avue-crud>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/contract-change';
|
||||
import { repushBatch as hcsRepushBatch } from '@/api/business/hcs';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { config, option } from '@/option/business/contract-change';
|
||||
import { approvalStatusOptions } from '@/option/business/common';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const contractChangeFormRoute = '/business/contract-change/form';
|
||||
const contractChangeListRefreshKey = 'contract-change-list-refresh';
|
||||
|
||||
export default {
|
||||
name: 'ContractChange',
|
||||
data() {
|
||||
return {
|
||||
api,
|
||||
config,
|
||||
tableForm: {},
|
||||
tableOption: this.buildTableOption(),
|
||||
loading: true,
|
||||
data: [],
|
||||
page: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
query: {},
|
||||
selectionList: [],
|
||||
mkEnabled: false,
|
||||
hcsRepushingId: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo', 'website']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
activated() {
|
||||
if (sessionStorage.getItem(contractChangeListRefreshKey)) {
|
||||
sessionStorage.removeItem(contractChangeListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildTableOption() {
|
||||
return {
|
||||
...option,
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
menuWidth: 320,
|
||||
column: (option.column || []).map(column => ({
|
||||
...column,
|
||||
display: false,
|
||||
align: 'center',
|
||||
headerAlign: 'center',
|
||||
})),
|
||||
};
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||||
},
|
||||
statusLabel(status) {
|
||||
return approvalStatusOptions.find(item => item.value === status)?.label || status || '-';
|
||||
},
|
||||
syncLabel(value) {
|
||||
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
|
||||
},
|
||||
syncTagType(value) {
|
||||
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
|
||||
},
|
||||
canHcsRepush(row) {
|
||||
return row.approvalStatus === 'approved' && row.contractId && row.syncStatus !== 'synced';
|
||||
},
|
||||
handleHcsBatchRepush() {
|
||||
if (this.hcsRepushingId) return;
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const rows = this.selectionList.filter(item => this.canHcsRepush(item));
|
||||
if (!rows.length) {
|
||||
this.$message.warning('请选择审批通过且未同步成功的变更单');
|
||||
return;
|
||||
}
|
||||
this.$confirm(`确定同步选中的${rows.length}条合同变更到合规系统?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = 'batch';
|
||||
const contractIds = [...new Set(rows.map(item => item.contractId))];
|
||||
const changeIds = rows.map(item => item.id).join(',');
|
||||
hcsRepushBatch('contract', contractIds)
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '批量同步失败');
|
||||
return api.markSyncStatus(changeIds, 'failed');
|
||||
}
|
||||
return api.markSyncStatus(changeIds, 'synced').then(() => {
|
||||
this.$message.success(res.data?.data || '批量同步成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
})
|
||||
.catch(() => api.markSyncStatus(changeIds, 'failed'))
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
inStatus(row, statusList) {
|
||||
if (!statusList || !statusList.length) return true;
|
||||
return statusList.includes(row.approvalStatus);
|
||||
},
|
||||
canEdit(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
!row.readonly &&
|
||||
this.inStatus(row, this.config.editStatus)
|
||||
);
|
||||
},
|
||||
canDelete(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_delete`) &&
|
||||
!row.readonly &&
|
||||
this.inStatus(row, this.config.deleteStatus)
|
||||
);
|
||||
},
|
||||
canView(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_view`) &&
|
||||
this.inStatus(row, this.config.viewStatus)
|
||||
);
|
||||
},
|
||||
canOperation(operation, row) {
|
||||
if (operation.permission && !this.hasPermission(operation.permission)) return false;
|
||||
if (
|
||||
row.readonly &&
|
||||
['submitApproval', 'withdraw', 'approve', 'reject'].includes(operation.action)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return this.inStatus(row, operation.status);
|
||||
},
|
||||
customOperations(row) {
|
||||
return (this.config.operations || []).filter(operation => {
|
||||
if (operation.mkOnly && !this.mkEnabled) return false;
|
||||
if (operation.localOnly && this.mkEnabled) return false;
|
||||
return this.canOperation(operation, row);
|
||||
});
|
||||
},
|
||||
openForm(mode, row = {}) {
|
||||
const query = { mode, id: row.id };
|
||||
if (mode === 'edit') query.name = '编辑合同变更';
|
||||
else if (mode === 'view') query.name = '查看合同变更';
|
||||
this.$router.push({ path: contractChangeFormRoute, query });
|
||||
},
|
||||
handleCustomOperation(operation, row) {
|
||||
if (operation.action === 'mkAudit') {
|
||||
this.handleMkAudit(row);
|
||||
return;
|
||||
}
|
||||
const run = () => {
|
||||
const request = () => this.api[operation.action](row.id);
|
||||
const afterMk =
|
||||
operation.action === 'submitApproval'
|
||||
? submitMkApprovalFlow({
|
||||
bizType: 'contract-change',
|
||||
bizId: row.id,
|
||||
subjectName: row.contractName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
mkEnabled: this.mkEnabled,
|
||||
}).then(() => request())
|
||||
: request();
|
||||
const loading =
|
||||
operation.action === 'approve'
|
||||
? this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
})
|
||||
: null;
|
||||
afterMk
|
||||
.then(res => {
|
||||
if (operation.action === 'approve' && res.data?.data === false) {
|
||||
this.$message.warning(res.data?.msg);
|
||||
this.onLoad(this.page, this.query);
|
||||
return;
|
||||
}
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
|
||||
return;
|
||||
}
|
||||
this.$message.success(`${operation.label}成功`);
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => loading && loading.close());
|
||||
};
|
||||
this.$confirm(operation.confirm || `确定${operation.label}该${this.config.title}?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => run());
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('变更单ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
this.selectionList = list;
|
||||
},
|
||||
selectionClear() {
|
||||
this.selectionList = [];
|
||||
this.$refs.crud?.toggleSelection?.();
|
||||
},
|
||||
currentChange(currentPage) {
|
||||
this.page.currentPage = currentPage;
|
||||
},
|
||||
sizeChange(pageSize) {
|
||||
this.page.pageSize = pageSize;
|
||||
},
|
||||
refreshChange() {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
rowDel(row) {
|
||||
this.$confirm('确定将选择数据删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.api.remove(row.id).then(() => {
|
||||
this.$message.success('删除成功!');
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleDelete() {
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定将选择数据删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.api.remove(this.ids).then(() => {
|
||||
this.$message.success('删除成功!');
|
||||
this.selectionClear();
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
});
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const values = {
|
||||
...this.query,
|
||||
...params,
|
||||
};
|
||||
this.api
|
||||
.getList(page.currentPage, page.pageSize, values)
|
||||
.then(res => {
|
||||
const data = res.data.data || {};
|
||||
this.page.total = data.total || 0;
|
||||
this.data = data.records || [];
|
||||
this.loading = false;
|
||||
this.selectionClear();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contract-change-page {
|
||||
:deep(.avue-crud__menu) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,32 @@
|
||||
<template>
|
||||
<basic-container class="contract-change-page">
|
||||
<div class="archive-page-form__title">{{ pageTitle }}</div>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="contract-change-form">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:disabled="pageReadonly"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
class="contract-change-form"
|
||||
>
|
||||
<section class="change-section contract-basic-section">
|
||||
<div class="dialog-section-title">基本信息</div>
|
||||
<div class="contract-basic-section__grid">
|
||||
<el-form-item label="变更类型" class="contract-basic-section__change-type">
|
||||
<el-radio-group v-model="form.changeType">
|
||||
<el-radio-group v-model="form.changeType" :disabled="pageReadonly">
|
||||
<el-radio label="合同信息变更" />
|
||||
<el-radio label="结算规则变更" />
|
||||
<el-radio label="终止合同" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="签约类型" prop="signType">
|
||||
<el-select v-model="form.signType" placeholder="请选择签约类型" clearable>
|
||||
<el-select
|
||||
v-model="form.signType"
|
||||
placeholder="请选择签约类型"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in signTypeOptions"
|
||||
:key="item.value"
|
||||
@@ -21,8 +35,14 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号">
|
||||
<el-input v-model="form.contractNo" disabled />
|
||||
<el-form-item label="合同编号" prop="contractNo">
|
||||
<el-input
|
||||
v-model="form.contractNo"
|
||||
maxlength="100"
|
||||
clearable
|
||||
placeholder="请输入合同编号"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
<el-input
|
||||
@@ -30,6 +50,7 @@
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入合同名称"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同类型">
|
||||
@@ -80,6 +101,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
<span>至</span>
|
||||
<el-date-picker
|
||||
@@ -88,6 +110,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -95,6 +118,7 @@
|
||||
<el-input
|
||||
v-model="form.paymentDays"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('paymentDays', value)"
|
||||
>
|
||||
<template #suffix>天</template>
|
||||
@@ -105,22 +129,36 @@
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否范本">
|
||||
<el-select v-model="form.templateFlag" placeholder="请选择">
|
||||
<el-select
|
||||
v-model="form.templateFlag"
|
||||
placeholder="请选择"
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="原件合同编号">
|
||||
<el-input v-model="form.originalContractNo" maxlength="100" placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="form.originalContractNo"
|
||||
maxlength="100"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否电子章">
|
||||
<el-select v-model="form.electronicSealFlag" placeholder="请选择">
|
||||
<el-select
|
||||
v-model="form.electronicSealFlag"
|
||||
placeholder="请选择"
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -129,13 +167,19 @@
|
||||
<el-input
|
||||
v-model="form.invoiceCycle"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('invoiceCycle', value)"
|
||||
>
|
||||
<template #suffix>天</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算币种" prop="settlementCurrency">
|
||||
<el-select v-model="form.settlementCurrency" placeholder="请选择结算币种" clearable>
|
||||
<el-select
|
||||
v-model="form.settlementCurrency"
|
||||
placeholder="请选择结算币种"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in settlementCurrencyOptions"
|
||||
:key="item.value"
|
||||
@@ -145,7 +189,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同格式">
|
||||
<el-select v-model="form.contractFormat" placeholder="请选择合同格式" clearable>
|
||||
<el-select
|
||||
v-model="form.contractFormat"
|
||||
placeholder="请选择合同格式"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractFormatOptions"
|
||||
:key="item.value"
|
||||
@@ -158,19 +207,30 @@
|
||||
<el-input
|
||||
v-model="form.copyCount"
|
||||
placeholder="请输入"
|
||||
:disabled="isSettlementRuleChange"
|
||||
@input="value => positiveIntegerInput('copyCount', value)"
|
||||
>
|
||||
<template #suffix>份</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-select
|
||||
v-model="form.legalSealFlag"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算方式">
|
||||
<el-select v-model="form.settlementMode" placeholder="请选择结算方式" clearable>
|
||||
<el-select
|
||||
v-model="form.settlementMode"
|
||||
placeholder="请选择结算方式"
|
||||
clearable
|
||||
:disabled="isSettlementRuleChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in settlementModeOptions"
|
||||
:key="item.value"
|
||||
@@ -191,6 +251,7 @@
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
|
||||
@@ -201,14 +262,21 @@
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入其他事项说明"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 风险客商提示:合同已推送合规并留痕高风险/黑名单时展示,变更提交同样要求特批附件 -->
|
||||
<div v-if="riskTipText" class="contract-manage-form__risk-tip">
|
||||
<div class="contract-manage-form__risk-tip-title">风险客商提示:</div>
|
||||
<div class="contract-manage-form__risk-tip-content">{{ riskTipText }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:readonly="isSettlementRuleChange"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="contractFileRows"
|
||||
:preview="previewAttachment"
|
||||
@@ -219,12 +287,37 @@
|
||||
<section class="change-section">
|
||||
<div class="section-head">
|
||||
<div class="dialog-section-title">计费信息</div>
|
||||
<el-button v-if="feeGenerationMode !== 'manual'" type="primary" plain @click="addPlan"
|
||||
<el-button
|
||||
v-if="!pageReadonly && feeGenerationMode !== 'manual'"
|
||||
type="primary"
|
||||
plain
|
||||
@click="addPlan"
|
||||
>添加</el-button
|
||||
>
|
||||
</div>
|
||||
<el-radio-group v-model="feeGenerationMode"><el-radio label="system">系统生成</el-radio><el-radio label="manual">账单导入生成</el-radio></el-radio-group>
|
||||
<el-table :data="plans" border class="change-table"><el-table-column type="index" label="序号" width="70" /><el-table-column prop="planName" label="方案名称" min-width="220" /><el-table-column label="默认方案" min-width="120"><template #default="{ row }">{{ row.defaultPlan ? '是' : '否' }}</template></el-table-column><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column label="操作" width="180"><template #default="{ row, $index }"><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template></el-table-column></el-table>
|
||||
<el-radio-group v-model="feeGenerationMode"
|
||||
><el-radio label="system">系统生成</el-radio
|
||||
><el-radio label="manual">账单导入生成</el-radio></el-radio-group
|
||||
>
|
||||
<el-table :data="plans" border class="change-table"
|
||||
><el-table-column type="index" label="序号" width="70" /><el-table-column
|
||||
prop="planName"
|
||||
label="方案名称"
|
||||
min-width="220"
|
||||
/><el-table-column label="默认方案" min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
row.defaultPlan ? '是' : '否'
|
||||
}}</template></el-table-column
|
||||
><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column
|
||||
v-if="!pageReadonly"
|
||||
label="操作"
|
||||
width="180"
|
||||
><template #default="{ row, $index }"
|
||||
><el-link type="primary" @click="editPlan(row, $index)">编辑</el-link
|
||||
><el-link type="danger" @click="plans.splice($index, 1)">删除</el-link></template
|
||||
></el-table-column
|
||||
></el-table
|
||||
>
|
||||
</section>
|
||||
|
||||
<section class="change-section">
|
||||
@@ -243,19 +336,106 @@
|
||||
<el-radio :label="0">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
|
||||
<el-form-item label="账单起始日" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择结算周期" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"><el-option v-for="item in cycleDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
|
||||
<el-form
|
||||
v-if="settlementRule.autoGenerate === 1"
|
||||
:model="settlementRule"
|
||||
label-position="right"
|
||||
label-width="auto"
|
||||
class="settlement-form"
|
||||
>
|
||||
<el-form-item label="账单起始日" required
|
||||
><el-date-picker
|
||||
v-model="settlementRule.billStartDate"
|
||||
type="date"
|
||||
placeholder="请选择账单起始日"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/></el-form-item>
|
||||
<el-form-item label="结算类型" required
|
||||
><el-select
|
||||
v-model="settlementRule.settlementType"
|
||||
placeholder="请选择结算类型"
|
||||
@change="handleSettlementTypeChange"
|
||||
><el-option
|
||||
v-for="item in settlementTypeOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required
|
||||
><el-select
|
||||
v-model="settlementRule.billCycleType"
|
||||
placeholder="请选择结算周期"
|
||||
@change="handleCycleTypeChange"
|
||||
><el-option
|
||||
v-for="item in billCycleTypeOptions"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required
|
||||
><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"
|
||||
><el-option
|
||||
v-for="item in billCutoffDayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></el-form-item>
|
||||
<el-form-item v-if="showSettlementCycleDays" label="周期天数" required
|
||||
><el-select v-model="settlementRule.cycleDays" placeholder="请选择周期天数"
|
||||
><el-option
|
||||
v-for="item in cycleDayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></el-form-item>
|
||||
</el-form>
|
||||
<el-table v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods" :data="settlementRule.customPeriods || []" border class="change-table custom-periods-table">
|
||||
<el-table
|
||||
v-if="settlementRule.autoGenerate === 1 && showSettlementCustomPeriods"
|
||||
:data="settlementRule.customPeriods || []"
|
||||
border
|
||||
class="change-table custom-periods-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="运单区间-开始日" min-width="180"><template #default="{ row, $index }"><el-select :model-value="row.startDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodStartDayChange($index, value)"><el-option v-for="item in billCutoffDayOptions" :key="`start-${item.value}`" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||
<el-table-column label="运单区间-结束日" min-width="200"><template #default="{ row, $index }"><el-select :model-value="row.endDay" placeholder="请选择" clearable @update:model-value="value => handleCustomPeriodEndDayChange($index, value)"><el-option v-for="item in customPeriodEndDayOptions(row)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link><el-link v-else type="danger" @click="removeCustomPeriodRow($index)">删除</el-link></template></el-table-column>
|
||||
<template #empty><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template>
|
||||
<el-table-column label="运单区间-开始日" min-width="180"
|
||||
><template #default="{ row, $index }"
|
||||
><el-select
|
||||
:model-value="row.startDay"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
@update:model-value="value => handleCustomPeriodStartDayChange($index, value)"
|
||||
><el-option
|
||||
v-for="item in billCutoffDayOptions"
|
||||
:key="`start-${item.value}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="运单区间-结束日" min-width="200"
|
||||
><template #default="{ row, $index }"
|
||||
><el-select
|
||||
:model-value="row.endDay"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
@update:model-value="value => handleCustomPeriodEndDayChange($index, value)"
|
||||
><el-option
|
||||
v-for="item in customPeriodEndDayOptions(row)"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column v-if="!pageReadonly" label="操作" width="100"
|
||||
><template #default="{ $index }"
|
||||
><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link
|
||||
><el-link v-else type="danger" @click="removeCustomPeriodRow($index)"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
<template #empty
|
||||
><el-link v-if="!pageReadonly" type="primary" @click="addCustomPeriodRow">添加</el-link></template
|
||||
>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
@@ -263,17 +443,48 @@
|
||||
<div class="dialog-section-title">
|
||||
付款比例设置
|
||||
<el-tooltip content="非必填;配置付款比例时,合计必须等于100%。" placement="top">
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"><el-icon-question-filled /></el-icon>
|
||||
<el-icon style="margin-left: 4px; color: #909399; cursor: pointer"
|
||||
><el-icon-question-filled
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-table :data="paymentRatioRows" border class="change-table">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" />
|
||||
<el-table-column label="付款比例上限(%)" min-width="220"><template #default="{ row }"><el-input-number v-model="row.ratioLimit" :min="0" :max="100" :precision="2" controls-position="right" /></template></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"><template #default="{ row }"><el-input v-model="row.remark" maxlength="200" /></template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{ $index }"><el-link type="danger" @click="paymentRatioRows.splice($index, 1)">删除</el-link></template></el-table-column>
|
||||
<el-table-column label="付款比例上限(%)" min-width="220"
|
||||
><template #default="{ row }"
|
||||
><el-input-number
|
||||
v-model="row.ratioLimit"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/></template
|
||||
></el-table-column>
|
||||
<el-table-column label="备注" min-width="220"
|
||||
><template #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.remark"
|
||||
maxlength="200"
|
||||
:disabled="isSettlementRuleChange"
|
||||
/></template
|
||||
></el-table-column>
|
||||
<el-table-column v-if="!pageReadonly && !isSettlementRuleChange" label="操作" width="100"
|
||||
><template #default="{ $index }"
|
||||
><el-link type="danger" @click="paymentRatioRows.splice($index, 1)"
|
||||
>删除</el-link
|
||||
></template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
<el-button
|
||||
v-if="!pageReadonly && !isSettlementRuleChange"
|
||||
type="primary"
|
||||
plain
|
||||
@click="addPaymentRatioRow"
|
||||
>添加</el-button
|
||||
>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor
|
||||
@@ -290,6 +501,7 @@
|
||||
attachment-type
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:readonly="isSettlementRuleChange"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="attachments"
|
||||
:preview="previewAttachment"
|
||||
@@ -322,8 +534,9 @@
|
||||
@update:rows="changeMaterials = $event"
|
||||
@upload-to-location="handleAttachmentUploadToLocation"
|
||||
/>
|
||||
<div class="page-footer">
|
||||
<el-button @click="$router.back()">取消</el-button>
|
||||
<div v-if="!pageReadonly" class="page-footer">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button v-if="isContractChangeModule" @click="saveDraft">保存草稿</el-button>
|
||||
<el-button type="primary" @click="submit">提交</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -336,7 +549,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/contract-manage';
|
||||
import * as contractApi from '@/api/business/contract-manage';
|
||||
import * as changeApi from '@/api/business/contract-change';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import ContractAttachmentSection, {
|
||||
@@ -374,6 +589,8 @@ const defaultContractFormatOptions = [
|
||||
{ label: '电子合同', value: '电子合同' },
|
||||
{ label: '纸质合同', value: '纸质合同' },
|
||||
];
|
||||
const contractChangeListRoute = '/business/contract-change';
|
||||
const contractChangeListRefreshKey = 'contract-change-list-refresh';
|
||||
const normalizeOptionalPositiveInteger = value => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
const number = Number(value);
|
||||
@@ -470,6 +687,24 @@ export default {
|
||||
'按数量',
|
||||
],
|
||||
rules: {
|
||||
contractNo: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (this.isSettlementRuleChange) {
|
||||
return callback();
|
||||
}
|
||||
const contractNo = String(value || '').trim();
|
||||
if (!contractNo) {
|
||||
return callback(new Error('请输入合同编号'));
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
return callback(new Error('合同编号不能超过100个字符'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
@@ -492,9 +727,39 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
pageTitle() {
|
||||
if (this.pageReadonly) return this.$route.query.name || '查看合同变更';
|
||||
return this.$route.query.name || '合同变更';
|
||||
},
|
||||
isContractChangeModule() {
|
||||
return (
|
||||
this.$route.path === '/business/contract-change/form' ||
|
||||
this.$route.path === '/business/contract-change/public-view'
|
||||
);
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/business/contract-change/public-view';
|
||||
},
|
||||
pageReadonly() {
|
||||
return this.isPublicViewPage || this.$route.query.mode === 'view';
|
||||
},
|
||||
// 高风险/黑名单客商提示:按合同推送合规留痕 hcs_risk_level/hcs_black_status 判定(与合同详情口径一致)
|
||||
riskTipText() {
|
||||
const riskLevel = String(this.form.hcsRiskLevel || '').trim();
|
||||
const blacklist = String(this.form.kingdeeBlackStatus || '').toLowerCase() === 'yes';
|
||||
if (riskLevel !== '2' && !blacklist) return '';
|
||||
const parties = [this.form.partyA, this.form.partyB]
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷。提交变更前须上传「特批附件」类型附件`;
|
||||
},
|
||||
isSettlementRuleChange() {
|
||||
return this.form.changeType === '结算规则变更';
|
||||
},
|
||||
changeAttachmentLocationOptions() {
|
||||
if (this.isSettlementRuleChange) {
|
||||
return ['变更材料'];
|
||||
}
|
||||
return ['用印材料', '附件(佐证材料等)', '变更材料'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
@@ -595,13 +860,25 @@ export default {
|
||||
async load() {
|
||||
const id = this.$route.query.id;
|
||||
if (!id) return;
|
||||
const res = await api.getDetail(id);
|
||||
const data = res.data?.data || res.data || {};
|
||||
let data = {};
|
||||
if (this.isPublicViewPage) {
|
||||
const res = await getMkPublicDetail('contract-change', id);
|
||||
data = res.data?.data || res.data || {};
|
||||
} else if (this.isContractChangeModule) {
|
||||
const res = await changeApi.getDetail(id);
|
||||
data = res.data?.data || res.data || {};
|
||||
} else {
|
||||
const res = await contractApi.getDetail(id);
|
||||
data = res.data?.data || res.data || {};
|
||||
}
|
||||
const isChangeRecord = this.isContractChangeModule || this.isPublicViewPage;
|
||||
this.form = {
|
||||
...data,
|
||||
changeContent: '',
|
||||
changeReason: '',
|
||||
changeAttachmentsJson: '',
|
||||
// 独立变更单保留 contractId;旧路由以合同 id 作为主键
|
||||
contractId: data.contractId || (isChangeRecord ? data.contractId : data.id),
|
||||
changeContent: isChangeRecord ? data.changeContent || '' : '',
|
||||
changeReason: isChangeRecord ? data.changeReason || '' : '',
|
||||
changeAttachmentsJson: data.changeAttachmentsJson || '',
|
||||
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
|
||||
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
|
||||
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
|
||||
@@ -609,14 +886,14 @@ export default {
|
||||
templateFlag: data.templateFlag ?? 0,
|
||||
electronicSealFlag: data.electronicSealFlag ?? 0,
|
||||
archiveStatus: data.archiveStatus || '未归档',
|
||||
changeType: '合同信息变更',
|
||||
changeType: data.changeType || '合同信息变更',
|
||||
};
|
||||
this.changeMaterials = [];
|
||||
this.changeMaterials = this.parse(data.changeAttachmentsJson);
|
||||
this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : [];
|
||||
this.plans = this.parse(data.billingPlanJson);
|
||||
this.attachments = this.mergeChangeAttachments(
|
||||
this.parse(data.attachmentsJson),
|
||||
this.parse(data.changeAttachmentsJson)
|
||||
isChangeRecord ? [] : this.parse(data.changeAttachmentsJson)
|
||||
);
|
||||
this.contractFileRows = normalizeContractFileRows(this.parse(data.contractFileJson));
|
||||
const rules = this.parseObject(data.settlementRuleJson);
|
||||
@@ -658,6 +935,70 @@ export default {
|
||||
data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system');
|
||||
this.paymentRatioRows = this.parse(data.paymentRatioJson);
|
||||
},
|
||||
goBack() {
|
||||
if (this.isContractChangeModule) {
|
||||
this.$router.push(contractChangeListRoute);
|
||||
return;
|
||||
}
|
||||
this.$router.back();
|
||||
},
|
||||
buildChangePayload() {
|
||||
if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule };
|
||||
else this.formalSettlementConfig = { ...this.settlementRule };
|
||||
const settlementRule = {
|
||||
preSettlementConfig: this.preSettlementConfig,
|
||||
formalSettlementConfig: this.formalSettlementConfig,
|
||||
};
|
||||
if (this.isSettlementRuleChange) {
|
||||
return {
|
||||
id: this.form.id,
|
||||
contractId: this.form.contractId,
|
||||
changeType: this.form.changeType,
|
||||
changeContent: this.form.changeContent,
|
||||
changeReason: this.form.changeReason,
|
||||
feeGenerationMode: this.feeGenerationMode,
|
||||
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
|
||||
billingPlanJson: JSON.stringify(this.plans),
|
||||
settlementRuleJson: JSON.stringify(settlementRule),
|
||||
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
|
||||
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
|
||||
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...this.form,
|
||||
contractId: this.form.contractId,
|
||||
changeType: this.form.changeType || '合同信息变更',
|
||||
settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB',
|
||||
copyCount: normalizeOptionalPositiveInteger(this.form.copyCount),
|
||||
invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle),
|
||||
paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays),
|
||||
contractAmount: normalizeOptionalAmount(this.form.contractAmount),
|
||||
startDate: this.period[0],
|
||||
endDate: this.period[1],
|
||||
feeGenerationMode: this.feeGenerationMode,
|
||||
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
|
||||
billingPlanJson: JSON.stringify(this.plans),
|
||||
settlementRuleJson: JSON.stringify(settlementRule),
|
||||
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
|
||||
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
|
||||
paymentRatioJson: JSON.stringify(this.paymentRatioRows),
|
||||
contractFileJson: JSON.stringify(this.contractFileRows),
|
||||
attachmentsJson: JSON.stringify(this.attachments),
|
||||
changeContent: this.form.changeContent,
|
||||
changeReason: this.form.changeReason,
|
||||
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
|
||||
};
|
||||
},
|
||||
async saveDraft() {
|
||||
if (this.form.changeType === '终止合同') {
|
||||
this.$message.warning('终止合同无需保存草稿,请直接提交');
|
||||
return;
|
||||
}
|
||||
const payload = this.buildChangePayload();
|
||||
await changeApi.saveDraft(payload);
|
||||
this.$message.success('草稿已保存');
|
||||
},
|
||||
parse(value) {
|
||||
try {
|
||||
const result = JSON.parse(value || '[]');
|
||||
@@ -720,6 +1061,12 @@ export default {
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (this.isSettlementRuleChange) {
|
||||
if (location === '变更材料') {
|
||||
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
@@ -743,7 +1090,66 @@ export default {
|
||||
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
|
||||
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
|
||||
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected', mkEnabled: await loadMkEnabled().catch(() => false) }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
async submit() {
|
||||
if (this.form.changeType === '终止合同') {
|
||||
const contractId = this.form.contractId || this.form.id;
|
||||
await contractApi.terminate(contractId, this.form.changeReason);
|
||||
this.$message.success('合同已终止');
|
||||
this.goBack();
|
||||
return;
|
||||
}
|
||||
await this.$refs.formRef.validate();
|
||||
if (!this.isSettlementRuleChange) {
|
||||
const contractNo = String(this.form.contractNo || '').trim();
|
||||
if (!contractNo) {
|
||||
this.$message.warning('请输入合同编号');
|
||||
return;
|
||||
}
|
||||
if (contractNo.length > 100) {
|
||||
this.$message.warning('合同编号不能超过100个字符');
|
||||
return;
|
||||
}
|
||||
this.form.contractNo = contractNo;
|
||||
const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0);
|
||||
if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) {
|
||||
this.$message.warning('付款比例上限合计必须等于100%');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule };
|
||||
else this.formalSettlementConfig = { ...this.settlementRule };
|
||||
if (
|
||||
!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') ||
|
||||
!this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const payload = this.buildChangePayload();
|
||||
if (this.isContractChangeModule) {
|
||||
await changeApi.submit(payload);
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'contract-change',
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.contractName || '',
|
||||
approvalStatus: this.form.approvalStatus || '',
|
||||
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||
});
|
||||
sessionStorage.setItem(contractChangeListRefreshKey, '1');
|
||||
this.$message.success('变更已提交');
|
||||
this.$router.push(contractChangeListRoute);
|
||||
return;
|
||||
}
|
||||
await contractApi.submitChange(payload);
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.contractName || '',
|
||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||
mkEnabled: await loadMkEnabled().catch(() => false),
|
||||
});
|
||||
this.$message.success('变更已提交');
|
||||
this.$router.back();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -767,6 +1173,10 @@ export default {
|
||||
}
|
||||
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
|
||||
.contract-basic-section__date-range span { color: #606266; }
|
||||
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字,与合同新增页一致
|
||||
.contract-manage-form__risk-tip { margin: 0 0 18px; padding: 10px 16px; background: #faecd8; border-radius: 4px; }
|
||||
.contract-manage-form__risk-tip-title { color: #f56c6c; font-weight: 600; line-height: 22px; }
|
||||
.contract-manage-form__risk-tip-content { color: #f56c6c; line-height: 22px; }
|
||||
.contract-basic-section__date-range :deep(.el-date-editor) { width: auto; min-width: 0; }
|
||||
.contract-basic-section :deep(.el-form-item) { align-items: center; }
|
||||
.contract-basic-section :deep(.el-form-item__label) { flex: 0 0 96px !important; width: 96px !important; height: auto; padding-right: 12px; white-space: normal; word-break: break-all; line-height: 18px; }
|
||||
|
||||
@@ -32,6 +32,15 @@
|
||||
>
|
||||
批量导出
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hcsImmediateEnabled"
|
||||
type="warning"
|
||||
plain
|
||||
:loading="hcsRepushingId === 'batch'"
|
||||
@click="handleHcsBatchRepush"
|
||||
>
|
||||
<el-icon class="el-icon--left"><Position /></el-icon>批量推送合规
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
@@ -72,6 +81,39 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
<template #hcsSyncStatus="{ row }">
|
||||
<el-tooltip v-if="row.hcsSyncMsg" placement="top" :content="row.hcsSyncMsg">
|
||||
<el-tag :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
|
||||
{{ hcsSyncLabel(row.hcsSyncStatus) }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag v-else-if="row.hcsSyncStatus" :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
|
||||
{{ hcsSyncLabel(row.hcsSyncStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<!-- 客商风险:等级与金蝶黑名单组合展示(推送链路按金蝶命中口径落库),复用公共展示方法;非无风险标签悬浮见明细 -->
|
||||
<template #customerRisk="{ row }">
|
||||
<el-tooltip
|
||||
v-if="contractRiskTagName(row)"
|
||||
placement="top"
|
||||
effect="dark"
|
||||
popper-class="customer-risk-popper"
|
||||
>
|
||||
<template #content>
|
||||
<div>{{ row.hcsRiskDetail || contractRiskTagName(row) }}</div>
|
||||
</template>
|
||||
<el-tag
|
||||
:type="isContractRisky(row) ? 'danger' : row.hcsRiskLevel === '1' ? 'warning' : 'success'"
|
||||
class="status-text"
|
||||
>
|
||||
{{ contractRiskTagName(row) }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
v-if="hasPermission(`${config.permission}_view`)"
|
||||
@@ -92,6 +134,13 @@
|
||||
>
|
||||
{{ operation.label }}
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hcsImmediateEnabled && canHcsRepush(row)"
|
||||
type="warning"
|
||||
@click="handleHcsRepush(row)"
|
||||
>
|
||||
{{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }}
|
||||
</el-link>
|
||||
<el-link v-if="canDelete(row)" type="danger" @click="removeRow(row)">删除</el-link>
|
||||
</template>
|
||||
</avue-crud>
|
||||
@@ -180,9 +229,18 @@
|
||||
<el-descriptions-item label="其他事项说明" :span="3">{{
|
||||
detailValue('remark')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailRiskTipText" label="风险客商提示" :span="3">
|
||||
<span style="color: #f56c6c">{{ detailRiskTipText }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</section>
|
||||
|
||||
<hcs-contract-panel
|
||||
v-if="!isPublicViewPage"
|
||||
:contract="detailRow"
|
||||
@refresh="initDetailPage"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
title="用印材料"
|
||||
description
|
||||
@@ -302,6 +360,7 @@
|
||||
<contract-attachment-section
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
attachment-type
|
||||
readonly
|
||||
:rows="detailAttachmentRows"
|
||||
:preview="previewAttachment"
|
||||
@@ -446,6 +505,7 @@
|
||||
clearable
|
||||
:loading="contractPartyLoading"
|
||||
@visible-change="visible => visible && loadContractPartyOptions()"
|
||||
@change="queryPartyRisk('partyA')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractPartyAOptions"
|
||||
@@ -463,6 +523,7 @@
|
||||
clearable
|
||||
:loading="contractPartyLoading"
|
||||
@visible-change="visible => visible && loadContractPartyOptions()"
|
||||
@change="queryPartyRisk('partyB')"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractPartyBOptions"
|
||||
@@ -617,6 +678,11 @@
|
||||
placeholder="请输入其他事项说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 风险客商提示(基本信息备注后):外部客商高风险/黑名单时展示,文案由后端 riskTip 拼装 -->
|
||||
<div v-if="partyRiskText" class="contract-manage-form__risk-tip">
|
||||
<div class="contract-manage-form__risk-tip-title">风险客商提示:</div>
|
||||
<div class="contract-manage-form__risk-tip-content">{{ partyRiskText }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
@@ -1049,6 +1115,15 @@ import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import * as api from '@/api/business/contract-manage';
|
||||
import { createFromContract } from '@/api/business/contract-change';
|
||||
import {
|
||||
getRisk,
|
||||
loadHcsImmediateEnabled,
|
||||
repush as hcsRepush,
|
||||
repushBatch as hcsRepushBatch,
|
||||
} from '@/api/business/hcs';
|
||||
import { isContractBlack, isRiskyTag, resolveRiskTagName } from '@/utils/risk-display';
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
|
||||
@@ -1069,6 +1144,7 @@ import ContractAttachmentSection, {
|
||||
isContractSupportingAttachmentLocation,
|
||||
normalizeContractFileRows,
|
||||
} from './components/contract-attachment-section.vue';
|
||||
import HcsContractPanel from './components/hcs-contract-panel.vue';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
import { ElImageViewer, ElCascader } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
@@ -1174,6 +1250,11 @@ const createCopiedDetail = (detail = {}) => {
|
||||
Object.keys(form).forEach(prop => {
|
||||
if (Object.prototype.hasOwnProperty.call(detail, prop)) form[prop] = detail[prop];
|
||||
});
|
||||
// 特批附件属于原合同的特批佐证,复制后须对新合同重新上传,否则会绕过高风险/黑名单校验
|
||||
const stripSpecAttachments = value => {
|
||||
const rows = parseArray(value).filter(item => item?.fileType !== '特批附件');
|
||||
return rows.length ? JSON.stringify(rows) : '';
|
||||
};
|
||||
return {
|
||||
...form,
|
||||
contractNo: '',
|
||||
@@ -1181,8 +1262,8 @@ const createCopiedDetail = (detail = {}) => {
|
||||
archiveStatus: '未归档',
|
||||
feeGenerationMode: detail.feeGenerationMode || '',
|
||||
billingEnabled: detail.billingEnabled,
|
||||
contractFileJson: detail.contractFileJson || '',
|
||||
attachmentsJson: detail.attachmentsJson || '',
|
||||
contractFileJson: stripSpecAttachments(detail.contractFileJson),
|
||||
attachmentsJson: stripSpecAttachments(detail.attachmentsJson),
|
||||
billingPlanJson: detail.billingPlanJson || '',
|
||||
paymentRatioJson: detail.paymentRatioJson || '',
|
||||
settlementRuleJson: detail.settlementRuleJson || '',
|
||||
@@ -1241,9 +1322,11 @@ export default {
|
||||
ContractAttachmentSection,
|
||||
BillingPlanEditor,
|
||||
ChangeRecordDetailDialog,
|
||||
HcsContractPanel,
|
||||
ElImageViewer,
|
||||
OpenFileViewer,
|
||||
PdfPreview,
|
||||
Position,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -1335,6 +1418,9 @@ export default {
|
||||
contractCategoryDictOptions: [],
|
||||
contractPartyAOptions: [],
|
||||
contractPartyBOptions: [],
|
||||
// 合规客商风险(C101-1 展示用):甲方/乙方选择后即时查询
|
||||
contractPartyCodeMap: {},
|
||||
partyRisk: {},
|
||||
contractPartyLoading: false,
|
||||
contractPartyRequestId: 0,
|
||||
organizationOptions: [],
|
||||
@@ -1373,6 +1459,9 @@ export default {
|
||||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||
detailLoading: false,
|
||||
detailRow: {},
|
||||
// 合规(HCS)推送/对账状态;受 thirdParty.hcs.push.immediate-enabled 控制显隐
|
||||
hcsRepushingId: null,
|
||||
hcsImmediateEnabled: false,
|
||||
detailContractFileRows: [],
|
||||
detailAttachmentRows: [],
|
||||
detailBillingPlanRows: [],
|
||||
@@ -1549,6 +1638,32 @@ export default {
|
||||
const endDate = this.detailRow.endDate || '-';
|
||||
return `${startDate} 至 ${endDate}`;
|
||||
},
|
||||
// 高风险/黑名单客商签约提示(C101-1,签约前提示;强制拦截在后端提交校验与推送链路)
|
||||
partyRiskText() {
|
||||
const parts = [];
|
||||
['partyA', 'partyB'].forEach(prop => {
|
||||
const risk = this.partyRisk[prop];
|
||||
if (!risk?.risky) return;
|
||||
// riskTip 由后端按风险项拼装(【公司名称】:黑名单;涉诉;被执行人等),缺失时兜底通用提示
|
||||
parts.push(
|
||||
risk.riskTip ||
|
||||
`${prop === 'partyA' ? '甲方' : '乙方'}[${this.form[prop] || ''}]为高风险/黑名单客商`
|
||||
);
|
||||
});
|
||||
if (!parts.length) return '';
|
||||
return `${parts.join(';')},按集团规定不可直接签约,提交前须上传「特批附件」类型附件,审批通过后将自动发起合同特批`;
|
||||
},
|
||||
// 详情风险客商提示(基本信息备注后展示):按推送留痕 hcs_risk_level/hcs_black_status 判定(甲乙取高/任一),文案与后端 riskTip 一致
|
||||
detailRiskTipText() {
|
||||
const riskLevel = String(this.detailRow.hcsRiskLevel || '').trim();
|
||||
const blacklist = String(this.detailRow.kingdeeBlackStatus || '').toLowerCase() === 'yes';
|
||||
if (riskLevel !== '2' && !blacklist) return '';
|
||||
const parties = [this.detailRow.partyA, this.detailRow.partyB]
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
return `【${parties || '相对方'}】:被列入外部黑名单;涉贪污受贿裁判相关提及方(3年以上及其他);被执行人信息;买卖合同纠纷;劳动纠纷;重大纠纷`;
|
||||
},
|
||||
detailLegalSealText() {
|
||||
const value = this.detailRow.legalSealFlag;
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
@@ -1617,6 +1732,13 @@ export default {
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
loadHcsImmediateEnabled()
|
||||
.then(enabled => {
|
||||
this.hcsImmediateEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.hcsImmediateEnabled = false;
|
||||
});
|
||||
}
|
||||
if (this.isFormPage) this.initFormPage();
|
||||
if (this.isDetailPage) this.initDetailPage();
|
||||
@@ -1670,6 +1792,100 @@ export default {
|
||||
if (stage === 'terminated') return 'danger';
|
||||
return 'info';
|
||||
},
|
||||
// ==================== 合规(HCS)推送/对账(照“同步金蝶”按钮模式) ====================
|
||||
hcsSyncLabel(value) {
|
||||
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
|
||||
},
|
||||
// ==================== 客商风险(推送链路按金蝶黑名单命中口径落库,展示复用公共方法) ====================
|
||||
contractRiskTagName(row) {
|
||||
return resolveRiskTagName(row.hcsRiskLevel, isContractBlack(row.kingdeeBlackStatus));
|
||||
},
|
||||
isContractRisky(row) {
|
||||
return isRiskyTag(row.hcsRiskLevel, isContractBlack(row.kingdeeBlackStatus));
|
||||
},
|
||||
hcsSyncTagType(value) {
|
||||
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
|
||||
},
|
||||
canHcsRepush(row) {
|
||||
return (
|
||||
this.hcsImmediateEnabled &&
|
||||
['approved', 'change_approved'].includes(row.approvalStatus) &&
|
||||
row.hcsSyncStatus !== 'synced'
|
||||
);
|
||||
},
|
||||
handleHcsRepush(row) {
|
||||
if (this.hcsRepushingId) return;
|
||||
this.$confirm('确定推送该数据到合同合规系统?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = row.id;
|
||||
hcsRepush('contract', row.id)
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '推送合规失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(res.data?.data || '推送合规成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
handleHcsBatchRepush() {
|
||||
if (this.hcsRepushingId) return;
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const invalidCount = this.selectionList.filter(item => !this.canHcsRepush(item)).length;
|
||||
if (invalidCount) {
|
||||
this.$message.warning(`选中数据中有${invalidCount}条不符合推送条件(仅审批通过且未同步的可推送),请重新选择`);
|
||||
return;
|
||||
}
|
||||
this.$confirm(`确定将选中的${this.selectionList.length}条数据推送到合同合规系统?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在批量推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = 'batch';
|
||||
hcsRepushBatch('contract', this.selectionList.map(item => item.id))
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '批量推送合规失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(res.data?.data || '批量推送合规完成');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
canEdit(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
@@ -2246,8 +2462,21 @@ export default {
|
||||
])
|
||||
.then(responses => {
|
||||
if (requestId !== this.contractPartyRequestId) return;
|
||||
const names = responses
|
||||
.flatMap(extractRecords)
|
||||
const records = responses.flatMap(extractRecords);
|
||||
// 名称→客商编码映射,供 C101-1 风险查询使用(§9-U6 编码体系联调确认)
|
||||
const codeMap = {};
|
||||
records.forEach(item => {
|
||||
const code = item.customerCode;
|
||||
if (!code) return;
|
||||
[item.fullName, item.shortName, item.customerName]
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean)
|
||||
.forEach(name => {
|
||||
codeMap[name] = code;
|
||||
});
|
||||
});
|
||||
this.contractPartyCodeMap = codeMap;
|
||||
const names = records
|
||||
.map(item => item.fullName || item.shortName || item.customerName || item.customerCode)
|
||||
.map(name => String(name || '').trim())
|
||||
.filter(Boolean);
|
||||
@@ -2255,11 +2484,40 @@ export default {
|
||||
const options = [...new Set(names)].map(name => ({ label: name, value: name }));
|
||||
this.contractPartyAOptions = options;
|
||||
this.contractPartyBOptions = options;
|
||||
// 复制/编辑回填的甲乙方不会触发下拉 change,加载选项后主动查一次 C101-1 风险
|
||||
['partyA', 'partyB'].forEach(prop => {
|
||||
if (this.form[prop]) this.queryPartyRisk(prop);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === this.contractPartyRequestId) this.contractPartyLoading = false;
|
||||
});
|
||||
},
|
||||
// C101-1 客商风险查询(展示用,强校验在后端提交/推送链路):失败静默不阻塞填写
|
||||
queryPartyRisk(prop) {
|
||||
const name = this.form[prop];
|
||||
const code = name ? this.contractPartyCodeMap[name] : null;
|
||||
if (!code) {
|
||||
this.partyRisk = { ...this.partyRisk, [prop]: null };
|
||||
return;
|
||||
}
|
||||
getRisk(code)
|
||||
.then(res => {
|
||||
const risk = res?.data?.data ?? res?.data ?? {};
|
||||
this.partyRisk = {
|
||||
...this.partyRisk,
|
||||
[prop]: {
|
||||
customerKind: risk.customerKind || '',
|
||||
// risky 由后端统一判定:外部客商高风险(等级=2)或黑名单;内部组织客商默认无风险恒为 false
|
||||
risky: risk.risky === true,
|
||||
riskTip: risk.riskTip || '',
|
||||
},
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
this.partyRisk = { ...this.partyRisk, [prop]: null };
|
||||
});
|
||||
},
|
||||
loadOrganizationOptions() {
|
||||
if (this.organizationLoading) return;
|
||||
this.organizationLoading = true;
|
||||
@@ -3011,12 +3269,33 @@ export default {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
return options.find(item => String(item.value) === String(value))?.label || value;
|
||||
},
|
||||
handleStartContractChange(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('合同ID为空,无法发起变更');
|
||||
return;
|
||||
}
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在创建变更单…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
createFromContract(row.id)
|
||||
.then(res => {
|
||||
const data = res?.data?.data || res?.data || {};
|
||||
if (!data.id) {
|
||||
this.$message.error(res?.data?.msg || '创建合同变更草稿失败');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/business/contract-change/form',
|
||||
query: { mode: 'edit', id: data.id, name: '合同变更' },
|
||||
});
|
||||
})
|
||||
.finally(() => loading.close());
|
||||
},
|
||||
handleOperation(operation, row) {
|
||||
if (operation.action === 'startChange') {
|
||||
this.$router.push({
|
||||
path: '/business/contract-manage/change',
|
||||
query: { id: row.id, name: '合同变更' },
|
||||
});
|
||||
this.handleStartContractChange(row);
|
||||
return;
|
||||
}
|
||||
if (operation.action === 'flow') {
|
||||
@@ -3046,10 +3325,26 @@ export default {
|
||||
})
|
||||
)
|
||||
: request();
|
||||
afterMk.then(() => {
|
||||
this.$message.success(operation.successMessage || `${operation.label}成功`);
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
// 合同通过会同步等待附件转传 + 合规推送,加遮罩防重复点击
|
||||
const loading =
|
||||
operation.action === 'approve'
|
||||
? this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
})
|
||||
: null;
|
||||
afterMk
|
||||
.then(res => {
|
||||
// data=false 表示审批已通过但合规推送失败,msg 带具体原因(照正式结算单审批)
|
||||
if (operation.action === 'approve' && res?.data?.data === false) {
|
||||
this.$message.warning(res.data.msg);
|
||||
} else {
|
||||
this.$message.success(operation.successMessage || `${operation.label}成功`);
|
||||
}
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => loading && loading.close());
|
||||
};
|
||||
if (operation.prompt) {
|
||||
this.$prompt(operation.prompt, operation.label, {
|
||||
@@ -3146,6 +3441,29 @@ export default {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.contract-manage-form__risk-alert {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 风险客商提示条(基本信息备注后):橙黄背景 + 红色文字(设计稿 2026-10)
|
||||
.contract-manage-form__risk-tip {
|
||||
margin: 0 0 18px;
|
||||
padding: 10px 16px;
|
||||
background: #faecd8;
|
||||
border-radius: 4px;
|
||||
|
||||
.contract-manage-form__risk-tip-title {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.contract-manage-form__risk-tip-content {
|
||||
color: #f56c6c;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.contract-manage-page {
|
||||
&__expiry-search {
|
||||
display: flex;
|
||||
@@ -3530,4 +3848,10 @@ export default {
|
||||
white-space: normal;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 风险客商悬浮提示(同客商列表页):popper 挂在 body 下,约束宽度 */
|
||||
.customer-risk-popper {
|
||||
max-width: 360px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<basic-container class="hcs-master-data-page">
|
||||
<div class="hcs-master-data-page__head">
|
||||
<div class="dialog-section-title">合规主数据(HCS)</div>
|
||||
<el-button type="primary" :loading="refreshing" @click="handleRefresh">刷新缓存</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="合同模板" name="template" />
|
||||
</el-tabs>
|
||||
|
||||
<el-table v-loading="loading" :data="templates" border>
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column prop="fdId" label="模板ID(fdTemplateId)" min-width="320" show-overflow-tooltip />
|
||||
<el-table-column prop="fdName" label="模板名称" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="fdStatus" label="状态" width="100" align="center" />
|
||||
<el-table-column prop="fdContractCharacter" label="合同性质" min-width="140" align="center" />
|
||||
<el-table-column label="创建时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ formatTime(row.fdCreateTime) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-alert
|
||||
class="hcs-master-data-page__tip"
|
||||
title="合同类别与合规模板的对应关系由 Nacos 配置 thirdParty.hcs.contract.template-mapping 维护;此列表用于核对映射的模板ID 是否存在。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getTemplates, refreshMasterData } from '@/api/business/hcs';
|
||||
|
||||
export default {
|
||||
name: 'HcsMasterData',
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'template',
|
||||
loading: false,
|
||||
refreshing: false,
|
||||
templates: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.loadTemplates();
|
||||
},
|
||||
methods: {
|
||||
loadTemplates() {
|
||||
this.loading = true;
|
||||
getTemplates()
|
||||
.then(response => {
|
||||
const data = response?.data?.data ?? response?.data;
|
||||
this.templates = Array.isArray(data) ? data : [];
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('合规模板列表加载失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
handleRefresh() {
|
||||
this.refreshing = true;
|
||||
refreshMasterData()
|
||||
.then(response => {
|
||||
this.$message.success(response?.data?.data || '合规主数据缓存已刷新');
|
||||
this.loadTemplates();
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error(error?.response?.data?.msg || '刷新失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshing = false;
|
||||
});
|
||||
},
|
||||
formatTime(value) {
|
||||
if (!value) return '-';
|
||||
return this.$dayjs(Number(value)).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hcs-master-data-page__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.dialog-section-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hcs-master-data-page__tip {
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -635,10 +635,11 @@
|
||||
min-width="100" /><el-table-column
|
||||
prop="quantity"
|
||||
label="数量"
|
||||
min-width="90" /><el-table-column
|
||||
prop="quantityUnit"
|
||||
label="数量单位"
|
||||
min-width="100" /><el-table-column
|
||||
min-width="120"
|
||||
><template #default="{ row }">{{
|
||||
formatCargoQuantityDisplay(row)
|
||||
}}</template></el-table-column
|
||||
><el-table-column
|
||||
prop="materialCode"
|
||||
label="物料编码"
|
||||
min-width="120" /><el-table-column
|
||||
@@ -1068,31 +1069,12 @@
|
||||
<el-option
|
||||
v-for="item in driverOptions"
|
||||
:key="item.id || item.driverName || item.name"
|
||||
:label="item.driverName || item.name"
|
||||
:label="driverOptionText(item)"
|
||||
:value="item.driverName || item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="运输方式">
|
||||
<el-select
|
||||
v-model="candidateQuery.transportType"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="请选择"
|
||||
:disabled="dialogMode === 'add'"
|
||||
@visible-change="visible => visible && loadTransportTypeOptions()"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in transportTypeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
<el-col
|
||||
:span="candidateSearchVisible ? 24 : 6"
|
||||
@@ -1201,6 +1183,16 @@
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="carrierContractName"
|
||||
label="承运商合同"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.carrierContractName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="planName"
|
||||
label="计划名称"
|
||||
@@ -1357,19 +1349,29 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<template #default="{ row, $index }">
|
||||
<el-link
|
||||
v-if="!dialogReadonly"
|
||||
v-if="!dialogReadonly && !isSelectedWaybillRemoveLocked(row)"
|
||||
type="danger"
|
||||
@click="removeSelectedWaybill($index)"
|
||||
>删除</el-link
|
||||
>
|
||||
<span
|
||||
v-else-if="!dialogReadonly && isSelectedWaybillRemoveLocked(row)"
|
||||
class="loading-manage-dialog__locked-tip"
|
||||
>已打卡</span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
|
||||
<section-card title="线路编辑">
|
||||
<template v-if="isRunningEdit" #extra>
|
||||
<span class="loading-manage-dialog__route-hint"
|
||||
>已打卡地点置灰不可拖拽,新增地址仅可在未打卡节点间调整</span
|
||||
>
|
||||
</template>
|
||||
<div class="loading-manage-dialog__route-list" v-if="routeNodes.length">
|
||||
<div
|
||||
v-for="(node, index) in routeNodes"
|
||||
@@ -1415,8 +1417,9 @@
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="数量单位" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="120">
|
||||
<template #default="{ row }">{{ formatCargoQuantityDisplay(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||
@@ -1436,7 +1439,7 @@
|
||||
<el-segmented
|
||||
v-model="dialogForm.carrierType"
|
||||
:options="carrierTypeOptions"
|
||||
:disabled="dialogReadonly"
|
||||
:disabled="dialogReadonly || isRunningTaskRequiredLocked"
|
||||
@change="handleCarrierTypeChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -1450,7 +1453,11 @@
|
||||
"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="dialogReadonly || !selectedWaybillRows.length"
|
||||
:disabled="
|
||||
dialogReadonly ||
|
||||
isRunningTaskRequiredLocked ||
|
||||
!selectedWaybillRows.length
|
||||
"
|
||||
:loading="taskCarrierLoading"
|
||||
:placeholder="taskCarrierPlaceholder"
|
||||
@change="handleTaskCarrierChange"
|
||||
@@ -1483,20 +1490,40 @@
|
||||
:fetch-suggestions="fetchDriverSuggestions"
|
||||
:loading="driverLoading"
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly || isRunningSelfDriverLocked"
|
||||
@select="item => handleDriverSuggestionSelect('dialog', item)"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="手机号"
|
||||
:required="dialogForm.carrierType === '自运'"
|
||||
>
|
||||
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.driverPhone"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly || isRunningSelfDriverLocked"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" required>
|
||||
<el-input v-model="dialogForm.vehicleNo" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.vehicleNo"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly || isRunningTaskRequiredLocked"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="挂车车牌号">
|
||||
<el-input v-model="dialogForm.trailerVehicleNo" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.trailerVehicleNo"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人">
|
||||
<el-autocomplete
|
||||
@@ -1506,11 +1533,21 @@
|
||||
:fetch-suggestions="fetchEscortSuggestions"
|
||||
:loading="escortLoading"
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly"
|
||||
@select="item => handleEscortSuggestionSelect('dialog', item)"
|
||||
/>
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span>{{ driverOptionText(item) }}</span>
|
||||
</template>
|
||||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogForm.carrierType !== '承运商'" label="押运人手机号">
|
||||
<el-input v-model="dialogForm.escortPhone" clearable placeholder="请输入" />
|
||||
<el-input
|
||||
v-model="dialogForm.escortPhone"
|
||||
clearable
|
||||
placeholder="请输入"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="里程(km)">
|
||||
<el-input
|
||||
@@ -1528,6 +1565,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="预计完成日期">
|
||||
@@ -1537,6 +1575,7 @@
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="loading-manage-dialog__span-2">
|
||||
@@ -1547,6 +1586,7 @@
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入,500字以内"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -1561,11 +1601,17 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="closeLoadingDialog">关闭</el-button>
|
||||
<el-button type="primary" plain :loading="dialogSaving === 'draft'" @click="saveDraft">
|
||||
<el-button
|
||||
v-if="!isRunningEdit"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="dialogSaving === 'draft'"
|
||||
@click="saveDraft"
|
||||
>
|
||||
暂存
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="dialogSaving === 'submit'" @click="submitLoading">
|
||||
{{ dialogMode === 'reassign' ? '确认派单' : '确认' }}
|
||||
{{ dialogMode === 'reassign' ? '确认派单' : isRunningEdit ? '保存' : '确认' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1823,7 +1869,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { fetchDriverCandidates, driverOptionText } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
getList as getProcessConfigList,
|
||||
@@ -1848,7 +1894,6 @@ import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-sear
|
||||
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||
|
||||
const defaultTransportType = '';
|
||||
const defaultCandidateTransportType = '';
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
|
||||
@@ -1889,7 +1934,6 @@ const createCandidateSearchForm = () => ({
|
||||
vehicleNo: '',
|
||||
carrierName: '',
|
||||
driverName: '',
|
||||
transportType: defaultCandidateTransportType,
|
||||
});
|
||||
|
||||
const createDialogForm = () => ({
|
||||
@@ -2107,6 +2151,8 @@ export default {
|
||||
routeNodes: [],
|
||||
cargoRows: [],
|
||||
routeDragIndex: -1,
|
||||
/** 进行中编辑:子运单打卡记录映射 waybillId -> records[] */
|
||||
waybillPunchMap: {},
|
||||
transportTypeOptions: [],
|
||||
projectOptions: [],
|
||||
customerOptions: [],
|
||||
@@ -2170,6 +2216,23 @@ export default {
|
||||
dialogReadonly() {
|
||||
return this.dialogMode === 'view';
|
||||
},
|
||||
/** 进行中配载编辑:可加子单/调未打卡路线,但任务必填字段锁定 */
|
||||
isRunningEdit() {
|
||||
return (
|
||||
this.dialogMode === 'edit' &&
|
||||
String(this.dialogForm.businessStatus || '') === 'running'
|
||||
);
|
||||
},
|
||||
/** 进行中任务必填字段(已派车)不可改 */
|
||||
isRunningTaskRequiredLocked() {
|
||||
return this.isRunningEdit;
|
||||
},
|
||||
/** 自运进行中:司机/手机号同属必填派车信息,不可改 */
|
||||
isRunningSelfDriverLocked() {
|
||||
return (
|
||||
this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '自运'
|
||||
);
|
||||
},
|
||||
// 打卡时间轴:已打卡按时间升序,未打卡保持原节点顺序排在其后
|
||||
orderedPunchRecords() {
|
||||
const records = Array.isArray(this.punchRecords) ? this.punchRecords : [];
|
||||
@@ -2325,6 +2388,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
parseRouteChangeContent,
|
||||
driverOptionText,
|
||||
formatTaskCarrierContractLabel(item = {}) {
|
||||
const carrierName = item.carrierName || item.partyB || '';
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
@@ -2913,6 +2977,7 @@ export default {
|
||||
if (status === 'running') {
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'complete', label: '完成' },
|
||||
];
|
||||
}
|
||||
@@ -3014,7 +3079,12 @@ export default {
|
||||
query: {
|
||||
mode,
|
||||
...(mode === 'edit' && row?.id ? { id: row.id } : {}),
|
||||
name: mode === 'edit' ? '编辑配载管理' : '新增配载管理',
|
||||
name:
|
||||
mode === 'edit'
|
||||
? row?.businessStatus === 'running'
|
||||
? '编辑进行中配载'
|
||||
: '编辑配载管理'
|
||||
: '新增配载管理',
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -3022,7 +3092,6 @@ export default {
|
||||
this.dialogMode = mode;
|
||||
this.dialogLoading = true;
|
||||
this.resetDialogData();
|
||||
if (mode === 'add') this.candidateQuery.transportType = 'road';
|
||||
try {
|
||||
if (row?.id) {
|
||||
const res = await loadingApi.getDetail(row.id);
|
||||
@@ -3037,6 +3106,9 @@ export default {
|
||||
this.dialogForm.loadingSubNos
|
||||
);
|
||||
this.syncProcessWaybillOptions();
|
||||
if (this.isRunningEdit) {
|
||||
await this.refreshWaybillPunchMap();
|
||||
}
|
||||
this.restoreRouteAndCargo();
|
||||
this.restoreRouteChangeRecords();
|
||||
this.loadPunchRecords();
|
||||
@@ -3046,7 +3118,6 @@ export default {
|
||||
} else {
|
||||
this.dialogForm = createDialogForm();
|
||||
this.candidateQuery = createCandidateSearchForm();
|
||||
this.candidateQuery.transportType = 'road';
|
||||
await this.loadCandidateWaybills();
|
||||
}
|
||||
} finally {
|
||||
@@ -3071,6 +3142,8 @@ export default {
|
||||
this.taskCarrierLoading = false;
|
||||
this.routeNodes = [];
|
||||
this.cargoRows = [];
|
||||
this.routeDragIndex = -1;
|
||||
this.waybillPunchMap = {};
|
||||
this.candidatePage.currentPage = 1;
|
||||
this.candidatePage.total = 0;
|
||||
this.candidateSearchVisible = false;
|
||||
@@ -3435,20 +3508,144 @@ export default {
|
||||
const statusName = String(row.businessStatusName || row.statusName || '');
|
||||
return statusName.includes('已完成');
|
||||
},
|
||||
isPunchRecordDone(record = {}) {
|
||||
if (record.punched === false || record.statusName === '未打卡') return false;
|
||||
return Boolean(record.punched || record.punchTime || record.punchAt || record.punchDateTime);
|
||||
},
|
||||
isLoadPunchRecord(record = {}) {
|
||||
if (!this.isPunchRecordDone(record)) return false;
|
||||
const code = String(record.nodeCode || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const name = String(record.nodeName || '').trim();
|
||||
return code === 'load' || name.includes('装货');
|
||||
},
|
||||
isUnloadPunchRecord(record = {}) {
|
||||
if (!this.isPunchRecordDone(record)) return false;
|
||||
const code = String(record.nodeCode || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const name = String(record.nodeName || '').trim();
|
||||
return (
|
||||
code === 'unload' ||
|
||||
name.includes('卸货') ||
|
||||
code === 'arrival' ||
|
||||
name.includes('到货') ||
|
||||
code === 'sign' ||
|
||||
name.includes('签收')
|
||||
);
|
||||
},
|
||||
waybillPunchRecords(row = {}) {
|
||||
const key = String(row?.id || '');
|
||||
if (!key) return [];
|
||||
const records = this.waybillPunchMap[key];
|
||||
return Array.isArray(records) ? records : [];
|
||||
},
|
||||
isWaybillLoadPunched(row = {}) {
|
||||
return this.waybillPunchRecords(row).some(record => this.isLoadPunchRecord(record));
|
||||
},
|
||||
isWaybillUnloadPunched(row = {}) {
|
||||
return this.waybillPunchRecords(row).some(record => this.isUnloadPunchRecord(record));
|
||||
},
|
||||
isWaybillPunchLocked(row = {}) {
|
||||
return this.isWaybillLoadPunched(row) || this.isWaybillUnloadPunched(row);
|
||||
},
|
||||
isSelectedWaybillRemoveLocked(row = {}) {
|
||||
if (this.isWaybillCompleted(row)) return true;
|
||||
return this.isRunningEdit && this.isWaybillPunchLocked(row);
|
||||
},
|
||||
isRouteNodeTagLocked(tag, rows = this.selectedWaybillRows) {
|
||||
const match = String(tag || '').match(/^(装|卸)(\d+)$/);
|
||||
if (!match) return false;
|
||||
const row = rows[Number(match[2]) - 1];
|
||||
if (!row) return false;
|
||||
if (this.isRunningEdit) {
|
||||
return match[1] === '装' ? this.isWaybillLoadPunched(row) : this.isWaybillUnloadPunched(row);
|
||||
}
|
||||
return this.isWaybillCompleted(row);
|
||||
},
|
||||
isRouteChangeNodeLocked(node = {}) {
|
||||
return Boolean(node?.locked);
|
||||
},
|
||||
syncRouteNodeLockedFlags(nodes = [], rows = this.selectedWaybillRows) {
|
||||
return (nodes || []).map(node => {
|
||||
const locked = (node.tags || []).some(tag => {
|
||||
const match = String(tag).match(/^(装|卸)(\d+)$/);
|
||||
if (!match) return false;
|
||||
const row = rows[Number(match[2]) - 1];
|
||||
return row ? this.isWaybillCompleted(row) : false;
|
||||
});
|
||||
const locked = (node.tags || []).some(tag => this.isRouteNodeTagLocked(tag, rows));
|
||||
return { ...node, locked };
|
||||
});
|
||||
},
|
||||
async refreshWaybillPunchMap() {
|
||||
const rows = (this.selectedWaybillRows || []).filter(row => row?.id);
|
||||
if (!rows.length) {
|
||||
this.waybillPunchMap = {};
|
||||
return;
|
||||
}
|
||||
const entries = await Promise.all(
|
||||
rows.map(async row => {
|
||||
try {
|
||||
const res = await getWaybillPunchRecords(row.id);
|
||||
const data = res?.data?.data || res?.data || {};
|
||||
return [String(row.id), Array.isArray(data.records) ? data.records : []];
|
||||
} catch (error) {
|
||||
return [String(row.id), []];
|
||||
}
|
||||
})
|
||||
);
|
||||
this.waybillPunchMap = Object.fromEntries(entries);
|
||||
},
|
||||
/**
|
||||
* 进行中编辑:已打卡地点保持在前且不可插入,新增地址只能排在已打卡节点之后。
|
||||
*/
|
||||
mergeRouteNodesKeepingPunched(prevNodes = [], builtNodes = []) {
|
||||
const prev = Array.isArray(prevNodes) ? prevNodes : [];
|
||||
const built = Array.isArray(builtNodes) ? builtNodes : [];
|
||||
if (!this.isRunningEdit) {
|
||||
return built;
|
||||
}
|
||||
const builtByAddress = new Map();
|
||||
built.forEach(node => {
|
||||
if (node?.address && !builtByAddress.has(node.address)) {
|
||||
builtByAddress.set(node.address, node);
|
||||
}
|
||||
});
|
||||
const lockedAddresses = new Set(
|
||||
built.filter(node => node?.locked && node.address).map(node => node.address)
|
||||
);
|
||||
const used = new Set();
|
||||
const lockedResult = [];
|
||||
prev.forEach(node => {
|
||||
if (!node?.address || !lockedAddresses.has(node.address) || used.has(node.address)) {
|
||||
return;
|
||||
}
|
||||
const latest = builtByAddress.get(node.address);
|
||||
if (!latest) return;
|
||||
lockedResult.push({ ...latest, locked: true });
|
||||
used.add(node.address);
|
||||
});
|
||||
built.forEach(node => {
|
||||
if (!node?.locked || !node.address || used.has(node.address)) return;
|
||||
lockedResult.push({ ...node, locked: true });
|
||||
used.add(node.address);
|
||||
});
|
||||
const unlockedResult = [];
|
||||
prev.forEach(node => {
|
||||
if (!node?.address || lockedAddresses.has(node.address) || used.has(node.address)) {
|
||||
return;
|
||||
}
|
||||
const latest = builtByAddress.get(node.address);
|
||||
if (!latest) return;
|
||||
unlockedResult.push({ ...latest, locked: false });
|
||||
used.add(node.address);
|
||||
});
|
||||
built.forEach(node => {
|
||||
if (node?.locked || !node?.address || used.has(node.address)) return;
|
||||
unlockedResult.push({ ...node, locked: false });
|
||||
used.add(node.address);
|
||||
});
|
||||
return [...lockedResult, ...unlockedResult].map((node, index) => ({
|
||||
...node,
|
||||
key: `${node.address}-${index}`,
|
||||
}));
|
||||
},
|
||||
openRouteChangeMapDialog(row, field) {
|
||||
if (this.isWaybillCompleted(row)) return;
|
||||
this.routeChangeMapTarget = { row, field };
|
||||
@@ -3699,20 +3896,24 @@ export default {
|
||||
async loadCandidateWaybills() {
|
||||
this.candidateLoading = true;
|
||||
try {
|
||||
const selectedProjectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
|
||||
const query = {
|
||||
...this.buildQueryParams(this.candidateQuery),
|
||||
// 待执行(需接单未接)与进行中(无需接单或已接单)均可配载
|
||||
businessStatus: 'pending,running',
|
||||
onlyUnassignedLoading: 1,
|
||||
// 仅查当前账户所属组织及子组织下的运单
|
||||
// 公路、待执行、未配载的判定都在后端可配载开关内
|
||||
// 该开关已把候选限定为公路,再按精确运输方式取值筛会漏掉「公路运输」这类历史写法
|
||||
loadableOnly: 1,
|
||||
// allDept=0 表示不指定组织:超管与「数据权限范围=全部组织」的账户后端不过滤组织,
|
||||
// 其余账户限定本组织(不展开下级)
|
||||
allDept: 0,
|
||||
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
|
||||
};
|
||||
const transportType = this.getCandidateTransportTypeQuery(query.transportType);
|
||||
// 已选运单后,候选列表优先限定同一项目,便于再按承运商合同乙方交集筛选
|
||||
if (selectedProjectId && !query.projectId && !query.projectName) {
|
||||
query.projectId = selectedProjectId;
|
||||
}
|
||||
const res = await getWaybillList(
|
||||
this.candidatePage.currentPage,
|
||||
this.candidatePage.pageSize,
|
||||
{ ...query, transportType }
|
||||
query
|
||||
);
|
||||
const page = unwrapPage(res);
|
||||
this.candidateRows = page.records;
|
||||
@@ -3721,20 +3922,12 @@ export default {
|
||||
this.candidateLoading = false;
|
||||
}
|
||||
},
|
||||
getCandidateTransportTypeQuery(value) {
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (['road', '公路运输'].includes(normalized)) return 'road,公路运输';
|
||||
return value || '';
|
||||
},
|
||||
handleCandidateSearch() {
|
||||
this.candidatePage.currentPage = 1;
|
||||
this.loadCandidateWaybills();
|
||||
},
|
||||
handleCandidateReset() {
|
||||
this.candidateQuery = createCandidateSearchForm();
|
||||
if (this.dialogMode === 'add') this.candidateQuery.transportType = 'road';
|
||||
this.candidatePage.currentPage = 1;
|
||||
this.loadCandidateWaybills();
|
||||
},
|
||||
@@ -3766,19 +3959,89 @@ export default {
|
||||
}
|
||||
})
|
||||
);
|
||||
if (this.isRunningEdit && String(this.dialogForm.carrierType || '').trim() === '承运商') {
|
||||
const contractId = this.dialogForm.carrierContractId;
|
||||
if (contractId) {
|
||||
const projectIds = [
|
||||
...new Set(
|
||||
[...this.selectedWaybillRows, ...selectedRows]
|
||||
.map(row => row.projectId)
|
||||
.filter(Boolean)
|
||||
),
|
||||
];
|
||||
if (projectIds.length) {
|
||||
try {
|
||||
const waybillIds = [...this.selectedWaybillRows, ...selectedRows]
|
||||
.map(row => row.id)
|
||||
.filter(Boolean);
|
||||
const response = await loadingApi.getCarrierContracts(projectIds, waybillIds);
|
||||
const contracts = response?.data?.data || response?.data || [];
|
||||
const stillAllowed = (Array.isArray(contracts) ? contracts : []).some(
|
||||
item => String(item.id) === String(contractId)
|
||||
);
|
||||
if (!stillAllowed) {
|
||||
ElMessage.warning('新增子运单须兼容当前配载单已选定的承运商合同');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// 合同校验失败时仍允许添加,最终由后端确认拦截
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const exists = new Set(
|
||||
this.selectedWaybillRows.map(row => String(row.id || row.waybillNo))
|
||||
);
|
||||
// 一张配载单的全部子运单必须同项目:以已选运单(无已选时以本批首张)的项目为准
|
||||
const anchorRow = [...this.selectedWaybillRows, ...selectedRows].find(row =>
|
||||
this.waybillProjectKey(row)
|
||||
);
|
||||
const anchorKey = anchorRow ? this.waybillProjectKey(anchorRow) : '';
|
||||
const mismatchedRows = anchorKey
|
||||
? selectedRows.filter(
|
||||
row => this.waybillProjectKey(row) && this.waybillProjectKey(row) !== anchorKey
|
||||
)
|
||||
: [];
|
||||
if (mismatchedRows.length) {
|
||||
const anchorName = anchorRow.projectName || anchorRow.projectId;
|
||||
const mismatchedText = mismatchedRows
|
||||
.map(
|
||||
row =>
|
||||
`${row.waybillNo || row.id}(项目:${row.projectName || row.projectId || '未知'})`
|
||||
)
|
||||
.join('、');
|
||||
this.$message.warning(
|
||||
`运单 ${mismatchedText} 与已选运单项目「${anchorName}」不一致,不能加入同一配载单`
|
||||
);
|
||||
}
|
||||
const nextRows = [...this.selectedWaybillRows];
|
||||
selectedRows.forEach(row => {
|
||||
if (mismatchedRows.includes(row)) return;
|
||||
const key = String(row.id || row.waybillNo);
|
||||
if (!exists.has(key)) {
|
||||
nextRows.push(row);
|
||||
exists.add(key);
|
||||
}
|
||||
});
|
||||
// 承运商类型:新增后须仍存在共同的承运商合同乙方(按客户合同乙方=承运商合同甲方匹配后取交集)
|
||||
if (
|
||||
String(this.dialogForm.carrierType || '').trim() === '承运商' &&
|
||||
nextRows.length > this.selectedWaybillRows.length
|
||||
) {
|
||||
const compatible = await this.hasCompatibleCarrierContracts(nextRows);
|
||||
if (!compatible) {
|
||||
this.$message.warning(
|
||||
'所选运单在同一项目下没有共同的承运商合同乙方,不能加入同一配载单'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const prevRouteNodes = [...this.routeNodes];
|
||||
this.selectedWaybillRows = nextRows;
|
||||
this.rebuildSummaryFromWaybills();
|
||||
if (this.isRunningEdit) {
|
||||
await this.refreshWaybillPunchMap();
|
||||
}
|
||||
this.rebuildSummaryFromWaybills(true, prevRouteNodes);
|
||||
await this.loadTaskCarrierOptions();
|
||||
this.$refs.candidateTableRef?.clearSelection?.();
|
||||
this.candidateSelection = [];
|
||||
@@ -3786,15 +4049,42 @@ export default {
|
||||
this.candidateAdding = false;
|
||||
}
|
||||
},
|
||||
async hasCompatibleCarrierContracts(rows = []) {
|
||||
const projectId = (rows || []).map(row => row.projectId).find(Boolean);
|
||||
const waybillIds = (rows || []).map(row => row.id).filter(Boolean);
|
||||
if (!projectId || waybillIds.length < 2) return true;
|
||||
try {
|
||||
const response = await loadingApi.getCarrierContracts([projectId], waybillIds);
|
||||
const contracts = response?.data?.data || response?.data || [];
|
||||
return (Array.isArray(contracts) ? contracts : []).length > 0;
|
||||
} catch (error) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
async addSelectedWaybills() {
|
||||
await this.addWaybillRows(this.candidateSelection);
|
||||
},
|
||||
waybillProjectKey(row = {}) {
|
||||
return String(row.projectId || row.projectName || '').trim();
|
||||
},
|
||||
removeSelectedWaybill(index) {
|
||||
const row = this.selectedWaybillRows[index];
|
||||
if (this.isSelectedWaybillRemoveLocked(row)) {
|
||||
ElMessage.warning('已打卡的子运单不可移除');
|
||||
return;
|
||||
}
|
||||
this.selectedWaybillRows.splice(index, 1);
|
||||
if (this.isRunningEdit) {
|
||||
this.refreshWaybillPunchMap().then(() => {
|
||||
this.rebuildSummaryFromWaybills();
|
||||
this.loadTaskCarrierOptions();
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.rebuildSummaryFromWaybills();
|
||||
this.loadTaskCarrierOptions();
|
||||
},
|
||||
rebuildSummaryFromWaybills(rebuildRoute = true) {
|
||||
rebuildSummaryFromWaybills(rebuildRoute = true, prevRouteNodes = null) {
|
||||
const rows = this.selectedWaybillRows;
|
||||
this.dialogForm.loadingSubNos = rows
|
||||
.map(row => row.waybillNo)
|
||||
@@ -3819,7 +4109,13 @@ export default {
|
||||
this.dialogForm.transportType = rows.find(row => row.transportType)?.transportType || '';
|
||||
}
|
||||
if (rebuildRoute) {
|
||||
this.routeNodes = this.buildRouteNodes(rows);
|
||||
const builtNodes = this.buildRouteNodes(rows);
|
||||
this.routeNodes = this.isRunningEdit
|
||||
? this.mergeRouteNodesKeepingPunched(
|
||||
prevRouteNodes || this.routeNodes,
|
||||
builtNodes
|
||||
)
|
||||
: builtNodes;
|
||||
}
|
||||
this.cargoRows = this.buildCargoRows(rows);
|
||||
// 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地
|
||||
@@ -3839,7 +4135,9 @@ export default {
|
||||
if (locked) node.locked = true;
|
||||
};
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const locked = this.isWaybillCompleted(row);
|
||||
const locked = this.isRunningEdit
|
||||
? this.isWaybillLoadPunched(row)
|
||||
: this.isWaybillCompleted(row);
|
||||
const departure = row.departureAddress || row.departureName;
|
||||
appendNode(
|
||||
departure,
|
||||
@@ -3849,7 +4147,9 @@ export default {
|
||||
);
|
||||
});
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const locked = this.isWaybillCompleted(row);
|
||||
const locked = this.isRunningEdit
|
||||
? this.isWaybillUnloadPunched(row)
|
||||
: this.isWaybillCompleted(row);
|
||||
const arrival = row.arrivalAddress || row.arrivalName;
|
||||
appendNode(
|
||||
arrival,
|
||||
@@ -3901,7 +4201,9 @@ export default {
|
||||
if (
|
||||
originalLockedKeys.some((key, lockedIndex) => key && key !== nextLockedKeys[lockedIndex])
|
||||
) {
|
||||
ElMessage.warning('已完成运单对应的路线不可调整');
|
||||
ElMessage.warning(
|
||||
this.isRunningEdit ? '已打卡地点不可调整,新地址只能在未打卡节点间拖拽' : '已完成运单对应的路线不可调整'
|
||||
);
|
||||
this.routeDragIndex = -1;
|
||||
return;
|
||||
}
|
||||
@@ -3941,14 +4243,28 @@ export default {
|
||||
return [this.normalizeCargoRow(row, row)];
|
||||
});
|
||||
},
|
||||
formatCargoQuantityDisplay(row = {}) {
|
||||
const quantity = blankCargoSentinel(row.quantity ?? row.cargoQuantity ?? '');
|
||||
const unit = String(
|
||||
blankCargoSentinel(
|
||||
row.quantityUnit ||
|
||||
row.goodsQuantityUnit ||
|
||||
row.cargoUnit ||
|
||||
row.unit ||
|
||||
''
|
||||
) || ''
|
||||
).trim();
|
||||
if (quantity === '' || quantity === undefined || quantity === null) {
|
||||
return unit || '';
|
||||
}
|
||||
return unit ? `${quantity} ${unit}` : String(quantity);
|
||||
},
|
||||
normalizeCargoRow(waybill, item = {}) {
|
||||
return sanitizeCargoRow({
|
||||
projectName: item.projectName || waybill.projectName || '',
|
||||
cargoName: item.cargoName || item.goodsName || waybill.cargoName || '',
|
||||
cargoType: item.cargoType || item.goodsType || waybill.cargoType || '',
|
||||
packageType: item.packageType || item.package || '',
|
||||
weight: item.weight || item.cargoWeight || '',
|
||||
volume: item.volume || item.cargoVolume || '',
|
||||
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
|
||||
quantityUnit:
|
||||
item.quantityUnit ||
|
||||
@@ -4145,15 +4461,15 @@ export default {
|
||||
try {
|
||||
const payload = {
|
||||
...this.normalizePayload(),
|
||||
// 确认生成正式配载单(待执行),覆盖草稿状态
|
||||
businessStatus: 'pending',
|
||||
// 进行中编辑保持 running;新建/草稿确认由后端按项目是否需接单写入 pending/running
|
||||
...(this.isRunningEdit ? { businessStatus: 'running' } : {}),
|
||||
};
|
||||
if (this.dialogMode === 'reassign') {
|
||||
await loadingApi.reassign(payload);
|
||||
ElMessage.success('派单成功');
|
||||
} else {
|
||||
await loadingApi.submit(payload);
|
||||
ElMessage.success('确认成功');
|
||||
ElMessage.success(this.isRunningEdit ? '保存成功' : '确认成功');
|
||||
}
|
||||
sessionStorage.setItem(loadingManageListRefreshKey, '1');
|
||||
this.closeLoadingDialog();
|
||||
@@ -4273,12 +4589,6 @@ export default {
|
||||
this.dialogForm.escortPhone = '';
|
||||
this.driverOptions = [];
|
||||
},
|
||||
getDialogCarrierFilter() {
|
||||
return {
|
||||
carrierId: this.dialogForm.carrierId || '',
|
||||
carrierName: this.dialogForm.carrierName || '',
|
||||
};
|
||||
},
|
||||
handleDriverChange(value) {
|
||||
const driver = this.driverOptions.find(item => (item.driverName || item.name) === value);
|
||||
if (driver && !this.dialogForm.driverPhone) {
|
||||
@@ -4312,16 +4622,12 @@ export default {
|
||||
},
|
||||
fetchEscortSuggestions(queryString, callback) {
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getDialogCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
this.escortLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
|
||||
carrier
|
||||
)
|
||||
fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '押运员',
|
||||
})
|
||||
.then(records => {
|
||||
callback(
|
||||
records.map(item => ({
|
||||
@@ -4401,17 +4707,13 @@ export default {
|
||||
}
|
||||
},
|
||||
async loadDialogDriverOptions(keyword = '') {
|
||||
const carrier = this.getDialogCarrierFilter();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
this.driverOptions = [];
|
||||
return [];
|
||||
}
|
||||
this.driverLoading = true;
|
||||
try {
|
||||
this.driverOptions = await fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
);
|
||||
this.driverOptions = await fetchDriverCandidates({
|
||||
size: 20,
|
||||
...(keyword ? { driverName: keyword } : {}),
|
||||
posts: '司机',
|
||||
});
|
||||
return this.driverOptions;
|
||||
} finally {
|
||||
this.driverLoading = false;
|
||||
@@ -4442,10 +4744,13 @@ export default {
|
||||
}
|
||||
this.taskCarrierLoading = true;
|
||||
try {
|
||||
const projectIds = [
|
||||
...new Set(this.selectedWaybillRows.map(row => row.projectId).filter(Boolean)),
|
||||
];
|
||||
const response = await loadingApi.getCarrierContracts(projectIds);
|
||||
// 配载单只有一个项目;传入子运单后按客户合同乙方=承运商合同甲方匹配并取承运商合同乙方交集
|
||||
const projectId = this.selectedWaybillRows.map(row => row.projectId).find(Boolean);
|
||||
const waybillIds = this.selectedWaybillRows.map(row => row.id).filter(Boolean);
|
||||
const response = await loadingApi.getCarrierContracts(
|
||||
projectId ? [projectId] : [],
|
||||
waybillIds
|
||||
);
|
||||
if (requestId !== this.taskCarrierRequestId) return [];
|
||||
const contracts = response.data?.data || response.data || [];
|
||||
if (this.dialogForm.carrierType !== '自运') {
|
||||
@@ -4787,6 +5092,16 @@ export default {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__route-hint {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__locked-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loading-manage-dialog__route-type {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
<el-radio-group
|
||||
v-model="row.confirmMode"
|
||||
:disabled="dialogReadonly || !row.enabled"
|
||||
@change="handleReturnConfirmChange(row)"
|
||||
@change="syncNodeFields"
|
||||
>
|
||||
<el-radio label="yes">是</el-radio>
|
||||
<el-radio label="no_confirm_complete">无需确认完成</el-radio>
|
||||
@@ -253,10 +253,7 @@
|
||||
</div>
|
||||
<div class="process-config-form__require-line">
|
||||
<span class="process-config-form__require-label">确认完成</span>
|
||||
<el-checkbox
|
||||
v-model="row.confirmInternal"
|
||||
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
|
||||
@change="syncNodeFields"
|
||||
<el-checkbox v-model="row.confirmInternal" :disabled="true"
|
||||
>内部人员</el-checkbox
|
||||
>
|
||||
</div>
|
||||
@@ -549,15 +546,18 @@ export default {
|
||||
row.confirmDriver = row.confirmMode === 'yes';
|
||||
this.syncNodeFields();
|
||||
},
|
||||
handleReturnConfirmChange(row) {
|
||||
row.confirmInternal = row.confirmMode === 'yes';
|
||||
this.syncNodeFields();
|
||||
},
|
||||
enforceAcceptDriver(node) {
|
||||
if (!node || (node.key !== 'accept' && node.name !== '接单')) return node;
|
||||
node.confirmDriver = node.confirmMode === 'yes';
|
||||
return node;
|
||||
},
|
||||
// 回单「确认完成」只有内部人员一种角色,是否确认为「是」时即由内部人员确认,
|
||||
// 界面上只读展示,值始终由 confirmMode 推导
|
||||
enforceReturnInternal(node) {
|
||||
if (!node || (node.key !== 'return' && node.name !== '回单')) return node;
|
||||
node.confirmInternal = node.confirmMode === 'yes';
|
||||
return node;
|
||||
},
|
||||
initDeptOptions() {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
@@ -724,7 +724,7 @@ export default {
|
||||
this.projectLoading = true;
|
||||
return getProjectList(1, 50, {
|
||||
projectName: keyword,
|
||||
approvalStatus: 'approved',
|
||||
approvalStatuses: 'approved,change_approved',
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.then(res => {
|
||||
@@ -808,7 +808,10 @@ export default {
|
||||
this.syncNodeFields();
|
||||
},
|
||||
syncNodeFields() {
|
||||
this.nodeRows.forEach(item => this.enforceAcceptDriver(item));
|
||||
this.nodeRows.forEach(item => {
|
||||
this.enforceAcceptDriver(item);
|
||||
this.enforceReturnInternal(item);
|
||||
});
|
||||
const enabledNodes = this.nodeRows.filter(item => item.enabled);
|
||||
this.form.includedNodes = enabledNodes.map(item => item.name).join(',');
|
||||
this.form.nodeConfigJson = JSON.stringify(
|
||||
|
||||
@@ -84,7 +84,8 @@ export default {
|
||||
this.postFormData('save', lbpmFormValues, this.buildFormData());
|
||||
},
|
||||
postFormData(status, formValues, formData) {
|
||||
postMkPublicProcessMessage('project-apply', {
|
||||
const bizType = this.$route.meta?.bizType || 'project-apply';
|
||||
postMkPublicProcessMessage(bizType, {
|
||||
status,
|
||||
formValues: formValues || {},
|
||||
formData,
|
||||
|
||||
@@ -52,12 +52,37 @@
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
v-if="!readonlyScope && hcsImmediateEnabled"
|
||||
:loading="hcsRepushingId === 'batch'"
|
||||
@click="handleHcsBatchRepush"
|
||||
>
|
||||
<el-icon class="el-icon--left"><Position /></el-icon>批量推送合规
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #approvalStatus="{ row }">
|
||||
{{ displayStatus(row, 'approvalStatus') }}
|
||||
</template>
|
||||
|
||||
<template #hcsSyncStatus="{ row }">
|
||||
<el-tooltip v-if="row.hcsSyncMsg" placement="top" :content="row.hcsSyncMsg">
|
||||
<el-tag :type="hcsSyncTagType(row.hcsSyncStatus)" class="status-text">
|
||||
{{ hcsSyncLabel(row.hcsSyncStatus) }}
|
||||
</el-tag>
|
||||
</el-tooltip>
|
||||
<el-tag
|
||||
v-else-if="row.hcsSyncStatus"
|
||||
:type="hcsSyncTagType(row.hcsSyncStatus)"
|
||||
class="status-text"
|
||||
>
|
||||
{{ hcsSyncLabel(row.hcsSyncStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #header>
|
||||
<div class="project-apply-page__risk-search">
|
||||
<span class="project-apply-page__risk-label">
|
||||
@@ -128,6 +153,13 @@
|
||||
>
|
||||
{{ operation.label }}
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hcsImmediateEnabled && canHcsRepush(row)"
|
||||
type="warning"
|
||||
@click="handleHcsRepush(row)"
|
||||
>
|
||||
{{ hcsRepushingId === row.id ? '推送中…' : '推送合规' }}
|
||||
</el-link>
|
||||
<el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
|
||||
</template>
|
||||
</avue-crud>
|
||||
@@ -217,35 +249,57 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务部门" prop="businessDeptId">
|
||||
<el-cascader
|
||||
v-model="businessDeptCascaderValue"
|
||||
:options="businessDeptTreeOptions"
|
||||
:props="deptCascaderProps"
|
||||
placeholder="请选择"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:disabled="!shouldShowBusinessDeptTooltip"
|
||||
:show-after="200"
|
||||
>
|
||||
<template #content>
|
||||
<div class="project-apply-form__field-tooltip">{{ businessDeptTooltipText }}</div>
|
||||
</template>
|
||||
<span class="project-apply-form__field-tooltip-trigger">
|
||||
<el-cascader
|
||||
v-model="businessDeptCascaderValue"
|
||||
:options="businessDeptTreeOptions"
|
||||
:props="deptCascaderProps"
|
||||
placeholder="请选择"
|
||||
filterable
|
||||
clearable
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台公司" prop="undertakeDeptId">
|
||||
<el-select
|
||||
v-model="form.undertakeDeptId"
|
||||
placeholder="请选择"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
clearable
|
||||
filterable
|
||||
:disabled="dialogReadonly"
|
||||
@change="handleUndertakeDeptChange"
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:disabled="!shouldShowUndertakeDeptTooltip"
|
||||
:show-after="200"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in platformCompanyOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<template #content>
|
||||
<div class="project-apply-form__field-tooltip">{{ undertakeDeptTooltipText }}</div>
|
||||
</template>
|
||||
<span class="project-apply-form__field-tooltip-trigger">
|
||||
<el-select
|
||||
v-model="form.undertakeDeptId"
|
||||
placeholder="请选择"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
clearable
|
||||
filterable
|
||||
:disabled="dialogReadonly"
|
||||
@change="handleUndertakeDeptChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in platformCompanyOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目由来" prop="projectSource">
|
||||
<el-select
|
||||
@@ -778,7 +832,21 @@
|
||||
<template v-if="isChangeDialog">
|
||||
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
|
||||
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
|
||||
<el-button
|
||||
v-if="isProjectChangeModule && !dialogReadonly"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="submitLoading"
|
||||
@click="saveProjectChangeDraft"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!dialogReadonly"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="submitChangeProject"
|
||||
>
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -916,12 +984,18 @@
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import * as api from '@/api/business/project-apply';
|
||||
import {
|
||||
loadHcsImmediateEnabled,
|
||||
repush as hcsRepush,
|
||||
repushBatch as hcsRepushBatch,
|
||||
} from '@/api/business/hcs';
|
||||
import { Position } from '@element-plus/icons-vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import {
|
||||
getList as getCustomerArchiveList,
|
||||
getDetail as getCustomerArchiveDetail,
|
||||
} from '@/api/vehicle/customer-archive';
|
||||
import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
|
||||
import { getBusinessDeptSelect, getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { getList as getUserList } from '@/api/system/user';
|
||||
@@ -951,6 +1025,7 @@ import {
|
||||
projectTypeOptions,
|
||||
} from '@/option/business/common';
|
||||
import { config, option } from '@/option/business/project-apply';
|
||||
import * as projectChangeApi from '@/api/business/project-change';
|
||||
|
||||
const emptyForm = () => ({
|
||||
projectType: '',
|
||||
@@ -1026,6 +1101,9 @@ const attachmentViewerPlugins = [
|
||||
const projectApplyListRoute = '/business/project-apply';
|
||||
const projectApplyFormRoute = '/business/project-apply/form';
|
||||
const projectApplyListRefreshKey = 'project-apply-list-refresh';
|
||||
const projectChangeListRoute = '/business/project-change';
|
||||
const projectChangeFormRoute = '/business/project-change/form';
|
||||
const projectChangeListRefreshKey = 'project-change-list-refresh';
|
||||
|
||||
const otherAttachmentType = { label: '其他附件', value: '其他附件' };
|
||||
|
||||
@@ -1060,6 +1138,7 @@ export default {
|
||||
ElImageViewer,
|
||||
OpenFileViewer,
|
||||
PdfPreview,
|
||||
Position,
|
||||
},
|
||||
data() {
|
||||
const validateAmount = (rule, value, callback) => {
|
||||
@@ -1121,6 +1200,9 @@ export default {
|
||||
{ label: '高风险', value: 'high', statKey: 'high' },
|
||||
{ label: '中风险', value: 'medium', statKey: 'medium' },
|
||||
],
|
||||
// 合规(HCS)推送状态;受 thirdParty.hcs.push.immediate-enabled 控制显隐
|
||||
hcsRepushingId: null,
|
||||
hcsImmediateEnabled: false,
|
||||
page: {
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
@@ -1208,7 +1290,40 @@ export default {
|
||||
},
|
||||
],
|
||||
changeReason: [
|
||||
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!this.isChangeDialog) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请输入变更原因'));
|
||||
return;
|
||||
}
|
||||
if (String(value).length > 2000) {
|
||||
callback(new Error('变更原因不能超过2000个字符'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
changeType: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (!this.isChangeDialog) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择变更类型'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
projectIntro: [
|
||||
{ max: 1000, message: '项目简介不能超过1000个字符', trigger: 'blur' },
|
||||
@@ -1314,6 +1429,33 @@ export default {
|
||||
this.handleBusinessDeptChange(this.form.businessDeptId);
|
||||
},
|
||||
},
|
||||
// 查看态禁用控件无 hover,用完整路径/名称做 tooltip
|
||||
businessDeptTooltipText() {
|
||||
const pathIds = this.businessDeptCascaderValue || [];
|
||||
if (pathIds.length && this.businessDeptTreeOptions?.length) {
|
||||
const labels = [];
|
||||
let nodes = this.businessDeptTreeOptions;
|
||||
for (const id of pathIds) {
|
||||
const node = (nodes || []).find(item => String(item.value) === String(id));
|
||||
if (!node) break;
|
||||
labels.push(node.label);
|
||||
nodes = node.children;
|
||||
}
|
||||
if (labels.length) return labels.join(' / ');
|
||||
}
|
||||
return String(this.form.businessDeptName || '').trim();
|
||||
},
|
||||
undertakeDeptTooltipText() {
|
||||
const name = String(this.form.undertakeDeptName || '').trim();
|
||||
if (name) return name;
|
||||
return this.resolveUndertakeDeptNames(this.parseUndertakeDeptIds(this.form.undertakeDeptId));
|
||||
},
|
||||
shouldShowBusinessDeptTooltip() {
|
||||
return this.dialogReadonly && !!this.businessDeptTooltipText;
|
||||
},
|
||||
shouldShowUndertakeDeptTooltip() {
|
||||
return this.dialogReadonly && !!this.undertakeDeptTooltipText;
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.canCreate,
|
||||
@@ -1337,8 +1479,11 @@ export default {
|
||||
isMajorProjectSupplementDialog() {
|
||||
return this.dialogType === 'majorSupplement';
|
||||
},
|
||||
isProjectChangeModule() {
|
||||
return this.$route.path === projectChangeFormRoute;
|
||||
},
|
||||
isChangeDialog() {
|
||||
return this.dialogType === 'change';
|
||||
return this.dialogType === 'change' || this.isProjectChangeModule;
|
||||
},
|
||||
isNewProjectDialog() {
|
||||
return ['add', 'majorSupplement'].includes(this.dialogType);
|
||||
@@ -1347,7 +1492,13 @@ export default {
|
||||
return this.isFormPageInstance;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/business/project-apply/public-view';
|
||||
return (
|
||||
this.$route.path === '/business/project-apply/public-view' ||
|
||||
this.$route.path === '/business/project-change/public-view'
|
||||
);
|
||||
},
|
||||
publicBizType() {
|
||||
return this.$route.meta?.bizType || 'project-apply';
|
||||
},
|
||||
projectFormContainer() {
|
||||
// 表单页实例始终用页面容器,避免 keep-alive 失活时误切弹窗
|
||||
@@ -1410,11 +1561,18 @@ export default {
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
loadHcsImmediateEnabled()
|
||||
.then(enabled => {
|
||||
this.hcsImmediateEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.hcsImmediateEnabled = false;
|
||||
});
|
||||
this.loadDeptOptions();
|
||||
this.loadCargoTypeOptions();
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadSettlementModeOptions();
|
||||
if (this.$route.path === projectApplyFormRoute) {
|
||||
if (this.$route.path === projectApplyFormRoute || this.$route.path === projectChangeFormRoute) {
|
||||
this.isFormPageInstance = true;
|
||||
this.openProjectFormPage();
|
||||
}
|
||||
@@ -1480,14 +1638,14 @@ export default {
|
||||
},
|
||||
openPublicProjectForm() {
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = 'view';
|
||||
this.dialogType = this.publicBizType === 'project-change' ? 'change' : 'view';
|
||||
this.dialogReadonly = true;
|
||||
this.projectBox = true;
|
||||
if (!id) {
|
||||
this.$message.error('缺少项目ID');
|
||||
return;
|
||||
}
|
||||
getMkPublicDetail('project-apply', id)
|
||||
getMkPublicDetail(this.publicBizType, id)
|
||||
.then(res => {
|
||||
const detail = res.data?.data || {};
|
||||
this.applyPublicDictOptions(detail);
|
||||
@@ -1550,15 +1708,32 @@ export default {
|
||||
openProjectFormPage() {
|
||||
const type = this.$route.query.mode || 'add';
|
||||
const id = this.$route.query.id;
|
||||
this.dialogType = type;
|
||||
this.dialogType = this.isProjectChangeModule ? (type === 'view' ? 'view' : 'change') : type;
|
||||
this.dialogReadonly = type === 'view';
|
||||
this.projectBox = true;
|
||||
if (this.isProjectChangeModule) {
|
||||
if (!id) {
|
||||
this.$message.error('缺少项目变更ID');
|
||||
return;
|
||||
}
|
||||
projectChangeApi.getDetail(id).then(res => {
|
||||
const detail = res.data.data || {};
|
||||
this.applyProjectDetail({
|
||||
...detail,
|
||||
changeType: detail.changeType || '项目变更',
|
||||
changeNo: detail.changeNo || '',
|
||||
changeDate: detail.changeDate || this.$dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (['add', 'majorSupplement'].includes(type)) {
|
||||
this.applyProjectDetail({
|
||||
...emptyForm(),
|
||||
projectType: type === 'majorSupplement' ? '重大项目' : '普通项目',
|
||||
});
|
||||
this.fillDefaultUsers();
|
||||
this.fillDefaultBusinessDept();
|
||||
return;
|
||||
}
|
||||
if (!id) return;
|
||||
@@ -1575,6 +1750,12 @@ export default {
|
||||
},
|
||||
closeProjectForm() {
|
||||
if (this.isProjectFormPage) {
|
||||
if (this.isProjectChangeModule) {
|
||||
sessionStorage.setItem(projectChangeListRefreshKey, '1');
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push(projectChangeListRoute);
|
||||
return;
|
||||
}
|
||||
this.markProjectApplyListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push(projectApplyListRoute);
|
||||
@@ -1612,6 +1793,94 @@ export default {
|
||||
return this.canOperation(operation, row);
|
||||
});
|
||||
},
|
||||
// ==================== 合规(HCS)推送/对账(照“同步金蝶”按钮模式) ====================
|
||||
hcsSyncLabel(value) {
|
||||
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
|
||||
},
|
||||
hcsSyncTagType(value) {
|
||||
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
|
||||
},
|
||||
canHcsRepush(row) {
|
||||
return (
|
||||
this.hcsImmediateEnabled &&
|
||||
!this.readonlyScope &&
|
||||
['approved', 'change_approved'].includes(row.approvalStatus) &&
|
||||
row.hcsSyncStatus !== 'synced'
|
||||
);
|
||||
},
|
||||
handleHcsRepush(row) {
|
||||
if (this.hcsRepushingId) return;
|
||||
this.$confirm('确定推送该数据到合同合规系统?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = row.id;
|
||||
hcsRepush('project', row.id)
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '推送合规失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(res.data?.data || '推送合规成功');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
handleHcsBatchRepush() {
|
||||
if (this.hcsRepushingId) return;
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
const invalidCount = this.selectionList.filter(item => !this.canHcsRepush(item)).length;
|
||||
if (invalidCount) {
|
||||
this.$message.warning(`选中数据中有${invalidCount}条不符合推送条件(仅审批通过且未同步的可推送),请重新选择`);
|
||||
return;
|
||||
}
|
||||
this.$confirm(`确定将选中的${this.selectionList.length}条数据推送到合同合规系统?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
// 同步推送合规耗时较长,加遮罩防重复操作(照审批通过遮罩)
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在批量推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.hcsRepushingId = 'batch';
|
||||
hcsRepushBatch('project', this.selectionList.map(item => item.id))
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '批量推送合规失败');
|
||||
return;
|
||||
}
|
||||
this.$message.success(res.data?.data || '批量推送合规完成');
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.catch(() => {
|
||||
// 失败原因由 axios 拦截器统一弹出后端 msg,此处不再重复提示
|
||||
})
|
||||
.finally(() => {
|
||||
this.hcsRepushingId = null;
|
||||
loading.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
canOperation(operation, row) {
|
||||
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
|
||||
if (!this.hasPermission(permission) || this.readonlyScope || row.readonly) return false;
|
||||
@@ -1733,7 +2002,7 @@ export default {
|
||||
},
|
||||
handleCustomOperation(operation, row) {
|
||||
if (operation.action === 'startChange') {
|
||||
this.openProjectDialog('change', row);
|
||||
this.startIndependentProjectChange(row);
|
||||
return;
|
||||
}
|
||||
if (operation.action === 'mkAudit') {
|
||||
@@ -1753,14 +2022,31 @@ export default {
|
||||
mkEnabled: this.mkEnabled,
|
||||
}).then(() => request())
|
||||
: request();
|
||||
afterMk.then(res => {
|
||||
if (res.data?.success === false || res.data?.data === false) {
|
||||
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
|
||||
return;
|
||||
}
|
||||
this.$message.success(`${operation.label}成功`);
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
// 项目通过会同步等待合规推送,加遮罩防重复点击
|
||||
const loading =
|
||||
operation.action === 'approve'
|
||||
? this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
})
|
||||
: null;
|
||||
afterMk
|
||||
.then(res => {
|
||||
// data=false 表示审批已通过但合规推送失败,msg 带具体原因(照正式结算单审批)
|
||||
if (operation.action === 'approve' && res.data?.data === false) {
|
||||
this.$message.warning(res.data?.msg);
|
||||
this.onLoad(this.page, this.query);
|
||||
return;
|
||||
}
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
|
||||
return;
|
||||
}
|
||||
this.$message.success(`${operation.label}成功`);
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => loading && loading.close());
|
||||
};
|
||||
if (operation.prompt) {
|
||||
this.$prompt(operation.prompt, operation.label, {
|
||||
@@ -1891,6 +2177,7 @@ export default {
|
||||
.filter(Boolean);
|
||||
this.syncSelectedCustomerOptions();
|
||||
this.ensureCurrentPlatformOption();
|
||||
this.ensureCurrentBusinessDeptOption();
|
||||
},
|
||||
fillDefaultUsers() {
|
||||
const userId = this.userInfo?.userId || this.userInfo?.id || '';
|
||||
@@ -1909,6 +2196,25 @@ export default {
|
||||
this.form.principalUserName = userName;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 新增/重大补录:业务部门默认取当前用户所属部门中第一个「是否业务部门=是」的组织。
|
||||
*/
|
||||
fillDefaultBusinessDept() {
|
||||
if (!['add', 'majorSupplement'].includes(this.dialogType)) return;
|
||||
if (this.form.businessDeptId) return;
|
||||
const raw = this.userInfo?.deptId || this.userInfo?.dept_id || '';
|
||||
const userDeptIds = String(raw)
|
||||
.split(/[,,]/)
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(item => item && item !== '-1');
|
||||
if (!userDeptIds.length || !this.deptOptions.length) return;
|
||||
const matched = userDeptIds.find(id =>
|
||||
this.deptOptions.some(item => String(item.value) === String(id))
|
||||
);
|
||||
if (!matched) return;
|
||||
this.form.businessDeptId = matched;
|
||||
this.handleBusinessDeptChange(matched);
|
||||
},
|
||||
resolveUserRealName(row = {}, role) {
|
||||
return (
|
||||
row[`${role}RealName`] ||
|
||||
@@ -2007,6 +2313,52 @@ export default {
|
||||
this.closeProjectForm();
|
||||
});
|
||||
},
|
||||
startIndependentProjectChange(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('项目ID为空,无法发起变更');
|
||||
return;
|
||||
}
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '正在创建项目变更单…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
projectChangeApi
|
||||
.createFromProject(row.id)
|
||||
.then(res => {
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || '创建项目变更失败');
|
||||
return;
|
||||
}
|
||||
const detail = res.data?.data || {};
|
||||
if (!detail.id) {
|
||||
this.$message.error('创建项目变更失败:未返回变更单ID');
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
path: projectChangeFormRoute,
|
||||
query: { mode: 'edit', id: detail.id, name: '项目变更' },
|
||||
});
|
||||
})
|
||||
.finally(() => loading.close());
|
||||
},
|
||||
saveProjectChangeDraft() {
|
||||
if (!this.isProjectChangeModule) return;
|
||||
this.submitLoading = true;
|
||||
projectChangeApi
|
||||
.saveDraft(this.normalizeSubmitForm({ includeChangeType: true }))
|
||||
.then(res => {
|
||||
if (res.data?.success === false || res.data?.data === false) {
|
||||
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
|
||||
return;
|
||||
}
|
||||
this.$message.success('保存成功!');
|
||||
this.closeProjectForm();
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitLoading = false;
|
||||
});
|
||||
},
|
||||
submitChangeProject() {
|
||||
this.$refs.projectForm.validate(valid => {
|
||||
if (!valid) {
|
||||
@@ -2015,7 +2367,34 @@ export default {
|
||||
}
|
||||
if (!this.validateAttachmentFileTypes()) return;
|
||||
this.submitLoading = true;
|
||||
// 变更记录仅在提交时生成,保存草稿不落记录
|
||||
if (this.isProjectChangeModule) {
|
||||
const payload = this.normalizeSubmitForm({ includeChangeType: true });
|
||||
const approvalStatus = this.form.approvalStatus || '';
|
||||
projectChangeApi
|
||||
.submit(payload)
|
||||
.then(res => {
|
||||
if (res.data?.success === false || res.data?.data === false) {
|
||||
this.$message.error(res.data?.msg || '提交失败,请联系管理员');
|
||||
return;
|
||||
}
|
||||
const id = this.form.id;
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'project-change',
|
||||
bizId: id,
|
||||
subjectName: this.form.projectName || '',
|
||||
approvalStatus,
|
||||
mkEnabled: this.mkEnabled,
|
||||
}).then(() => {
|
||||
this.$message.success('提交成功!');
|
||||
this.closeProjectForm();
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitLoading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 兼容旧入口:变更记录仅在提交时生成,保存草稿不落记录
|
||||
this.api
|
||||
.submitChange(this.normalizeSubmitForm({ includeChangeType: true }))
|
||||
.then(res => {
|
||||
@@ -2491,17 +2870,61 @@ export default {
|
||||
this.$refs.projectForm?.validateField('projectType');
|
||||
},
|
||||
loadDeptOptions() {
|
||||
getDeptTree().then(res => {
|
||||
const deptTree = res.data.data || [];
|
||||
// 新增/编辑/重大补录/项目变更均可选择业务部门,排除外部组织
|
||||
const businessDeptTree = ['add', 'edit', 'majorSupplement', 'change'].includes(this.dialogType)
|
||||
? this.excludeExternalOrganization(deptTree)
|
||||
: deptTree;
|
||||
this.deptOptions = this.flattenDept(businessDeptTree);
|
||||
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree);
|
||||
});
|
||||
Promise.all([getDeptTree(), getBusinessDeptSelect()])
|
||||
.then(([treeRes, bizRes]) => {
|
||||
const deptTree = this.excludeExternalOrganization(treeRes.data.data || []);
|
||||
const bizList = bizRes.data.data || [];
|
||||
const bizIds = new Set(
|
||||
bizList
|
||||
.map(item => (item.id == null || item.id === '' ? '' : String(item.id)))
|
||||
.filter(Boolean)
|
||||
);
|
||||
// 仅保留「是否业务部门=是」的节点及其祖先,祖先不可选
|
||||
const businessDeptTree = this.filterBusinessDeptTree(deptTree, bizIds);
|
||||
this.deptOptions = this.flattenDept(businessDeptTree).filter(item =>
|
||||
bizIds.has(String(item.value))
|
||||
);
|
||||
this.businessDeptTreeOptions = this.toDeptCascaderOptions(businessDeptTree, bizIds);
|
||||
this.fillDefaultBusinessDept();
|
||||
this.ensureCurrentBusinessDeptOption();
|
||||
})
|
||||
.catch(() => {
|
||||
this.deptOptions = [];
|
||||
this.businessDeptTreeOptions = [];
|
||||
});
|
||||
this.loadPlatformCompanyOptions();
|
||||
},
|
||||
/**
|
||||
* 剪枝组织树:只保留业务部门节点及其祖先路径。
|
||||
*/
|
||||
filterBusinessDeptTree(list = [], bizIds = new Set()) {
|
||||
return (list || []).reduce((result, item) => {
|
||||
const children = item.children?.length
|
||||
? this.filterBusinessDeptTree(item.children, bizIds)
|
||||
: [];
|
||||
const isBiz = bizIds.has(String(item.id));
|
||||
if (!isBiz && !children.length) {
|
||||
return result;
|
||||
}
|
||||
result.push({
|
||||
...item,
|
||||
children: children.length ? children : undefined,
|
||||
});
|
||||
return result;
|
||||
}, []);
|
||||
},
|
||||
ensureCurrentBusinessDeptOption() {
|
||||
const id = this.form.businessDeptId;
|
||||
if (!id) return;
|
||||
const exists = this.deptOptions.some(item => String(item.value) === String(id));
|
||||
if (exists) return;
|
||||
const label = this.form.businessDeptName || String(id);
|
||||
this.deptOptions.unshift({ label, rawLabel: label, value: id });
|
||||
const pathExists = this.findDeptPath(id, this.businessDeptTreeOptions);
|
||||
if (!pathExists) {
|
||||
this.businessDeptTreeOptions.unshift({ label, value: id });
|
||||
}
|
||||
},
|
||||
loadPlatformCompanyOptions() {
|
||||
getPlatformCompanySelect().then(res => {
|
||||
const list = res.data.data || [];
|
||||
@@ -2545,12 +2968,22 @@ export default {
|
||||
return result;
|
||||
}, []);
|
||||
},
|
||||
toDeptCascaderOptions(list = []) {
|
||||
return list.map(item => ({
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: item.id,
|
||||
children: item.children?.length ? this.toDeptCascaderOptions(item.children) : undefined,
|
||||
}));
|
||||
toDeptCascaderOptions(list = [], bizIds = null) {
|
||||
return list.map(item => {
|
||||
const value = item.id == null || item.id === '' ? item.value : item.id;
|
||||
const option = {
|
||||
label: item.title || item.deptName || item.name || item.label,
|
||||
value: value == null || value === '' ? '' : String(value),
|
||||
children: item.children?.length
|
||||
? this.toDeptCascaderOptions(item.children, bizIds)
|
||||
: undefined,
|
||||
};
|
||||
// 仅业务部门可选;祖先节点保留路径但禁用
|
||||
if (bizIds && !bizIds.has(String(option.value))) {
|
||||
option.disabled = true;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
},
|
||||
flattenDept(list = [], level = 0) {
|
||||
return list.reduce((result, item) => {
|
||||
@@ -2901,6 +3334,20 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__field-tooltip-trigger {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__field-tooltip {
|
||||
max-width: 480px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<basic-container class="project-change-page">
|
||||
<avue-crud
|
||||
:option="tableOption"
|
||||
:table-loading="loading"
|
||||
:data="data"
|
||||
v-model:page="page"
|
||||
v-model="tableForm"
|
||||
ref="crud"
|
||||
:permission="permissionList"
|
||||
@search-change="searchChange"
|
||||
@search-reset="searchReset"
|
||||
@selection-change="selectionChange"
|
||||
@current-change="currentChange"
|
||||
@size-change="sizeChange"
|
||||
@refresh-change="refreshChange"
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
v-if="hasPermission(`${config.permission}_delete`)"
|
||||
@click="handleDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<template #approvalStatus="{ row }">
|
||||
{{ row.approvalStatusName || statusLabel(row.approvalStatus) }}
|
||||
</template>
|
||||
|
||||
<template #syncStatus="{ row }">
|
||||
<el-tag v-if="row.syncStatus" :type="syncTagType(row.syncStatus)" class="status-text">
|
||||
{{ syncLabel(row.syncStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="canView(row)"
|
||||
@click="openForm('view', row)"
|
||||
>
|
||||
查看
|
||||
</el-link>
|
||||
<el-link type="primary" v-if="canEdit(row)" @click="openForm('edit', row)">
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
v-if="canDelete(row)"
|
||||
@click="rowDel(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
<el-link
|
||||
v-for="operation in customOperations(row)"
|
||||
:key="operation.action"
|
||||
:type="operation.type === 'warning' ? 'warning' : operation.type === 'success' ? 'success' : 'primary'"
|
||||
@click="handleCustomOperation(operation, row)"
|
||||
>
|
||||
{{ operation.label }}
|
||||
</el-link>
|
||||
</template>
|
||||
</avue-crud>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/project-change';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { config, option } from '@/option/business/project-change';
|
||||
import { approvalStatusOptions } from '@/option/business/common';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const projectChangeFormRoute = '/business/project-change/form';
|
||||
const projectChangeListRefreshKey = 'project-change-list-refresh';
|
||||
|
||||
export default {
|
||||
name: 'ProjectChange',
|
||||
data() {
|
||||
return {
|
||||
api,
|
||||
config,
|
||||
tableForm: {},
|
||||
tableOption: this.buildTableOption(),
|
||||
loading: true,
|
||||
data: [],
|
||||
page: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
query: {},
|
||||
selectionList: [],
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo', 'website']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
delBtn: false,
|
||||
editBtn: false,
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
activated() {
|
||||
if (sessionStorage.getItem(projectChangeListRefreshKey)) {
|
||||
sessionStorage.removeItem(projectChangeListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
buildTableOption() {
|
||||
return {
|
||||
...option,
|
||||
addBtn: false,
|
||||
viewBtn: false,
|
||||
editBtn: false,
|
||||
delBtn: false,
|
||||
menuWidth: 320,
|
||||
column: (option.column || []).map(column => ({
|
||||
...column,
|
||||
display: false,
|
||||
align: 'center',
|
||||
headerAlign: 'center',
|
||||
})),
|
||||
};
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
|
||||
},
|
||||
statusLabel(status) {
|
||||
return approvalStatusOptions.find(item => item.value === status)?.label || status || '-';
|
||||
},
|
||||
syncLabel(value) {
|
||||
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
|
||||
},
|
||||
syncTagType(value) {
|
||||
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
|
||||
},
|
||||
inStatus(row, statusList) {
|
||||
if (!statusList || !statusList.length) return true;
|
||||
return statusList.includes(row.approvalStatus);
|
||||
},
|
||||
canEdit(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_edit`) &&
|
||||
!row.readonly &&
|
||||
this.inStatus(row, this.config.editStatus)
|
||||
);
|
||||
},
|
||||
canDelete(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_delete`) &&
|
||||
!row.readonly &&
|
||||
this.inStatus(row, this.config.deleteStatus)
|
||||
);
|
||||
},
|
||||
canView(row) {
|
||||
return (
|
||||
this.hasPermission(`${this.config.permission}_view`) &&
|
||||
this.inStatus(row, this.config.viewStatus)
|
||||
);
|
||||
},
|
||||
canOperation(operation, row) {
|
||||
if (operation.permission && !this.hasPermission(operation.permission)) return false;
|
||||
if (row.readonly && ['submitApproval', 'withdraw', 'approve', 'reject'].includes(operation.action)) {
|
||||
return false;
|
||||
}
|
||||
return this.inStatus(row, operation.status);
|
||||
},
|
||||
customOperations(row) {
|
||||
return (this.config.operations || []).filter(operation => {
|
||||
if (operation.mkOnly && !this.mkEnabled) return false;
|
||||
if (operation.localOnly && this.mkEnabled) return false;
|
||||
return this.canOperation(operation, row);
|
||||
});
|
||||
},
|
||||
openForm(mode, row = {}) {
|
||||
const query = { mode, id: row.id };
|
||||
if (mode === 'edit') query.name = '编辑项目变更';
|
||||
else if (mode === 'view') query.name = '查看项目变更';
|
||||
this.$router.push({ path: projectChangeFormRoute, query });
|
||||
},
|
||||
handleCustomOperation(operation, row) {
|
||||
if (operation.action === 'mkAudit') {
|
||||
this.handleMkAudit(row);
|
||||
return;
|
||||
}
|
||||
const run = () => {
|
||||
const request = () => this.api[operation.action](row.id);
|
||||
const afterMk =
|
||||
operation.action === 'submitApproval'
|
||||
? submitMkApprovalFlow({
|
||||
bizType: 'project-change',
|
||||
bizId: row.id,
|
||||
subjectName: row.projectName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
mkEnabled: this.mkEnabled,
|
||||
}).then(() => request())
|
||||
: request();
|
||||
const loading =
|
||||
operation.action === 'approve'
|
||||
? this.$loading({
|
||||
lock: true,
|
||||
text: '正在推送合规,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
})
|
||||
: null;
|
||||
afterMk
|
||||
.then(res => {
|
||||
if (operation.action === 'approve' && res.data?.data === false) {
|
||||
this.$message.warning(res.data?.msg);
|
||||
this.onLoad(this.page, this.query);
|
||||
return;
|
||||
}
|
||||
if (res.data?.success === false) {
|
||||
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
|
||||
return;
|
||||
}
|
||||
this.$message.success(`${operation.label}成功`);
|
||||
this.onLoad(this.page, this.query);
|
||||
})
|
||||
.finally(() => loading && loading.close());
|
||||
};
|
||||
this.$confirm(operation.confirm || `确定${operation.label}该${this.config.title}?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => run());
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('变更单ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
this.selectionList = list;
|
||||
},
|
||||
selectionClear() {
|
||||
this.selectionList = [];
|
||||
this.$refs.crud?.toggleSelection?.();
|
||||
},
|
||||
currentChange(currentPage) {
|
||||
this.page.currentPage = currentPage;
|
||||
},
|
||||
sizeChange(pageSize) {
|
||||
this.page.pageSize = pageSize;
|
||||
},
|
||||
refreshChange() {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
rowDel(row) {
|
||||
this.$confirm('确定将选择数据删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.api.remove(row.id).then(() => {
|
||||
this.$message.success('删除成功!');
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
});
|
||||
},
|
||||
handleDelete() {
|
||||
if (!this.selectionList.length) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定将选择数据删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
this.api.remove(this.ids).then(() => {
|
||||
this.$message.success('删除成功!');
|
||||
this.selectionClear();
|
||||
this.onLoad(this.page, this.query);
|
||||
});
|
||||
});
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const values = {
|
||||
...this.query,
|
||||
...params,
|
||||
};
|
||||
this.api
|
||||
.getList(page.currentPage, page.pageSize, values)
|
||||
.then(res => {
|
||||
const data = res.data.data || {};
|
||||
this.page.total = data.total || 0;
|
||||
this.data = data.records || [];
|
||||
this.loading = false;
|
||||
this.selectionClear();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.project-change-page {
|
||||
:deep(.avue-crud__menu) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -577,10 +577,7 @@ export default {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少承运商信息`);
|
||||
return;
|
||||
}
|
||||
if (!row.driverName) {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少司机信息`);
|
||||
return;
|
||||
}
|
||||
// 司机是否必填由调度明细弹窗按承运类型判定(承运商承运时司机选填),此处不再重复校验
|
||||
if (!row.vehicleNo) {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
|
||||
return;
|
||||
|
||||
@@ -203,7 +203,11 @@
|
||||
@click="openBatchDialog(row)"
|
||||
>更换运单批次</el-link
|
||||
><el-link
|
||||
v-if="(row.processStatus !== '处理完成' || row.auditStatus === '待审核') && row.auditStatus !== '审核驳回'"
|
||||
v-if="
|
||||
row.processStatus !== '处理失败' &&
|
||||
(row.processStatus !== '处理完成' || row.auditStatus === '待审核') &&
|
||||
row.auditStatus !== '审核驳回'
|
||||
"
|
||||
type="primary"
|
||||
@click="openBatchDialog(row)"
|
||||
>更换运单批次</el-link
|
||||
@@ -320,7 +324,7 @@
|
||||
<el-form-item label="执行凭证" required
|
||||
><el-upload
|
||||
action="#"
|
||||
accept=".zip,.7z"
|
||||
accept=".zip"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-change="selectVoucherFile"
|
||||
@@ -332,7 +336,7 @@
|
||||
>上传</el-button
|
||||
><template #tip
|
||||
><span class="voucher-manage-page__upload-tip"
|
||||
>请上传车辆的执行凭证压缩包,支持zip、7z格式。 上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
|
||||
>请上传车辆的执行凭证压缩包,仅支持 zip 格式;无法解析的文件将标记为「处理失败」,可重新上传。上传附件大于50M时为超大附件,确认提交后可在“批量补录->上传任务”中查看上传进度。</span
|
||||
></template
|
||||
></el-upload
|
||||
><el-progress
|
||||
@@ -549,7 +553,7 @@
|
||||
ref="resumeFileInput"
|
||||
class="voucher-manage-page__hidden-file"
|
||||
type="file"
|
||||
accept=".zip,.7z"
|
||||
accept=".zip"
|
||||
@change="resumeFileSelected"
|
||||
/>
|
||||
</template>
|
||||
@@ -998,6 +1002,12 @@ const uploadVoucher = async file => {
|
||||
};
|
||||
const selectVoucherFile = file => {
|
||||
if (!file?.raw) return;
|
||||
const fileName = String(file.raw.name || '').toLowerCase();
|
||||
if (!fileName.endsWith('.zip')) {
|
||||
ElMessage.warning('仅支持 zip 压缩包,无法解析的文件将标记为处理失败');
|
||||
clearFile();
|
||||
return;
|
||||
}
|
||||
selectedVoucherFile.value = file.raw;
|
||||
editing.fileName = file.raw.name;
|
||||
editing.fileUrl = '';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:config="config"
|
||||
:crud-option="option"
|
||||
:detail-id="detailId"
|
||||
:menu-width="250"
|
||||
:menu-width="320"
|
||||
standalone-form-page
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -271,7 +271,7 @@ export default {
|
||||
this.unwrapData(
|
||||
await billLedgerApi.getAvailableOptions(
|
||||
this.billDialog.keyword,
|
||||
this.form.deptId,
|
||||
undefined,
|
||||
this.form.billLedgerId
|
||||
)
|
||||
) || [];
|
||||
|
||||
@@ -251,10 +251,10 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="不含税单价" min-width="130">
|
||||
<el-table-column label="合计金额" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.unitPriceNoTax"
|
||||
v-model="row.totalAmount"
|
||||
:disabled="readonly"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
@@ -263,9 +263,6 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="不含税金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.amountNoTax) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税率" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
@@ -282,17 +279,26 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税额" min-width="120">
|
||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||
<el-table-column label="不含税单价" min-width="140" align="right">
|
||||
<template #default="{ row }">{{ formatUnitPrice(row.unitPriceNoTax) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合计" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.totalAmount) }}</template>
|
||||
<el-table-column label="不含税金额" min-width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.amountNoTax) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税额" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.taxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" :disabled="readonly" maxlength="200" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="readonly" label="开票状态" min-width="90" align="center">
|
||||
<template #default="{ row }">{{ sheet.invoiceStatusName || '未开票' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="readonly" label="金蝶开票结果备注" min-width="200">
|
||||
<template #default="{ row }">{{ sheet.kingdeeResultRemark }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="!readonly" label="操作" width="110" fixed="right" align="center">
|
||||
<template #default="{ $index }">
|
||||
<div class="invoice-form-page__links">
|
||||
@@ -1101,15 +1107,30 @@ export default {
|
||||
addLine(sheet) {
|
||||
sheet.lines.push(emptyLine());
|
||||
},
|
||||
/**
|
||||
* 填写合计金额 + 税率后反算:
|
||||
* 合计金额 = 数量 × 不含税单价 × (1 + 税率)
|
||||
* 不含税单价 = 合计金额 / 数量 / (1 + 税率),保留 6 位小数
|
||||
* 不含税金额 = 数量 × 不含税单价,保留 2 位小数
|
||||
*/
|
||||
recalculateLine(row) {
|
||||
const quantity = Number(row.quantity || 0);
|
||||
const unitPrice = Number(row.unitPriceNoTax || 0);
|
||||
const amountNoTax = Number((quantity * unitPrice).toFixed(2));
|
||||
const totalAmount = Number(Number(row.totalAmount || 0).toFixed(2));
|
||||
const rate = Number(row.taxRate || 0) / 100;
|
||||
row.totalAmount = totalAmount;
|
||||
row.amountWithTax = totalAmount;
|
||||
if (quantity <= 0 || totalAmount <= 0) {
|
||||
row.unitPriceNoTax = 0;
|
||||
row.amountNoTax = 0;
|
||||
row.taxAmount = 0;
|
||||
return;
|
||||
}
|
||||
const factor = 1 + rate;
|
||||
const unitPriceNoTax = Number((totalAmount / quantity / factor).toFixed(6));
|
||||
const amountNoTax = Number((quantity * unitPriceNoTax).toFixed(2));
|
||||
row.unitPriceNoTax = unitPriceNoTax;
|
||||
row.amountNoTax = amountNoTax;
|
||||
row.taxAmount = rate ? Number((amountNoTax * rate).toFixed(2)) : 0;
|
||||
row.totalAmount = Number((amountNoTax + row.taxAmount).toFixed(2));
|
||||
row.amountWithTax = row.totalAmount;
|
||||
row.taxAmount = Number((totalAmount - amountNoTax).toFixed(2));
|
||||
},
|
||||
validateBusiness() {
|
||||
if (!this.selectedDetailIds.length) throw new Error('请至少选择一条开票明细');
|
||||
@@ -1122,12 +1143,14 @@ export default {
|
||||
for (const line of sheet.lines) {
|
||||
if (!line.goodsCategory || !line.goodsName) throw new Error('请完整填写商品和服务信息');
|
||||
if (
|
||||
[line.quantity, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
|
||||
[line.quantity, line.totalAmount, line.unitPriceNoTax, line.amountNoTax, line.taxRate].some(
|
||||
Number.isNaN
|
||||
)
|
||||
) {
|
||||
throw new Error('开票商品行金额格式不正确');
|
||||
}
|
||||
if (Number(line.quantity || 0) <= 0) throw new Error('开票商品行数量必须大于0');
|
||||
if (Number(line.totalAmount || 0) <= 0) throw new Error('开票商品行合计金额必须大于0');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1227,6 +1250,9 @@ export default {
|
||||
formatMoney(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatUnitPrice(value) {
|
||||
return Number(value || 0).toFixed(6);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -125,6 +125,17 @@
|
||||
{{ displayValue(row[column.prop]) }}
|
||||
</el-tag>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<template v-else-if="column.prop === 'kingdeeStatusName'">
|
||||
<el-tooltip
|
||||
:disabled="row.kingdeeStatus !== 'failed'"
|
||||
:content="row.kingdeeFailReason || '推送金蝶失败,可点击「同步金蝶」重试'"
|
||||
placement="top"
|
||||
>
|
||||
<el-tag :type="kingdeeStatusType(row.kingdeeStatus)">{{
|
||||
row.kingdeeStatusName || '-'
|
||||
}}</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template v-else-if="column.prop === 'kingdeeLockStatus'">
|
||||
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||
<el-tag :type="lockTag(row).type" class="status-text">{{ lockTag(row).label }}</el-tag>
|
||||
@@ -238,6 +249,7 @@ import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { kingdeeStatusType } from '@/utils/kingdee';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
applicationNo: '',
|
||||
@@ -270,6 +282,7 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
approveLoading: false,
|
||||
batchSyncing: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
mkEnabled: false,
|
||||
@@ -408,11 +421,17 @@ export default {
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
|
||||
if (this.approveLoading) return;
|
||||
this.approveLoading = true;
|
||||
let loading = null;
|
||||
try {
|
||||
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '审批处理中,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
@@ -420,9 +439,10 @@ export default {
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
await this.loadTable();
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
loading?.close();
|
||||
this.approveLoading = false;
|
||||
}
|
||||
},
|
||||
async handleReturn(row) {
|
||||
@@ -492,6 +512,7 @@ export default {
|
||||
''
|
||||
);
|
||||
},
|
||||
kingdeeStatusType,
|
||||
lockTag(row) {
|
||||
if (row.kingdeeLockStatus === 'locked') {
|
||||
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||
|
||||
@@ -255,6 +255,7 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
approveLoading: false,
|
||||
pushLoading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
mkEnabled: false,
|
||||
@@ -365,11 +366,17 @@ export default {
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
|
||||
if (this.approveLoading) return;
|
||||
this.approveLoading = true;
|
||||
let loading = null;
|
||||
try {
|
||||
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '审批处理中,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
@@ -377,9 +384,10 @@ export default {
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
await this.loadTable();
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
loading?.close();
|
||||
this.approveLoading = false;
|
||||
}
|
||||
},
|
||||
async handleReturn(row) {
|
||||
|
||||
@@ -219,6 +219,11 @@
|
||||
show-word-limit /></el-form-item
|
||||
></el-col>
|
||||
</el-row>
|
||||
<!-- 风险客商提示(基本信息备注后,同合同新增页):收款方外部客商高风险/黑名单时展示,文案由后端 riskTip 拼装 -->
|
||||
<div v-if="payeeRiskText" class="payment-form-page__risk-tip">
|
||||
<div class="payment-form-page__risk-tip-title">风险客商提示:</div>
|
||||
<div class="payment-form-page__risk-tip-content">{{ payeeRiskText }}</div>
|
||||
</div>
|
||||
</section-card>
|
||||
<section-card v-if="isSettlementRelatedType" title="结算信息">
|
||||
<el-table :data="settlementRows" border
|
||||
@@ -244,7 +249,7 @@
|
||||
<el-table-column prop="paymentRatio" label="付款比例(%)" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="multipleSettlementRows && !readonly"
|
||||
v-if="!readonly"
|
||||
:model-value="row.paymentRatio"
|
||||
:min="0"
|
||||
:max="rowPaymentRatioMax(row)"
|
||||
@@ -262,7 +267,7 @@
|
||||
<el-table-column prop="appliedAmount" label="付款金额" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="multipleSettlementRows && !readonly"
|
||||
v-if="!readonly"
|
||||
:model-value="row.appliedAmount"
|
||||
:min="0"
|
||||
:max="Number(row.payableAmount || 0)"
|
||||
@@ -719,6 +724,7 @@ import * as billLedgerApi from '@/api/payment/billLedger';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getDetail as getContractDetail } from '@/api/business/contract-manage';
|
||||
import { getRisk as getCustomerRisk } from '@/api/business/hcs';
|
||||
import * as formalApi from '@/api/settlement/formalSettlement';
|
||||
import * as preApi from '@/api/settlement/preSettlement';
|
||||
import {
|
||||
@@ -874,6 +880,9 @@ export default {
|
||||
/** 合同付款比例设置(按第1笔、第2笔…顺序) */
|
||||
contractPaymentRatioRows: [],
|
||||
payeeCustomerLevel: '',
|
||||
// 收款方 C101-1 风险查询留档:risky=高风险/黑名单,tip=后端拼装提示文案
|
||||
payeeRisky: false,
|
||||
payeeRiskTip: '',
|
||||
attachmentRuleLoading: false,
|
||||
// 最近一次从结算单同步过来的附件 URL,切换结算单时用于整批替换
|
||||
linkedSettlementAttachmentUrls: [],
|
||||
@@ -1014,69 +1023,53 @@ export default {
|
||||
referenceSettlementCount() {
|
||||
return this.referenceSettlementNos.length;
|
||||
},
|
||||
multipleSettlementRows() {
|
||||
return this.referenceRows.length > 1;
|
||||
},
|
||||
// 结算单号展示:单张显示单号;多张仅提示数量,明细在下方「结算信息」表,悬停可看全部单号
|
||||
referenceDisplayText() {
|
||||
if (this.referenceSettlementCount > 1) return `已选 ${this.referenceSettlementCount} 张结算单`;
|
||||
return this.referenceLabel;
|
||||
},
|
||||
settlementRows() {
|
||||
if (this.referenceRows.length > 1) {
|
||||
// 多结算单:行内比例/金额独立维护,不回落到表头比例(表头与结算信息不关联)
|
||||
return this.referenceRows.map(item => {
|
||||
const paymentRatio =
|
||||
item.paymentRatio === undefined || item.paymentRatio === null || item.paymentRatio === ''
|
||||
? 0
|
||||
: Number(item.paymentRatio);
|
||||
const appliedAmount = Number(item.appliedAmount || 0);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint({ ...item, paymentRatio }),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (this.referenceRows.length) {
|
||||
return this.referenceRows.map(item => {
|
||||
const appliedAmount =
|
||||
item.fixedAppliedAmount === true
|
||||
? Number(item.appliedAmount || 0)
|
||||
: Number(
|
||||
(
|
||||
(Number(item.settlementAmount || 0) * Number(this.form.paymentRatio || 0)) /
|
||||
100
|
||||
).toFixed(2)
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint(item),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (!this.referenceLabel) return [];
|
||||
return [
|
||||
{
|
||||
settlementNo: this.referenceLabel,
|
||||
settlementAmount: this.form.settlementAmount,
|
||||
payableAmount: this.form.payableAmount,
|
||||
paymentRatio: this.form.paymentRatio,
|
||||
appliedAmount: this.form.appliedAmount,
|
||||
paymentSequence: 1,
|
||||
...this.buildSettlementRatioHint({ paymentSequence: 1 }),
|
||||
},
|
||||
];
|
||||
const source = this.referenceRows.length
|
||||
? this.referenceRows
|
||||
: this.referenceLabel
|
||||
? [
|
||||
{
|
||||
settlementNo: this.referenceLabel,
|
||||
settlementAmount: this.form.settlementAmount,
|
||||
payableAmount: this.form.payableAmount,
|
||||
paymentRatio: null,
|
||||
appliedAmount: null,
|
||||
paymentSequence: 1,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
// 结算行比例/金额只取行内数据,不回落到付款基本信息
|
||||
return source.map(item => {
|
||||
const paymentRatio = this.nullableNumber(item.paymentRatio);
|
||||
const appliedAmount = this.nullableNumber(item.appliedAmount);
|
||||
return {
|
||||
...item,
|
||||
paymentRatio,
|
||||
appliedAmount,
|
||||
...this.buildSettlementRatioHint({ ...item, paymentRatio }),
|
||||
};
|
||||
});
|
||||
},
|
||||
highRiskPayee() {
|
||||
// 收款方 C101-1 高风险/黑名单,或客商等级 D/E/F(本地评级口径),命中须上传特批附件
|
||||
if (this.payeeRisky) return true;
|
||||
const level = String(this.payeeCustomerLevel || '')
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
return ['D', 'D级', 'E', 'E级', 'F', 'F级'].includes(level) || level.includes('高风险');
|
||||
},
|
||||
// 高风险/黑名单收款方提示(C101-1,强制拦截在后端提交校验与金蝶推送门禁):
|
||||
// 等级/黑名单明细以后端 riskTip 为准(黑名单=金蝶命中、等级=C101-1,统一口径),前端不再重复拼接
|
||||
payeeRiskText() {
|
||||
if (!this.payeeRisky) return '';
|
||||
const tip = this.payeeRiskTip || `收款方[${this.form.payeeName || ''}]为高风险/黑名单客商`;
|
||||
return `${tip},按集团规定不可直接付款,提交前须上传「特批附件」类型附件,审批通过后将自动发起付款特批`;
|
||||
},
|
||||
requiredAttachmentMaterials() {
|
||||
if (this.form.paymentType === 'project_advance') {
|
||||
return [ATTACHMENT_MATERIALS.contract];
|
||||
@@ -1127,11 +1120,12 @@ export default {
|
||||
if (
|
||||
this.amountSyncing ||
|
||||
this.form.paymentType === 'project_advance' ||
|
||||
!this.paymentAmountBase
|
||||
!this.paymentAmountBase ||
|
||||
this.nullableNumber(this.form.paymentRatio) === null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 结算金额变化时按上方比例重算申请付款金额,仍不联动结算信息明细
|
||||
// 结算金额变化时只按上方比例重算申请付款金额,不改结算信息明细
|
||||
this.amountSyncing = true;
|
||||
this.form.appliedAmount = Number(
|
||||
((this.paymentAmountBase * Number(this.form.paymentRatio || 0)) / 100).toFixed(2)
|
||||
@@ -1229,14 +1223,6 @@ export default {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
selected.billType &&
|
||||
selected.billType !== 'received' &&
|
||||
!this.billMatchesSettlementParties(selected)
|
||||
) {
|
||||
callback(new Error('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票'));
|
||||
return;
|
||||
}
|
||||
if (selected.maturityDate) {
|
||||
const maturity = this.$dayjs(selected.maturityDate).startOf('day');
|
||||
const today = this.$dayjs().startOf('day');
|
||||
@@ -1540,8 +1526,6 @@ export default {
|
||||
paymentSequence: amount.paymentSequence,
|
||||
}),
|
||||
settlementNo: item.formalSettlementNo || item.settlementNo || '',
|
||||
appliedAmount: Number(item.appliedAmount || 0),
|
||||
fixedAppliedAmount: true,
|
||||
};
|
||||
});
|
||||
this.seedMultipleRowPayments(referenceRows);
|
||||
@@ -1553,27 +1537,6 @@ export default {
|
||||
(total, item) => total + Number(item.payableAmount || 0),
|
||||
0
|
||||
);
|
||||
const multiple = referenceRows.length > 1;
|
||||
// 多结算单:表头比例/申请金额独立;单结算单:沿用行金额或按比例计算
|
||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||
const rowsAppliedTotal = referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
const headerAppliedAmount = multiple
|
||||
? Number(((settlementAmount * paymentRatio) / 100).toFixed(2))
|
||||
: Number(
|
||||
(
|
||||
rowsAppliedTotal > 0
|
||||
? rowsAppliedTotal
|
||||
: (payableAmount * paymentRatio) / 100
|
||||
).toFixed(2)
|
||||
);
|
||||
const headerPaymentRatio = multiple
|
||||
? paymentRatio
|
||||
: settlementAmount
|
||||
? Number(((headerAppliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: paymentRatio;
|
||||
const settlementIds = rows.map(item => item.settlementId || item.id).filter(Boolean);
|
||||
this.amountSyncing = true;
|
||||
this.referenceRows = referenceRows;
|
||||
@@ -1600,8 +1563,7 @@ export default {
|
||||
settlementAmount: Number(settlementAmount.toFixed(2)),
|
||||
payableAmount: Number(payableAmount.toFixed(2)),
|
||||
billType: '',
|
||||
paymentRatio: headerPaymentRatio,
|
||||
appliedAmount: headerAppliedAmount,
|
||||
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
|
||||
invoices: rows.flatMap(item =>
|
||||
this.createInvoiceRows(item.invoices || [], item.formalSettlementNo || item.settlementNo)
|
||||
),
|
||||
@@ -1657,7 +1619,7 @@ export default {
|
||||
referenceType: isPre ? 'pre' : 'formal',
|
||||
settlementAmount: Number(Number(item.settlementAmount || 0).toFixed(2)),
|
||||
payableAmount: Number(Number(item.payableAmount ?? item.settlementAmount ?? 0).toFixed(2)),
|
||||
appliedAmount: Number(Number(item.appliedAmount || 0).toFixed(2)),
|
||||
appliedAmount: this.nullableNumber(item.appliedAmount),
|
||||
paymentRatio: Number(item.settlementAmount || 0)
|
||||
? Number(
|
||||
(
|
||||
@@ -1666,11 +1628,10 @@ export default {
|
||||
).toFixed(2)
|
||||
)
|
||||
: null,
|
||||
fixedAppliedAmount: this.readonly,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (rows.length > 1) {
|
||||
if (rows.length) {
|
||||
this.form.preSettlementIds = rows.filter(item => item.referenceType === 'pre').map(item => item.id);
|
||||
this.form.settlementIds = rows.filter(item => item.referenceType === 'formal').map(item => item.id);
|
||||
if (this.form.preSettlementIds.length) this.form.preSettlementId = this.form.preSettlementIds[0];
|
||||
@@ -1704,18 +1665,27 @@ export default {
|
||||
settlementAmount: Number(Number(settlementAmount || 0).toFixed(2)),
|
||||
payableAmount: Number(Math.max(0, payableAmount).toFixed(2)),
|
||||
paymentSequence: Number.isFinite(paymentSequence) && paymentSequence > 0 ? paymentSequence : 1,
|
||||
paymentRatio: null,
|
||||
appliedAmount: null,
|
||||
};
|
||||
},
|
||||
nullableNumber(value) {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
const amount = Number(value);
|
||||
return Number.isFinite(amount) ? amount : null;
|
||||
},
|
||||
/** 付款基本信息内部:已填写比例时,按结算金额重算申请付款金额;不读取结算行付款金额。 */
|
||||
headerAppliedAmountByRatio(settlementAmount) {
|
||||
if (this.nullableNumber(this.form.paymentRatio) === null) return this.form.appliedAmount;
|
||||
return Number(((Number(settlementAmount || 0) * Number(this.form.paymentRatio)) / 100).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 按合同付款比例配置,判断当前付款次序是否超上限。
|
||||
* @param {Object} row 结算信息行(含 paymentSequence)
|
||||
*/
|
||||
buildSettlementRatioHint(row = {}) {
|
||||
const sequence = Number(row.paymentSequence || 1);
|
||||
const rawRatio =
|
||||
row.paymentRatio !== undefined && row.paymentRatio !== null && row.paymentRatio !== ''
|
||||
? row.paymentRatio
|
||||
: this.form.paymentRatio;
|
||||
const rawRatio = row.paymentRatio;
|
||||
const paymentRatio = Number(rawRatio);
|
||||
const ratioConfig = this.contractPaymentRatioRows[sequence - 1];
|
||||
const ratioLimitValue = ratioConfig?.ratioLimit;
|
||||
@@ -1858,21 +1828,6 @@ export default {
|
||||
(total, item) => total + Number(item.payableAmount || 0),
|
||||
0
|
||||
);
|
||||
const multiple = this.referenceRows.length > 1;
|
||||
const paymentRatio = Number(this.form.paymentRatio || 0);
|
||||
const rowsAppliedTotal = this.referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
// 多结算单:表头申请金额只由上方比例计算;单结算单仍按可付款金额×比例
|
||||
const headerAppliedAmount = multiple
|
||||
? Number(((settlementAmount * paymentRatio) / 100).toFixed(2))
|
||||
: Number(((payableAmount * paymentRatio) / 100).toFixed(2));
|
||||
const headerPaymentRatio = multiple
|
||||
? paymentRatio
|
||||
: settlementAmount && rowsAppliedTotal > 0
|
||||
? Number(((rowsAppliedTotal / settlementAmount) * 100).toFixed(2))
|
||||
: paymentRatio;
|
||||
// 尾款付款场景下选择预付结算单时保留尾款付款类型,不强制改为进度预付
|
||||
this.amountSyncing = true;
|
||||
Object.assign(this.form, {
|
||||
@@ -1898,8 +1853,7 @@ export default {
|
||||
settlementAmount: Number(settlementAmount.toFixed(2)),
|
||||
payableAmount: Number(payableAmount.toFixed(2)),
|
||||
billType: '',
|
||||
paymentRatio: headerPaymentRatio,
|
||||
appliedAmount: headerAppliedAmount,
|
||||
appliedAmount: this.headerAppliedAmountByRatio(settlementAmount),
|
||||
});
|
||||
this.rememberSettlementParties(first.payerName, first.payeeName);
|
||||
this.paymentRecords = [];
|
||||
@@ -2381,6 +2335,8 @@ export default {
|
||||
const preserveCurrentValue = autoSelectDefault === false;
|
||||
this.receiptAccountOptions = [];
|
||||
this.payeeCustomerLevel = '';
|
||||
this.payeeRisky = false;
|
||||
this.payeeRiskTip = '';
|
||||
if (!partyBName) {
|
||||
if (!preserveCurrentValue) this.clearReceiptAccount();
|
||||
return;
|
||||
@@ -2412,6 +2368,17 @@ export default {
|
||||
}
|
||||
const detail = this.unwrapData(await getCustomerArchiveDetail(customer.id));
|
||||
this.payeeCustomerLevel = detail.customerLevel || '';
|
||||
// 收款方实时查 C101-1 客商风险(后端对内部组织客商默认返回无风险):高风险/黑名单在
|
||||
// 基本信息"风险客商提示"条常驻展示并要求特批附件;查询失败不阻断填写,提交时后端强校验兜底
|
||||
if (detail.customerCode) {
|
||||
try {
|
||||
const risk = this.unwrapData(await getCustomerRisk(detail.customerCode)) || {};
|
||||
this.payeeRisky = Boolean(risk.risky);
|
||||
this.payeeRiskTip = risk.riskTip || '';
|
||||
} catch (error) {
|
||||
// 忽略风险查询异常,提交时后端 C101-1 强校验兜底
|
||||
}
|
||||
}
|
||||
this.receiptAccountOptions = (detail.receiptAccounts || [])
|
||||
.filter(item => item.accountName || item.accountHolderName || item.bankAccount)
|
||||
.map(item => ({
|
||||
@@ -2444,8 +2411,7 @@ export default {
|
||||
}
|
||||
},
|
||||
payeeReceiptLabel(item) {
|
||||
const name = item.accountName || item.accountHolderName || '未命名收款方';
|
||||
return item.bankAccount ? `${name}(${item.bankAccount})` : name;
|
||||
return item.accountName || item.accountHolderName || '未命名收款方';
|
||||
},
|
||||
receiptAccountLabel(item) {
|
||||
return [item.accountName || item.accountHolderName, item.bankName, item.bankAccount]
|
||||
@@ -2603,7 +2569,7 @@ export default {
|
||||
this.unwrapData(
|
||||
await billLedgerApi.getAvailableOptions(
|
||||
keyword,
|
||||
this.form.deptId,
|
||||
undefined,
|
||||
selectedId,
|
||||
this.settlementPayerName,
|
||||
this.settlementPayeeName
|
||||
@@ -2628,7 +2594,7 @@ export default {
|
||||
this.billPage.current,
|
||||
this.billPage.size,
|
||||
String(this.billQuery.keyword || '').trim() || undefined,
|
||||
this.form.deptId,
|
||||
undefined,
|
||||
this.form.billLedgerId,
|
||||
this.settlementPayerName,
|
||||
this.settlementPayeeName
|
||||
@@ -2667,14 +2633,6 @@ export default {
|
||||
);
|
||||
},
|
||||
selectBill(row) {
|
||||
if (
|
||||
row?.billType &&
|
||||
row.billType !== 'received' &&
|
||||
!this.billMatchesSettlementParties(row)
|
||||
) {
|
||||
ElMessage.warning('汇票付款仅可选择收票类型,或出票单位与收票单位均为当前结算收付款方的汇票');
|
||||
return;
|
||||
}
|
||||
if (row?.maturityDate) {
|
||||
const maturity = this.$dayjs(row.maturityDate).startOf('day');
|
||||
const today = this.$dayjs().startOf('day');
|
||||
@@ -2736,7 +2694,8 @@ export default {
|
||||
this.referenceParams('formal')
|
||||
);
|
||||
const data = this.unwrapData(response) || {};
|
||||
const records = data.records || [];
|
||||
// 已调整的结算单为调整重推后的归档原单,金额已作废,不可再发起付款
|
||||
const records = (data.records || []).filter(row => row.adjustStatus !== 'adjusted');
|
||||
this.formalRows = await this.fillReferencePayableAmounts(records, 'settlement_payment');
|
||||
this.formalPage.total = Number(data.total || 0);
|
||||
} finally {
|
||||
@@ -3097,22 +3056,54 @@ export default {
|
||||
return Number(Math.min(100, (payableAmount / settlementAmount) * 100).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 多结算单行金额规范化:仅在行内反算比例,不使用表头比例灌入(表头与结算信息不关联)。
|
||||
* 结算行金额规范化:只在行内反算比例,不使用付款基本信息的比例和申请金额。
|
||||
*/
|
||||
seedMultipleRowPayments(rows = []) {
|
||||
if (rows.length <= 1) return;
|
||||
rows.forEach(item => {
|
||||
const settlementAmount = Number(item.settlementAmount || 0);
|
||||
const payableAmount = Number(item.payableAmount || 0);
|
||||
let applied = Number(item.appliedAmount || 0);
|
||||
if (!Number.isFinite(applied) || applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
item.appliedAmount = Number(applied.toFixed(2));
|
||||
item.paymentRatio = settlementAmount
|
||||
? Number(((item.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
if (this.nullableNumber(item.appliedAmount) === null && this.nullableNumber(item.paymentRatio) === null) {
|
||||
item.appliedAmount = null;
|
||||
item.paymentRatio = null;
|
||||
return;
|
||||
}
|
||||
this.syncRowPayment(item, 'amount');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 结算行付款比例与付款金额联动。比例 = 付款金额 / 结算总金额。
|
||||
* @param {'ratio'|'amount'} source 以哪一侧为准
|
||||
*/
|
||||
syncRowPayment(row, source) {
|
||||
const settlementAmount = Number(row.settlementAmount || 0);
|
||||
const payableAmount = Number(row.payableAmount || 0);
|
||||
if (source === 'ratio') {
|
||||
const ratio = this.nullableNumber(row.paymentRatio);
|
||||
if (ratio === null) {
|
||||
row.paymentRatio = null;
|
||||
row.appliedAmount = null;
|
||||
return;
|
||||
}
|
||||
let applied = Number(((settlementAmount * ratio) / 100).toFixed(2));
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
row.appliedAmount = applied;
|
||||
row.paymentRatio = settlementAmount
|
||||
? Number(((applied / settlementAmount) * 100).toFixed(2))
|
||||
: Number(ratio.toFixed(2));
|
||||
return;
|
||||
}
|
||||
const appliedValue = this.nullableNumber(row.appliedAmount);
|
||||
if (appliedValue === null) {
|
||||
row.appliedAmount = null;
|
||||
row.paymentRatio = null;
|
||||
return;
|
||||
}
|
||||
let applied = appliedValue;
|
||||
if (applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = payableAmount;
|
||||
row.appliedAmount = Number(applied.toFixed(2));
|
||||
row.paymentRatio = settlementAmount
|
||||
? Number(((row.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
},
|
||||
findReferenceRow(row) {
|
||||
return this.referenceRows.find(
|
||||
item =>
|
||||
@@ -3121,59 +3112,26 @@ export default {
|
||||
item.settlementNo === row.settlementNo
|
||||
);
|
||||
},
|
||||
/** 结算信息付款金额合计 */
|
||||
settlementRowsAppliedTotal(rows = this.referenceRows) {
|
||||
return (rows || []).reduce((total, item) => total + Number(item.appliedAmount || 0), 0);
|
||||
},
|
||||
/** 结算信息合计不得超过上方申请付款金额 */
|
||||
ensureSettlementAppliedWithinHeader(showWarning = true) {
|
||||
if (!this.multipleSettlementRows) return true;
|
||||
const headerApplied = Number(this.form.appliedAmount || 0);
|
||||
const rowsTotal = Number(this.settlementRowsAppliedTotal().toFixed(2));
|
||||
if (rowsTotal - headerApplied > 1e-8) {
|
||||
if (showWarning) {
|
||||
this.$message.warning(
|
||||
`结算信息付款金额合计(${rowsTotal.toFixed(2)})不能大于申请付款金额(${headerApplied.toFixed(2)})`
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleRowPaymentRatioChange(row, value) {
|
||||
const target = this.findReferenceRow(row);
|
||||
if (!target) return;
|
||||
const settlementAmount = Number(target.settlementAmount || 0);
|
||||
const payableAmount = Number(target.payableAmount || 0);
|
||||
const ratio = value === null || value === undefined || value === '' ? 0 : Number(value);
|
||||
let applied = Number(((settlementAmount * ratio) / 100).toFixed(2));
|
||||
if (applied > payableAmount) applied = Number(payableAmount.toFixed(2));
|
||||
target.appliedAmount = applied;
|
||||
target.paymentRatio = settlementAmount
|
||||
? Number(((applied / settlementAmount) * 100).toFixed(2))
|
||||
: Number(ratio.toFixed(2));
|
||||
// 仅刷新结算金额/可付款金额合计,不回写表头申请金额与比例
|
||||
this.syncHeaderSettlementTotalsFromRows();
|
||||
this.ensureSettlementAppliedWithinHeader(true);
|
||||
target.paymentRatio = value;
|
||||
this.syncRowPayment(target, 'ratio');
|
||||
this.syncHeaderSettlementTotalsFromRows(true);
|
||||
},
|
||||
handleRowAppliedAmountChange(row, value) {
|
||||
const target = this.findReferenceRow(row);
|
||||
if (!target) return;
|
||||
const settlementAmount = Number(target.settlementAmount || 0);
|
||||
const payableAmount = Number(target.payableAmount || 0);
|
||||
let applied = value === null || value === undefined || value === '' ? 0 : Number(value);
|
||||
if (!Number.isFinite(applied) || applied < 0) applied = 0;
|
||||
if (applied > payableAmount) applied = payableAmount;
|
||||
target.appliedAmount = Number(applied.toFixed(2));
|
||||
target.paymentRatio = settlementAmount
|
||||
? Number(((target.appliedAmount / settlementAmount) * 100).toFixed(2))
|
||||
: 0;
|
||||
this.syncHeaderSettlementTotalsFromRows();
|
||||
this.ensureSettlementAppliedWithinHeader(true);
|
||||
target.appliedAmount = value;
|
||||
this.syncRowPayment(target, 'amount');
|
||||
this.syncHeaderSettlementTotalsFromRows(true);
|
||||
},
|
||||
/** 多结算单时仅同步结算金额/可付款金额合计,申请付款金额与比例由上方独立维护 */
|
||||
syncHeaderSettlementTotalsFromRows() {
|
||||
if (this.referenceRows.length <= 1) return;
|
||||
/**
|
||||
* 结算信息行合计回填付款基本信息。
|
||||
* @param {boolean} syncApplied 行内编辑时同步申请付款金额与比例;回显加载时保持原值,避免改写历史单据
|
||||
*/
|
||||
syncHeaderSettlementTotalsFromRows(syncApplied = false) {
|
||||
if (!this.referenceRows.length) return;
|
||||
const settlementAmount = this.referenceRows.reduce(
|
||||
(total, item) => total + Number(item.settlementAmount || 0),
|
||||
0
|
||||
@@ -3182,9 +3140,21 @@ export default {
|
||||
(total, item) => total + Number(item.payableAmount || 0),
|
||||
0
|
||||
);
|
||||
const appliedAmount = this.referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
this.amountSyncing = true;
|
||||
this.form.settlementAmount = Number(settlementAmount.toFixed(2));
|
||||
this.form.payableAmount = Number(payableAmount.toFixed(2));
|
||||
if (syncApplied) {
|
||||
this.form.appliedAmount = Number(appliedAmount.toFixed(2));
|
||||
if (this.paymentAmountBase) {
|
||||
this.form.paymentRatio = Number(
|
||||
((appliedAmount / this.paymentAmountBase) * 100).toFixed(2)
|
||||
);
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.amountSyncing = false;
|
||||
this.$refs.formRef?.validateField?.(['settlementAmount', 'appliedAmount']);
|
||||
@@ -3278,8 +3248,8 @@ export default {
|
||||
settlementLines: (this.referenceRows || []).map(item => ({
|
||||
formalSettlementId: item.referenceType === 'formal' ? item.id : null,
|
||||
preSettlementId: item.referenceType === 'pre' ? item.id : null,
|
||||
paymentRatio: item.paymentRatio,
|
||||
appliedAmount: item.appliedAmount,
|
||||
paymentRatio: this.nullableNumber(item.paymentRatio),
|
||||
appliedAmount: this.nullableNumber(item.appliedAmount),
|
||||
})),
|
||||
paymentMethod,
|
||||
billLedgerId,
|
||||
@@ -3340,20 +3310,33 @@ export default {
|
||||
ElMessage.warning('申请付款金额必填且必须大于0');
|
||||
return false;
|
||||
}
|
||||
if (this.multipleSettlementRows) {
|
||||
const invalidRow = this.referenceRows.find(
|
||||
item => Number(item.appliedAmount || 0) > Number(item.payableAmount || 0)
|
||||
);
|
||||
if (this.isSettlementRelatedType && this.referenceRows.length) {
|
||||
const invalidRow = this.referenceRows.find(item => {
|
||||
const rowApplied = Number(item.appliedAmount || 0);
|
||||
return !Number.isFinite(rowApplied) || rowApplied <= 0;
|
||||
});
|
||||
if (invalidRow) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning(`${invalidRow.settlementNo || '结算单'}付款金额不能超过可付款金额`);
|
||||
ElMessage.warning(`${invalidRow.settlementNo || '结算单'}请填写付款金额,且必须大于0`);
|
||||
return false;
|
||||
}
|
||||
const rowsTotal = Number(this.settlementRowsAppliedTotal().toFixed(2));
|
||||
if (rowsTotal - appliedAmount > 1e-8) {
|
||||
const exceededRow = this.referenceRows.find(
|
||||
item => Number(item.appliedAmount || 0) > Number(item.payableAmount || 0)
|
||||
);
|
||||
if (exceededRow) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning(`${exceededRow.settlementNo || '结算单'}付款金额不能超过可付款金额`);
|
||||
return false;
|
||||
}
|
||||
const rowAppliedTotal = this.referenceRows.reduce(
|
||||
(total, item) => total + Number(item.appliedAmount || 0),
|
||||
0
|
||||
);
|
||||
// 金蝶按付款基本信息申请金额推单,行合计不等时预付建档分摊会与金蝶金额不一致
|
||||
if (Math.abs(rowAppliedTotal - appliedAmount) > 0.005) {
|
||||
this.clearMessages();
|
||||
ElMessage.warning(
|
||||
`结算信息付款金额合计(${rowsTotal.toFixed(2)})不能大于申请付款金额(${appliedAmount.toFixed(2)})`
|
||||
`结算行付款金额合计${rowAppliedTotal.toFixed(2)}与申请付款金额${appliedAmount.toFixed(2)}不一致,请调整`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -3388,6 +3371,15 @@ export default {
|
||||
this.clearMessages();
|
||||
const saved = await this.saveDraft({ showSuccess: false, closePage: false });
|
||||
if (!saved) return;
|
||||
// 高风险/黑名单收款方必须上传特批附件(与后端提交强校验同口径)
|
||||
if (
|
||||
this.missingAttachmentMaterials.some(
|
||||
item => item.key === ATTACHMENT_MATERIALS.specialApproval.key
|
||||
)
|
||||
) {
|
||||
this.$message.warning('收款方为高风险/黑名单客商,必须上传「特批附件」类型附件后再提交审批');
|
||||
return;
|
||||
}
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
await api.submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
@@ -3420,6 +3412,26 @@ export default {
|
||||
.payment-form-page__form {
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
|
||||
// 风险客商提示条(基本信息备注后,同合同新增页):橙黄背景 + 红色文字
|
||||
.payment-form-page__risk-tip {
|
||||
margin: 0 0 18px;
|
||||
padding: 10px 16px;
|
||||
background: #faecd8;
|
||||
border-radius: 4px;
|
||||
|
||||
.payment-form-page__risk-tip-title {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.payment-form-page__risk-tip-content {
|
||||
color: #f56c6c;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-form-page__settlement-nos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -68,6 +68,18 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
<template #kingdeeStatusName="{ row }">
|
||||
<el-tooltip
|
||||
:disabled="row.kingdeeStatus !== 'failed'"
|
||||
:content="row.kingdeeFailReason || '推送金蝶失败,可点击「同步金蝶」重试'"
|
||||
placement="top"
|
||||
>
|
||||
<el-tag :type="kingdeeStatusType(row.kingdeeStatus)">{{
|
||||
row.kingdeeStatusName || '-'
|
||||
}}</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_view')"
|
||||
@@ -151,6 +163,7 @@ import { paymentApplicationOption } from '@/option/payment/paymentApplication';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { kingdeeStatusType } from '@/utils/kingdee';
|
||||
|
||||
const paymentApplicationListRoute = '/payment/payment-application';
|
||||
const paymentApplicationListRefreshKey = 'payment-application-list-refresh';
|
||||
@@ -163,6 +176,7 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
approveLoading: false,
|
||||
syncLoading: false,
|
||||
resultSyncing: false,
|
||||
query: {},
|
||||
@@ -327,10 +341,17 @@ export default {
|
||||
this.refreshChange();
|
||||
},
|
||||
async handleApprove(row) {
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
|
||||
if (this.approveLoading) return;
|
||||
this.approveLoading = true;
|
||||
let loading = null;
|
||||
try {
|
||||
await this.$confirm('确认审批通过该付款申请?', '提示', { type: 'warning' });
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '审批处理中,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
@@ -338,9 +359,10 @@ export default {
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
await this.onLoad(this.page);
|
||||
this.refreshChange();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
loading?.close();
|
||||
this.approveLoading = false;
|
||||
}
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
@@ -454,6 +476,7 @@ export default {
|
||||
''
|
||||
);
|
||||
},
|
||||
kingdeeStatusType,
|
||||
lockTag(row) {
|
||||
if (row.kingdeeLockStatus === 'locked') {
|
||||
return { show: true, type: 'danger', label: '金蝶锁定中', tip: '该单据已被金蝶锁定,不可编辑/作废' };
|
||||
|
||||
@@ -64,6 +64,20 @@
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select' && editable"
|
||||
v-model="form[field.prop]"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in formSelectOptions(field)"
|
||||
:key="item.dictKey"
|
||||
:label="item.dictValue"
|
||||
:value="item.dictKey"
|
||||
/>
|
||||
</el-select>
|
||||
<span
|
||||
v-else-if="field.prop === 'formalSettlementNo'"
|
||||
:class="{ 'form-readonly--code': form.formalSettlementNo }"
|
||||
@@ -71,6 +85,9 @@
|
||||
{{ form.formalSettlementNo || (form.id ? '-' : '系统自动生成') }}
|
||||
</span>
|
||||
<span v-else-if="field.money">{{ formatMoney(form[field.prop]) }}</span>
|
||||
<span v-else-if="field.type === 'select'">{{
|
||||
formSelectLabel(field, form[field.prop])
|
||||
}}</span>
|
||||
<span v-else-if="field.type === 'contract'">{{
|
||||
displayValue(form.contractName)
|
||||
}}</span>
|
||||
@@ -424,26 +441,7 @@
|
||||
|
||||
<section-card title="发票信息">
|
||||
<div class="formal-editor__invoice-claim">
|
||||
<el-form inline label-position="right" label-width="auto" @submit.prevent>
|
||||
<el-form-item label="发票号码">
|
||||
<el-input
|
||||
v-model="invoiceClaimNo"
|
||||
clearable
|
||||
maxlength="32"
|
||||
placeholder="请输入发票号码"
|
||||
@keyup.enter="openInvoiceClaimDialog"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="invoiceClaimLoading"
|
||||
@click="openInvoiceClaimDialog"
|
||||
>
|
||||
认领查询
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" @click="openInvoiceClaimDialog">认领查询</el-button>
|
||||
</div>
|
||||
<el-table :data="invoices" border>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
@@ -544,7 +542,6 @@
|
||||
title="查询可认领发票"
|
||||
confirm-text="确认认领"
|
||||
multiple
|
||||
:initial-invoice-no="String(invoiceClaimNo || '').trim()"
|
||||
:query-api="invoicePoolQueryApi"
|
||||
@confirm="handleInvoiceClaimConfirm"
|
||||
/>
|
||||
@@ -697,11 +694,23 @@
|
||||
<el-table-column prop="changeTime" label="变更时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ displayValue(row.changeTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openChangeRecordDetail(row)">详情</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section-card>
|
||||
</div>
|
||||
|
||||
<change-record-detail-dialog
|
||||
v-model="changeRecordDetailVisible"
|
||||
:rows="changeRecordDetailRows"
|
||||
/>
|
||||
<template v-if="!pageMode" #footer>
|
||||
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
|
||||
<el-button v-if="canSync" type="primary" plain :loading="syncLoading" @click="handleSync"
|
||||
>同步</el-button
|
||||
>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button
|
||||
v-if="editable"
|
||||
@@ -717,7 +726,9 @@
|
||||
>
|
||||
</template>
|
||||
<div v-if="pageMode && !publicMode" class="formal-editor__page-actions">
|
||||
<el-button v-if="editable" @click="handleSyncPlaceholder">同步</el-button>
|
||||
<el-button v-if="canSync" type="primary" plain :loading="syncLoading" @click="handleSync"
|
||||
>同步</el-button
|
||||
>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button
|
||||
v-if="editable"
|
||||
@@ -1044,6 +1055,7 @@ import {
|
||||
claimInvoices as claimInvoicesApi,
|
||||
queryInvoicePool,
|
||||
save,
|
||||
syncKingdee,
|
||||
} from '@/api/settlement/formalSettlement';
|
||||
import {
|
||||
getDetail as getPreSettlementDetail,
|
||||
@@ -1071,6 +1083,7 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { h } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
|
||||
import { isApprovalSettlementChange } from '@/utils/settlement-change-record';
|
||||
import { normalizeCurrencyCode } from '@/utils/currency';
|
||||
import {
|
||||
settlementRecordTypeName,
|
||||
@@ -1083,6 +1096,7 @@ import {
|
||||
viewKingdeeInvoiceFile,
|
||||
} from '@/utils/kingdee-attachment';
|
||||
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
|
||||
import ProjectAdvanceWriteOff from './project-advance-write-off.vue';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
@@ -1093,7 +1107,12 @@ const LOCAL_CURRENCY_HIDDEN_FIELDS = ['exchangeRateDate', 'exchangeRate', 'local
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementEditor',
|
||||
components: { InfoFilled, KingdeeInvoicePoolDialog, ProjectAdvanceWriteOff },
|
||||
components: {
|
||||
InfoFilled,
|
||||
KingdeeInvoicePoolDialog,
|
||||
ChangeRecordDetailDialog,
|
||||
ProjectAdvanceWriteOff,
|
||||
},
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
recordId: [String, Number],
|
||||
@@ -1114,6 +1133,7 @@ export default {
|
||||
loading: false,
|
||||
draftSaving: false,
|
||||
saving: false,
|
||||
syncLoading: false,
|
||||
form: createFormalSettlementForm(),
|
||||
sources: [],
|
||||
details: [],
|
||||
@@ -1123,14 +1143,14 @@ export default {
|
||||
receiptClaims: [],
|
||||
adjustments: [],
|
||||
changeRecords: [],
|
||||
changeRecordDetailVisible: false,
|
||||
changeRecordDetailRows: [],
|
||||
projectAdvanceWriteOffs: [],
|
||||
attachments: [],
|
||||
selectedAttachmentRows: [],
|
||||
invoices: [],
|
||||
// 开票申请经金蝶回写的发票结果(只读展示,含板式文件/数电PDF下载地址)
|
||||
issuedInvoices: [],
|
||||
invoiceClaimNo: '',
|
||||
invoiceClaimLoading: false,
|
||||
// 金蝶侧发票附件查看/下载进行中(行组级 loading 守卫)
|
||||
kingdeeAttachmentBusy: false,
|
||||
invoiceClaimDialog: {
|
||||
@@ -1162,6 +1182,7 @@ export default {
|
||||
projects: [],
|
||||
feeOptions: [],
|
||||
feeCategoryOptions: [],
|
||||
businessModelOptions: [],
|
||||
transportTypeOptions: [],
|
||||
fields: formalSettlementFormFields,
|
||||
sourceTableColumns: sourceColumns,
|
||||
@@ -1256,6 +1277,16 @@ export default {
|
||||
editable() {
|
||||
return !this.readonly;
|
||||
},
|
||||
// 查看页面「同步」按钮:仅已审批且未同步金蝶(含推送失败)的单据显示,口径与后端推送闸门一致
|
||||
canSync() {
|
||||
return (
|
||||
this.readonly &&
|
||||
!this.publicMode &&
|
||||
this.hasPermission('sync') &&
|
||||
this.form.approvalStatus === 'approved' &&
|
||||
this.form.kingdeeSyncStatus !== 'synced'
|
||||
);
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -1373,16 +1404,12 @@ export default {
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.form.localSettlementAmount = (
|
||||
Number(value || 0) * Number(this.form.exchangeRate || 1)
|
||||
).toFixed(2);
|
||||
summaryTotal() {
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
'form.exchangeRate'(value) {
|
||||
this.form.localSettlementAmount = (
|
||||
Number(this.summaryTotal || 0) * Number(value || 1)
|
||||
).toFixed(2);
|
||||
'form.exchangeRate'() {
|
||||
if (this._skipExchangeRateRebuild) return;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -1438,7 +1465,9 @@ export default {
|
||||
this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || [];
|
||||
}
|
||||
this.adjustments = data.adjustments || [];
|
||||
this.changeRecords = data.changeRecords || [];
|
||||
this.changeRecords = (data.changeRecords || []).filter(
|
||||
row => !isApprovalSettlementChange(row.changeContent)
|
||||
);
|
||||
this.projectAdvanceWriteOffs = (data.projectAdvanceWriteOffs || []).map(row => ({
|
||||
...row,
|
||||
writeOffAmount: Number(row.writeOffAmount || 0),
|
||||
@@ -1476,13 +1505,14 @@ export default {
|
||||
this.receiptClaims = [];
|
||||
this.adjustments = [];
|
||||
this.changeRecords = [];
|
||||
this.changeRecordDetailVisible = false;
|
||||
this.changeRecordDetailRows = [];
|
||||
this.projectAdvanceWriteOffs = [];
|
||||
this.advancePayments = [];
|
||||
this.attachments = [];
|
||||
this.selectedAttachmentRows = [];
|
||||
this.invoices = [];
|
||||
this.issuedInvoices = [];
|
||||
this.invoiceClaimNo = '';
|
||||
this.invoiceClaimDialog.visible = false;
|
||||
this.invoiceClaimDialog.selected = [];
|
||||
this.attachmentUploadFiles = [];
|
||||
@@ -1499,6 +1529,7 @@ export default {
|
||||
this.loadFeeOptions(),
|
||||
this.loadFeeCategoryOptions(),
|
||||
this.loadAttachmentTypeOptions(),
|
||||
this.loadBusinessModelOptions(),
|
||||
this.loadTransportTypeOptions(),
|
||||
]);
|
||||
}
|
||||
@@ -1664,8 +1695,9 @@ export default {
|
||||
current.settlementAmount = current.originalAmount;
|
||||
summaryMap.set(key, current);
|
||||
});
|
||||
// 先保留预结算手工费用行,再按明细本位币 + 结算汇率重算系统归集行
|
||||
this.summaryFees = [...summaryMap.values()];
|
||||
if (!this.summaryFees.length) await this.refreshSummaryFees();
|
||||
await this.refreshSummaryFees();
|
||||
},
|
||||
async loadAllContracts() {
|
||||
const response = await getContractOptions('');
|
||||
@@ -1767,13 +1799,19 @@ export default {
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
const currency = this.form.currency;
|
||||
if (!currency || currency === 'RMB') return;
|
||||
if (!currency || currency === 'RMB') {
|
||||
this.buildSummaryFeesFromDetails();
|
||||
return;
|
||||
}
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this._skipExchangeRateRebuild = true;
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
this._skipExchangeRateRebuild = false;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${currency} 的BFI汇率数据`);
|
||||
}
|
||||
@@ -1781,6 +1819,37 @@ export default {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
|
||||
*/
|
||||
convertLocalToSettlementAmount(localAmount) {
|
||||
const amount = Number(localAmount || 0);
|
||||
if (!Number.isFinite(amount)) return 0;
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
return Number(amount.toFixed(2));
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
if (rate <= 0) return 0;
|
||||
return Number((amount / rate).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
|
||||
*/
|
||||
syncSettlementAmountsFromSummary() {
|
||||
const settlementTotal = this.summaryFees.reduce(
|
||||
(total, row) => total + Number(row.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
this.form.localSettlementAmount = this.form.settlementAmount;
|
||||
this.form.exchangeRate = 1;
|
||||
return;
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount =
|
||||
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
|
||||
},
|
||||
openCandidateDialog() {
|
||||
this.candidate.visible = true;
|
||||
this.candidate.page.current = 1;
|
||||
@@ -2002,8 +2071,9 @@ export default {
|
||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||
);
|
||||
const append = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||
const originalAmount = Number(originalValue || 0);
|
||||
const settlementAmount = Number(settlementValue || 0);
|
||||
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
|
||||
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
|
||||
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
|
||||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||
return;
|
||||
}
|
||||
@@ -2041,6 +2111,7 @@ export default {
|
||||
});
|
||||
const manualRows = this.summaryFees.filter(row => Number(row.manualFlag) === 1);
|
||||
this.summaryFees = [...generatedMap.values(), ...manualRows];
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少费用快照时退化为明细自身的汇总行。
|
||||
resolveDetailFeePairs(detail) {
|
||||
@@ -2424,6 +2495,7 @@ export default {
|
||||
row.settlementAmountTax = Number(data.afterAmount || 0);
|
||||
} catch (error) {
|
||||
row.calculateError = error.message || '费用试算失败';
|
||||
this.$message.warning(row.calculateError);
|
||||
} finally {
|
||||
if (version === row.adjustCalculateVersion) row.calculating = false;
|
||||
}
|
||||
@@ -2796,6 +2868,8 @@ export default {
|
||||
sourceDetailId: row.sourcePreSettlementId ? undefined : row.sourceDetailId || row.id,
|
||||
adjustAmount: Number(row.adjustAmount || 0),
|
||||
})),
|
||||
businessModel: this.form.businessModel || undefined,
|
||||
transportType: this.form.transportType || undefined,
|
||||
exchangeRateDate: this.form.exchangeRateDate,
|
||||
exchangeRate: this.form.exchangeRate,
|
||||
summaryFees: this.summaryFees
|
||||
@@ -2829,8 +2903,30 @@ export default {
|
||||
remark: this.form.remark,
|
||||
};
|
||||
},
|
||||
handleSyncPlaceholder() {
|
||||
this.$message.info('开发中');
|
||||
// 查看页面单张推送金蝶(A102-2/A302-2):结果与异常原因弹框展示,成败后均刷新详情与列表
|
||||
async handleSync() {
|
||||
if (this.syncLoading) return;
|
||||
this.syncLoading = true;
|
||||
try {
|
||||
const res = await syncKingdee(this.form.id, { meta: { suppressErrorMessage: true } });
|
||||
await this.loadFormalDetail(this.recordId);
|
||||
this.$emit('success');
|
||||
let message = `同步成功:${this.unwrapData(res) || '-'}`;
|
||||
if (['pending', 'partial'].includes(this.form.attachmentSyncStatus)) {
|
||||
message += '\n该单附件未全部上传,可重推补传';
|
||||
}
|
||||
await this.$alert(message, '同步结果', { type: 'success', confirmButtonText: '知道了' });
|
||||
} catch (error) {
|
||||
const reason = String(error?.message || error || '未知错误').trim() || '未知错误';
|
||||
await this.loadFormalDetail(this.recordId);
|
||||
this.$emit('success');
|
||||
await this.$alert(`同步失败,${reason}`, '同步结果', {
|
||||
type: 'error',
|
||||
confirmButtonText: '知道了',
|
||||
});
|
||||
} finally {
|
||||
this.syncLoading = false;
|
||||
}
|
||||
},
|
||||
async handleDraftSave() {
|
||||
// 保存草稿:不做表单校验,直接落草稿并返回列表刷新
|
||||
@@ -2886,6 +2982,55 @@ export default {
|
||||
const lineNo = row?.lineNo;
|
||||
return lineNo === -1 || String(lineNo) === '-1' ? '' : this.displayValue(lineNo);
|
||||
},
|
||||
parseChangeRecordData(value) {
|
||||
if (!value) return {};
|
||||
if (typeof value === 'object') return value;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
formatChangeRecordValue(value) {
|
||||
if (value === undefined || value === null || value === '') return '空';
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) return '空';
|
||||
return value
|
||||
.map(item => {
|
||||
if (!item || typeof item !== 'object') return String(item);
|
||||
return Object.entries(item)
|
||||
.map(([key, itemValue]) => `${key}:${this.formatChangeRecordValue(itemValue)}`)
|
||||
.join(',');
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
},
|
||||
buildChangeRecordDetailRows(row = {}) {
|
||||
const before = this.parseChangeRecordData(row.beforeData);
|
||||
const after = this.parseChangeRecordData(row.afterData);
|
||||
const fields = [...new Set([...Object.keys(before), ...Object.keys(after)])];
|
||||
const rows = fields
|
||||
.filter(field => JSON.stringify(before[field]) !== JSON.stringify(after[field]))
|
||||
.map(field => ({
|
||||
field,
|
||||
before: this.formatChangeRecordValue(before[field]),
|
||||
after: this.formatChangeRecordValue(after[field]),
|
||||
}));
|
||||
if (row.changeContent) {
|
||||
rows.unshift({ field: '变更内容', before: '空', after: row.changeContent });
|
||||
}
|
||||
if (row.changeReason) {
|
||||
rows.push({ field: '变更原因', before: '空', after: row.changeReason });
|
||||
}
|
||||
return rows;
|
||||
},
|
||||
openChangeRecordDetail(row) {
|
||||
this.changeRecordDetailRows = this.buildChangeRecordDetailRows(row);
|
||||
this.changeRecordDetailVisible = true;
|
||||
},
|
||||
paymentMethodName(value) {
|
||||
const labels = {
|
||||
bank_transfer: '银行转账',
|
||||
@@ -2915,9 +3060,36 @@ export default {
|
||||
formatDetailMileage(value) {
|
||||
return this.readonly && Number(value) === -1 ? '-' : this.displayValue(value);
|
||||
},
|
||||
async loadBusinessModelOptions() {
|
||||
const { data } = await getDictionary({ code: 'business_model' });
|
||||
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
|
||||
this.businessModelOptions = records.map(item => ({
|
||||
dictKey: item.dictKey ?? item.value,
|
||||
dictValue: item.dictValue ?? item.label ?? item.name,
|
||||
}));
|
||||
},
|
||||
async loadTransportTypeOptions() {
|
||||
const { data } = await getDictionary({ code: 'transport_type' });
|
||||
this.transportTypeOptions = data?.data || [];
|
||||
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
|
||||
this.transportTypeOptions = records.map(item => ({
|
||||
dictKey: item.dictKey ?? item.value,
|
||||
dictValue: item.dictValue ?? item.label ?? item.name,
|
||||
}));
|
||||
},
|
||||
formSelectOptions(field) {
|
||||
const key = field?.optionsKey;
|
||||
const options = key ? this[key] : [];
|
||||
return Array.isArray(options) ? options : [];
|
||||
},
|
||||
formSelectLabel(field, value) {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
const normalized = String(value).trim();
|
||||
const option = this.formSelectOptions(field).find(
|
||||
item =>
|
||||
String(item.dictKey ?? '').trim() === normalized ||
|
||||
String(item.dictValue ?? '').trim() === normalized
|
||||
);
|
||||
return option?.dictValue || normalized;
|
||||
},
|
||||
transportTypeName(value) {
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
@@ -3088,10 +3260,47 @@ export default {
|
||||
return files.length ? JSON.stringify(files) : '';
|
||||
},
|
||||
parseFeeItems(value) {
|
||||
const raw = this.parseRawFeeItems(value);
|
||||
const metaKeys = new Set([
|
||||
'currency',
|
||||
'totalFreightAmount',
|
||||
'otherFreightAmount',
|
||||
'freightItems',
|
||||
'freightTotal',
|
||||
'totalFreight',
|
||||
'totalAmount',
|
||||
'freight',
|
||||
'freightAmount',
|
||||
'transportFee',
|
||||
'otherFeeTotal',
|
||||
'otherAmount',
|
||||
]);
|
||||
const result = {};
|
||||
let otherFeeAmount = 0;
|
||||
Object.entries(raw).forEach(([key, amount]) => {
|
||||
if (!key) return;
|
||||
if (key === 'otherFreightAmount' || key === 'otherFeeTotal' || key === 'otherAmount') {
|
||||
otherFeeAmount += Number(amount || 0);
|
||||
return;
|
||||
}
|
||||
if (metaKeys.has(key)) return;
|
||||
if (amount && typeof amount === 'object') return;
|
||||
result[key] = amount;
|
||||
});
|
||||
const hasOtherFee = Object.keys(result).some(
|
||||
name => String(name).includes('其它费') || String(name).includes('其他费')
|
||||
);
|
||||
if (otherFeeAmount && !hasOtherFee) {
|
||||
result['其它费用'] = otherFeeAmount;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
parseRawFeeItems(value) {
|
||||
if (!value) return {};
|
||||
if (typeof value === 'object') return value;
|
||||
if (typeof value === 'object') return { ...value };
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
@@ -3253,21 +3462,9 @@ export default {
|
||||
}
|
||||
.formal-editor__invoice-claim {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.formal-editor__invoice-claim-hint {
|
||||
color: var(--el-color-danger);
|
||||
font-size: 13px;
|
||||
}
|
||||
.formal-editor__invoice-claim :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.formal-editor__invoice-claim :deep(.el-input) {
|
||||
width: 360px;
|
||||
}
|
||||
.formal-editor__issued-invoices {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,17 @@
|
||||
}}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #kingdeeSyncStatus="{ row }">{{ kingdeeSyncName(row.kingdeeSyncStatus) }}</template>
|
||||
<template #kingdeeSyncStatus="{ row }">
|
||||
<el-tooltip
|
||||
:disabled="row.kingdeeSyncStatus !== 'failed'"
|
||||
:content="row.kingdeeFailReason || '推送金蝶失败,可点击「同步金蝶」重试'"
|
||||
placement="top"
|
||||
>
|
||||
<el-tag :type="kingdeeStatusType(row.kingdeeSyncStatus)">{{
|
||||
kingdeeSyncName(row.kingdeeSyncStatus)
|
||||
}}</el-tag>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<template #kingdeeLockStatus="{ row }">
|
||||
<el-tooltip v-if="lockTag(row).show" :content="lockTag(row).tip" placement="top">
|
||||
@@ -84,6 +94,7 @@
|
||||
<script>
|
||||
import { invoiceStatusName } from '@/option/settlement/formalSettlementSearch';
|
||||
import { kingdeeSyncName, paymentStatusName } from '@/option/settlement/formalSettlement';
|
||||
import { kingdeeStatusType } from '@/utils/kingdee';
|
||||
|
||||
export default {
|
||||
name: 'FormalSettlementTablePanel',
|
||||
@@ -186,6 +197,7 @@ export default {
|
||||
''
|
||||
);
|
||||
},
|
||||
kingdeeStatusType,
|
||||
invoiceName(value, settlementType) {
|
||||
return invoiceStatusName(value, settlementType);
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<el-form-item :label="field.label" :prop="field.prop">
|
||||
<el-input
|
||||
v-if="field.type === 'contract'"
|
||||
:model-value="formatContractOptionLabel(form)"
|
||||
:model-value="form.contractName"
|
||||
readonly
|
||||
:disabled="!editable || details.length > 0"
|
||||
placeholder="请选择合同"
|
||||
@@ -53,6 +53,23 @@
|
||||
:disabled="!editable"
|
||||
@change="recalculateLocalAmount"
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="field.type === 'select' && editable"
|
||||
v-model="form[field.prop]"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in formSelectOptions(field)"
|
||||
:key="item.dictKey"
|
||||
:label="item.dictValue"
|
||||
:value="item.dictKey"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else-if="field.type === 'select'" class="form-readonly">
|
||||
{{ formSelectLabel(field, form[field.prop]) }}
|
||||
</span>
|
||||
<el-input
|
||||
v-else-if="field.type === 'textarea'"
|
||||
v-model="form[field.prop]"
|
||||
@@ -493,7 +510,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openAdjustChangeRecord(row)">查看</el-link>
|
||||
<el-link type="primary" @click="openAdjustChangeRecord(row)">详情</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -999,8 +1016,8 @@ import {
|
||||
settlementDetailColumns,
|
||||
settlementSummaryColumns,
|
||||
} from '@/option/settlement/preSettlementTable';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { isTierUnitPrice, isUnitPriceAdjustable } from '@/utils/settlement-adjust-rule';
|
||||
import { isApprovalSettlementChange } from '@/utils/settlement-change-record';
|
||||
import { normalizeCurrencyCode } from '@/utils/currency';
|
||||
import { settlementStatusName } from '@/utils/settlementStatus';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
@@ -1095,6 +1112,7 @@ export default {
|
||||
},
|
||||
feeOptions: [],
|
||||
feeCategoryOptions: [],
|
||||
businessModelOptions: [],
|
||||
transportTypeOptions: [],
|
||||
summaryFees: [],
|
||||
details: [],
|
||||
@@ -1144,6 +1162,8 @@ export default {
|
||||
readonly: false,
|
||||
detailId: '',
|
||||
sourceDetailId: '',
|
||||
// 未保存明细(id 仍等于来源应收应付明细)时先暂存,保存预结算单后再生效
|
||||
draftMode: false,
|
||||
detailLineNo: '',
|
||||
reason: '',
|
||||
},
|
||||
@@ -1312,9 +1332,12 @@ export default {
|
||||
if (value) this.initialize();
|
||||
},
|
||||
},
|
||||
summaryTotal(value) {
|
||||
this.form.settlementAmount = Number(value || 0).toFixed(2);
|
||||
this.recalculateLocalAmount();
|
||||
summaryTotal() {
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
'form.exchangeRate'() {
|
||||
if (this._skipExchangeRateRebuild) return;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
filteredDetails: {
|
||||
handler(list) {
|
||||
@@ -1330,7 +1353,6 @@ export default {
|
||||
methods: {
|
||||
isTierUnitPrice,
|
||||
isUnitPriceAdjustable,
|
||||
formatContractOptionLabel,
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission?.[code], false);
|
||||
},
|
||||
@@ -1344,6 +1366,7 @@ export default {
|
||||
if (!this.publicMode) {
|
||||
await this.loadFeeOptions();
|
||||
await this.loadFeeCategoryOptions();
|
||||
await this.loadBusinessModelOptions();
|
||||
await this.loadTransportTypeOptions();
|
||||
await this.loadAttachmentTypeOptions();
|
||||
}
|
||||
@@ -1485,6 +1508,7 @@ export default {
|
||||
this.detailFeeCache = {};
|
||||
this.adjustDialog.detailId = '';
|
||||
this.adjustDialog.sourceDetailId = '';
|
||||
this.adjustDialog.draftMode = false;
|
||||
this.adjustDialog.detailLineNo = '';
|
||||
this.adjustChangeRecordVisible = false;
|
||||
this.adjustChangeRecord = null;
|
||||
@@ -1523,7 +1547,7 @@ export default {
|
||||
createTime: row.paymentCreateTime || row.createTime,
|
||||
}));
|
||||
this.changeRecords = (detail.changeRecords || []).filter(
|
||||
row => row.changeContent !== '预结算审批通过'
|
||||
row => !isApprovalSettlementChange(row.changeContent)
|
||||
);
|
||||
this.attachments = this.parseAttachments(detail.attachmentsJson);
|
||||
this.sortAttachments();
|
||||
@@ -1647,6 +1671,14 @@ export default {
|
||||
const { data } = await getDictionary({ code: 'fee_category' });
|
||||
this.feeCategoryOptions = data?.data || [];
|
||||
},
|
||||
async loadBusinessModelOptions() {
|
||||
const { data } = await getDictionary({ code: 'business_model' });
|
||||
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
|
||||
this.businessModelOptions = records.map(item => ({
|
||||
dictKey: item.dictKey ?? item.value,
|
||||
dictValue: item.dictValue ?? item.label ?? item.name,
|
||||
}));
|
||||
},
|
||||
async loadTransportTypeOptions() {
|
||||
const { data } = await getDictionary({ code: 'transport_type' });
|
||||
const records = Array.isArray(data?.data) ? data.data : data?.data?.records || [];
|
||||
@@ -1655,6 +1687,21 @@ export default {
|
||||
dictValue: item.dictValue ?? item.label ?? item.name,
|
||||
}));
|
||||
},
|
||||
formSelectOptions(field) {
|
||||
const key = field?.optionsKey;
|
||||
const options = key ? this[key] : [];
|
||||
return Array.isArray(options) ? options : [];
|
||||
},
|
||||
formSelectLabel(field, value) {
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
const normalized = String(value).trim();
|
||||
const option = this.formSelectOptions(field).find(
|
||||
item =>
|
||||
String(item.dictKey ?? '').trim() === normalized ||
|
||||
String(item.dictValue ?? '').trim() === normalized
|
||||
);
|
||||
return option?.dictValue || normalized;
|
||||
},
|
||||
handleContractChange(id) {
|
||||
const contract = this.contractOptions.find(item => String(item.id) === String(id));
|
||||
if (!contract) {
|
||||
@@ -1741,6 +1788,8 @@ export default {
|
||||
return {
|
||||
id: this.form.id || undefined,
|
||||
contractId: this.form.contractId,
|
||||
businessModel: this.form.businessModel || undefined,
|
||||
transportType: this.form.transportType || undefined,
|
||||
exchangeRateDate: this.form.exchangeRateDate || undefined,
|
||||
exchangeRate: this.form.exchangeRate,
|
||||
attachmentsJson: JSON.stringify(this.attachments || []),
|
||||
@@ -1844,8 +1893,9 @@ export default {
|
||||
.map(row => [`${row.feeType}\u0000${row.feeItem}`, row])
|
||||
);
|
||||
const appendSummary = (feeItem, originalValue, settlementValue, fallbackFeeType = '') => {
|
||||
const originalAmount = Number(originalValue || 0);
|
||||
const settlementAmount = Number(settlementValue || 0);
|
||||
// 明细费用按本位币归集,外币结算时再按汇率换算成结算原币金额
|
||||
const originalAmount = this.convertLocalToSettlementAmount(originalValue);
|
||||
const settlementAmount = this.convertLocalToSettlementAmount(settlementValue);
|
||||
if (!feeItem || !Number.isFinite(originalAmount) || !Number.isFinite(settlementAmount)) {
|
||||
return;
|
||||
}
|
||||
@@ -1900,7 +1950,7 @@ export default {
|
||||
});
|
||||
nextFeeMap.forEach(row => orderedFees.push(row));
|
||||
this.summaryFees = orderedFees;
|
||||
this.recalculateLocalAmount();
|
||||
this.syncSettlementAmountsFromSummary();
|
||||
},
|
||||
// 每条明细对应的费用行:[调整前基准行, 调整后当前行],缺少来源费用快照时退化为明细自身的汇总行。
|
||||
resolveDetailFeePairs(detail) {
|
||||
@@ -1988,17 +2038,21 @@ export default {
|
||||
},
|
||||
handleExchangeRateDateChange() {
|
||||
this.fetchBfiExchangeRate();
|
||||
this.recalculateLocalAmount();
|
||||
},
|
||||
async fetchBfiExchangeRate() {
|
||||
if (!this.form.currency || this.form.currency === 'RMB') return;
|
||||
if (!this.form.currency || this.form.currency === 'RMB') {
|
||||
this.buildSummaryFeesFromDetails();
|
||||
return;
|
||||
}
|
||||
if (!this.form.exchangeRateDate) return;
|
||||
try {
|
||||
const { data } = await getBfiExchangeRate(this.form.currency, this.form.exchangeRateDate);
|
||||
const rateData = data?.data;
|
||||
if (rateData?.excval != null) {
|
||||
this._skipExchangeRateRebuild = true;
|
||||
this.form.exchangeRate = Number(rateData.excval);
|
||||
this.recalculateLocalAmount();
|
||||
this._skipExchangeRateRebuild = false;
|
||||
this.buildSummaryFeesFromDetails();
|
||||
} else {
|
||||
this.$message.warning(`未查询到 ${this.form.currency} 的BFI汇率数据`);
|
||||
}
|
||||
@@ -2006,9 +2060,39 @@ export default {
|
||||
this.$message.warning('查询BFI汇率失败,请手动输入结算汇率');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 明细金额按本位币理解;外币结算时结算合计原金额/结算金额 = 本位币金额 / 结算汇率。
|
||||
*/
|
||||
convertLocalToSettlementAmount(localAmount) {
|
||||
const amount = Number(localAmount || 0);
|
||||
if (!Number.isFinite(amount)) return 0;
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
return Number(amount.toFixed(2));
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
if (rate <= 0) return 0;
|
||||
return Number((amount / rate).toFixed(2));
|
||||
},
|
||||
/**
|
||||
* 结算金额取结算合计原币汇总值;本位币合计 = 结算金额 × 结算汇率。
|
||||
*/
|
||||
syncSettlementAmountsFromSummary() {
|
||||
const settlementTotal = this.summaryFees.reduce(
|
||||
(total, row) => total + Number(row.settlementAmount || 0),
|
||||
0
|
||||
);
|
||||
this.form.settlementAmount = Number(settlementTotal || 0).toFixed(2);
|
||||
if (this.isLocalCurrencySettlement) {
|
||||
this.form.localSettlementAmount = this.form.settlementAmount;
|
||||
this.form.exchangeRate = 1;
|
||||
return;
|
||||
}
|
||||
const rate = Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount =
|
||||
rate > 0 ? (Number(this.form.settlementAmount || 0) * rate).toFixed(2) : '0.00';
|
||||
},
|
||||
recalculateLocalAmount() {
|
||||
const rate = this.form.currency === 'RMB' ? 1 : Number(this.form.exchangeRate || 0);
|
||||
this.form.localSettlementAmount = (Number(this.form.settlementAmount || 0) * rate).toFixed(2);
|
||||
this.buildSummaryFeesFromDetails();
|
||||
},
|
||||
openCandidateDialog() {
|
||||
if (!this.form.contractId) {
|
||||
@@ -2251,17 +2335,21 @@ export default {
|
||||
},
|
||||
async openAdjustDialog(row, readonly) {
|
||||
const detailRow = row;
|
||||
const sourceDetailId = detailRow.sourceDetailId || '';
|
||||
// 未保存明细 id 仍等于来源应收应付明细 id,调整结果先暂存
|
||||
const draftMode =
|
||||
!this.form.id ||
|
||||
!detailRow.id ||
|
||||
String(detailRow.id) === String(detailRow.sourceDetailId);
|
||||
this.adjustDialog.visible = true;
|
||||
this.adjustDialog.loading = true;
|
||||
this.adjustDialog.readonly = readonly;
|
||||
this.adjustDialog.draftMode = draftMode;
|
||||
this.adjustDialog.detailId = detailRow.id;
|
||||
this.adjustDialog.detailLineNo = detailRow.lineNo;
|
||||
this.adjustDialog.sourceDetailId =
|
||||
detailRow.id && String(detailRow.id) === String(detailRow.sourceDetailId)
|
||||
? detailRow.sourceDetailId
|
||||
: '';
|
||||
const sourceDetailId = String(this.adjustDialog.sourceDetailId || '');
|
||||
const pending = sourceDetailId ? this.pendingAdjustments[sourceDetailId] : null;
|
||||
this.adjustDialog.sourceDetailId = sourceDetailId;
|
||||
const pendingKey = String(sourceDetailId || '');
|
||||
const pending = pendingKey ? this.pendingAdjustments[pendingKey] : null;
|
||||
// 底部备注回显结算明细备注;暂存过的调整优先取暂存备注
|
||||
this.adjustDialog.reason =
|
||||
pending?.reason !== undefined && pending?.reason !== null
|
||||
@@ -2278,11 +2366,12 @@ export default {
|
||||
this.adjustRows = cachedFees.map(item => this.normalizeAdjustRow(item));
|
||||
return;
|
||||
}
|
||||
if (sourceDetailId) {
|
||||
if (!this.sourceFeeSnapshots[sourceDetailId]) {
|
||||
this.sourceFeeSnapshots[sourceDetailId] = await this.loadSourceFeeRows(sourceDetailId);
|
||||
if (draftMode && sourceDetailId) {
|
||||
if (!this.sourceFeeSnapshots[String(sourceDetailId)]) {
|
||||
this.sourceFeeSnapshots[String(sourceDetailId)] =
|
||||
await this.loadSourceFeeRows(sourceDetailId);
|
||||
}
|
||||
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[sourceDetailId]);
|
||||
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[String(sourceDetailId)]);
|
||||
return;
|
||||
}
|
||||
const cachedFees = this.detailFeeCache[detailRow.id];
|
||||
@@ -2554,7 +2643,7 @@ export default {
|
||||
if (
|
||||
['transportQuantity', 'mileage', 'unitPrice'].includes(changedField) &&
|
||||
this.adjustDialog.sourceDetailId &&
|
||||
row.id &&
|
||||
(row.sourceFeeId || row.id) &&
|
||||
(changedField !== 'unitPrice' || isUnitPriceAdjustable(row))
|
||||
) {
|
||||
this.scheduleSourceAdjustCalculation(row, changedField);
|
||||
@@ -2570,7 +2659,8 @@ export default {
|
||||
try {
|
||||
const response = await calculateAdjustedFee({
|
||||
detailId: this.adjustDialog.sourceDetailId,
|
||||
feeId: row.id,
|
||||
// 已保存预结算费用行需用源应收应付费用ID试算
|
||||
feeId: row.sourceFeeId || row.id,
|
||||
transportQuantity: Number(row.transportQuantity || 0),
|
||||
mileage: Number(row.mileage || 0),
|
||||
unitPrice: row.unitPrice === null || row.unitPrice === '' ? null : Number(row.unitPrice),
|
||||
@@ -2596,6 +2686,7 @@ export default {
|
||||
row.settlementAmountTax = Number(data.afterAmount || 0);
|
||||
} catch (error) {
|
||||
row.calculateError = error.message || '费用试算失败';
|
||||
this.$message.warning(row.calculateError);
|
||||
} finally {
|
||||
if (version === row.adjustCalculateVersion) row.calculating = false;
|
||||
}
|
||||
@@ -2611,7 +2702,7 @@ export default {
|
||||
}
|
||||
this.adjustDialog.saving = true;
|
||||
try {
|
||||
if (this.adjustDialog.sourceDetailId) {
|
||||
if (this.adjustDialog.draftMode && this.adjustDialog.sourceDetailId) {
|
||||
this.pendingAdjustments[this.adjustDialog.sourceDetailId] = {
|
||||
reason: this.adjustDialog.reason,
|
||||
rows: this.adjustRows.map(row => ({ ...row, feeItems: { ...row.feeItems } })),
|
||||
@@ -2970,6 +3061,42 @@ export default {
|
||||
});
|
||||
},
|
||||
parseFeeItems(value) {
|
||||
const raw = this.parseRawFeeItems(value);
|
||||
const metaKeys = new Set([
|
||||
'currency',
|
||||
'totalFreightAmount',
|
||||
'otherFreightAmount',
|
||||
'freightItems',
|
||||
'freightTotal',
|
||||
'totalFreight',
|
||||
'totalAmount',
|
||||
'freight',
|
||||
'freightAmount',
|
||||
'transportFee',
|
||||
'otherFeeTotal',
|
||||
'otherAmount',
|
||||
]);
|
||||
const result = {};
|
||||
let otherFeeAmount = 0;
|
||||
Object.entries(raw).forEach(([key, amount]) => {
|
||||
if (!key) return;
|
||||
if (key === 'otherFreightAmount' || key === 'otherFeeTotal' || key === 'otherAmount') {
|
||||
otherFeeAmount += Number(amount || 0);
|
||||
return;
|
||||
}
|
||||
if (metaKeys.has(key)) return;
|
||||
if (amount && typeof amount === 'object') return;
|
||||
result[key] = amount;
|
||||
});
|
||||
const hasOtherFee = Object.keys(result).some(
|
||||
name => String(name).includes('其它费') || String(name).includes('其他费')
|
||||
);
|
||||
if (otherFeeAmount && !hasOtherFee) {
|
||||
result['其它费用'] = otherFeeAmount;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
parseRawFeeItems(value) {
|
||||
if (!value) return {};
|
||||
if (typeof value === 'object') return { ...value };
|
||||
try {
|
||||
|
||||
@@ -380,7 +380,13 @@ export default {
|
||||
},
|
||||
async loadFormalSettlements(keyword = '') {
|
||||
const { data } = await api.getFormalSettlements(keyword);
|
||||
this.candidates = data?.data || data || [];
|
||||
const payload = data?.data ?? data;
|
||||
// 兼容误返回分页对象的情况,只取正式结算单列表
|
||||
this.candidates = Array.isArray(payload)
|
||||
? payload
|
||||
: Array.isArray(payload?.records)
|
||||
? payload.records
|
||||
: [];
|
||||
},
|
||||
async loadFeeOptions() {
|
||||
const { data } = await api.getFeeOptions();
|
||||
|
||||
@@ -926,14 +926,34 @@ const stripInternalRowFields = row => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 合并费用列名与各行 feeItems 键,避免保存/回显时丢动态费用列 */
|
||||
const collectFeeItemNames = (preferred = [], detailRows = []) => {
|
||||
const names = [];
|
||||
const seen = new Set();
|
||||
const pushName = value => {
|
||||
const name = String(value || '').trim();
|
||||
if (!name || seen.has(name)) return;
|
||||
seen.add(name);
|
||||
names.push(name);
|
||||
};
|
||||
(preferred || []).forEach(pushName);
|
||||
(detailRows || []).forEach(row => {
|
||||
const feeItems = row?.feeItems;
|
||||
if (!feeItems || typeof feeItems !== 'object' || Array.isArray(feeItems)) return;
|
||||
Object.keys(feeItems).forEach(pushName);
|
||||
});
|
||||
return names;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const { file, ...batch } = form;
|
||||
const payloadRows = rows.value.map(stripInternalRowFields);
|
||||
return {
|
||||
...batch,
|
||||
settlementBillType: form.settlementType === 'payable' ? form.settlementBillType || 'formal' : 'formal',
|
||||
waybillMode: form.waybillMode === 'history' ? 'history' : 'new',
|
||||
feeItemNames: [...feeItemNames.value],
|
||||
rows: rows.value.map(stripInternalRowFields),
|
||||
feeItemNames: collectFeeItemNames(feeItemNames.value, payloadRows),
|
||||
rows: payloadRows,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1436,9 +1456,13 @@ const loadDetail = async id => {
|
||||
if (detail.projectId) {
|
||||
await loadContractsByProject(detail.projectId, detail.projectName || '');
|
||||
}
|
||||
feeItemNames.value = Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [];
|
||||
parsedHeaders.value = [];
|
||||
const detailRows = Array.isArray(detail.rows) ? detail.rows : [];
|
||||
// 先按库中列名与明细 feeItems 合并,再规范化行数据,保证草稿再打开仍展示全部费用列
|
||||
feeItemNames.value = collectFeeItemNames(
|
||||
Array.isArray(detail.feeItemNames) ? detail.feeItemNames : [],
|
||||
detailRows
|
||||
);
|
||||
rows.value = detailRows.map((item, index) => normalizeParsedRow(item, index));
|
||||
refreshDuplicateFlags();
|
||||
// 查看模式始终只读;编辑时仅草稿可改,已完成/已作废强制只读
|
||||
|
||||
@@ -125,15 +125,17 @@
|
||||
<span class="reconciliation-editor__filter-label">货物名称</span>
|
||||
<el-input v-model="internalQuery.cargoName" placeholder="请输入" clearable />
|
||||
</div>
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
<div class="reconciliation-editor__filter-actions">
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredInternalRows"
|
||||
@@ -189,6 +191,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -263,7 +268,7 @@
|
||||
<template v-else-if="isExternalRowEditing(row) && isExternalEditableColumn(column.prop)">
|
||||
<el-input-number
|
||||
v-if="column.feeItemName"
|
||||
v-model="row.feeItems[column.feeItemName]"
|
||||
v-model="externalEditTarget(row).feeItems[column.feeItemName]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
@@ -271,7 +276,7 @@
|
||||
/>
|
||||
<el-date-picker
|
||||
v-else-if="isExternalDateColumn(column.prop)"
|
||||
v-model="row[column.prop]"
|
||||
v-model="externalEditTarget(row)[column.prop]"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
format="YYYY-MM-DD"
|
||||
@@ -280,7 +285,7 @@
|
||||
/>
|
||||
<el-input-number
|
||||
v-else-if="isExternalNumberColumn(column.prop)"
|
||||
v-model="row[column.prop]"
|
||||
v-model="externalEditTarget(row)[column.prop]"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
@@ -288,7 +293,7 @@
|
||||
/>
|
||||
<el-select
|
||||
v-else-if="column.prop === 'transportType'"
|
||||
v-model="row[column.prop]"
|
||||
v-model="externalEditTarget(row)[column.prop]"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
@@ -300,7 +305,7 @@
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-else v-model="row[column.prop]" size="small" />
|
||||
<el-input v-else v-model="externalEditTarget(row)[column.prop]" size="small" />
|
||||
</template>
|
||||
<span v-else-if="column.prop === 'transportType'">
|
||||
{{ transportTypeLabel(row.transportType) }}
|
||||
@@ -318,6 +323,9 @@
|
||||
"
|
||||
>{{ formatDate(row[column.prop]) }}</span
|
||||
>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{
|
||||
formatOptionalMoney(row.unitPrice)
|
||||
}}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -476,6 +484,7 @@
|
||||
<span v-else-if="column.feeItemName">{{ formatMoney(getFeeItemAmount(row, column.feeItemName)) }}</span>
|
||||
<span v-else-if="column.prop === 'mileage'">{{ formatMileage(row.mileage) }}</span>
|
||||
<span v-else-if="column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime'">{{ formatDate(row[column.prop]) }}</span>
|
||||
<span v-else-if="column.prop === 'unitPrice'">{{ formatOptionalMoney(row.unitPrice) }}</span>
|
||||
<span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span>
|
||||
<span v-else>{{ displayValue(row[column.prop]) }}</span>
|
||||
</template>
|
||||
@@ -630,6 +639,10 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { h } from 'vue';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import {
|
||||
consumeImportResponse,
|
||||
resolveImportErrorMessage,
|
||||
} from '@/utils/import-excel';
|
||||
import { transportReconciliationFormFields } from '@/option/settlement/transportReconciliationForm';
|
||||
import {
|
||||
internalColumns,
|
||||
@@ -676,6 +689,8 @@ export default {
|
||||
completionDialog: { visible: false, loading: false },
|
||||
completionSummaryRowsData: null,
|
||||
externalEditing: {},
|
||||
// 调整中的草稿,输入只改草稿,点「完成」才写回列表
|
||||
externalEditDrafts: {},
|
||||
externalEditSnapshots: {},
|
||||
matchingStarted: false,
|
||||
rules: {
|
||||
@@ -932,6 +947,7 @@ export default {
|
||||
this.internalDetails = [];
|
||||
this.externalDetails = [];
|
||||
this.externalEditing = {};
|
||||
this.externalEditDrafts = {};
|
||||
this.externalEditSnapshots = {};
|
||||
this.completionSummaryRowsData = null;
|
||||
this.matchingStarted = false;
|
||||
@@ -1038,12 +1054,24 @@ export default {
|
||||
.map(normalized)
|
||||
.join('|');
|
||||
},
|
||||
blankOptionalNumber(value) {
|
||||
if (value === null || value === undefined || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeInternalRow(row) {
|
||||
const feeItems =
|
||||
row.feeItems && typeof row.feeItems === 'object' && Object.keys(row.feeItems).length
|
||||
? row.feeItems
|
||||
: this.parseFeeItems(row.feeItemsJson);
|
||||
return { ...row, feeItems };
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
};
|
||||
},
|
||||
normalizeExternalRow(row, index) {
|
||||
const feeItems =
|
||||
@@ -1053,6 +1081,9 @@ export default {
|
||||
return {
|
||||
...row,
|
||||
feeItems,
|
||||
unitPrice: this.blankOptionalNumber(row.unitPrice),
|
||||
mileage: this.blankOptionalNumber(row.mileage),
|
||||
freightAmount: this.blankOptionalNumber(row.freightAmount),
|
||||
_externalKey: row.id || row.externalLineNo || `external-${index}`,
|
||||
};
|
||||
},
|
||||
@@ -1280,6 +1311,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
return snapshot;
|
||||
});
|
||||
},
|
||||
@@ -1293,6 +1327,9 @@ export default {
|
||||
snapshot.feeItemsJson = row.feeItemsJson || JSON.stringify(row.feeItems || {});
|
||||
snapshot.actualDepartureTime = this.normalizeExternalDateTime(row.actualDepartureTime);
|
||||
snapshot.actualCompletionTime = this.normalizeExternalDateTime(row.actualCompletionTime);
|
||||
snapshot.unitPrice = this.blankOptionalNumber(row.unitPrice);
|
||||
snapshot.mileage = this.blankOptionalNumber(row.mileage);
|
||||
snapshot.freightAmount = this.blankOptionalNumber(row.freightAmount);
|
||||
snapshot.suspectedDuplicate = duplicateIds.has(String(row.id));
|
||||
return snapshot;
|
||||
});
|
||||
@@ -1322,7 +1359,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async handleUpdate() {
|
||||
await this.$confirm('将以外部账单数据更新运单及内部结算明细,是否继续?', '更新账单', {
|
||||
await this.$confirm('将以外部账单数据更新内部结算明细,是否继续?', '更新账单', {
|
||||
type: 'warning',
|
||||
});
|
||||
this.actionLoading = true;
|
||||
@@ -1503,23 +1540,24 @@ export default {
|
||||
this.form.reconciliationMode === 'cargo'
|
||||
? await api.importCargo(this.currentId, file)
|
||||
: await api.importVehicle(this.currentId, file);
|
||||
const contentType = response.headers?.['content-type'] || response.data?.type || '';
|
||||
if (contentType.includes('spreadsheetml') || contentType.includes('ms-excel')) {
|
||||
downloadXls(
|
||||
response.data,
|
||||
`运输对账导入失败明细${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`
|
||||
);
|
||||
// 成功时 axios 拦截器可能已把 JSON Blob 解析成对象,不能再调 .text()
|
||||
const consumed = await consumeImportResponse(response, {
|
||||
businessName: '运输对账',
|
||||
dayjsFormat: this.$dayjs().format('YYYY-MM-DD HH-mm-ss'),
|
||||
});
|
||||
if (consumed.kind === 'excel') {
|
||||
downloadXls(consumed.blob, consumed.fileName);
|
||||
this.$message.warning('部分数据导入失败,已下载失败明细');
|
||||
} else {
|
||||
const result = JSON.parse(await response.data.text());
|
||||
if (result.code !== 200) throw new Error(result.msg || '导入失败');
|
||||
this.$message.success('外部账单导入成功');
|
||||
}
|
||||
this.matchingStarted = false;
|
||||
await this.loadDetail();
|
||||
this.matchingStarted = false;
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '外部账单导入失败');
|
||||
if (!error?._notified) {
|
||||
this.$message.error(await resolveImportErrorMessage(error));
|
||||
}
|
||||
}
|
||||
},
|
||||
async downloadTemplate(mode) {
|
||||
@@ -1661,6 +1699,28 @@ export default {
|
||||
isExternalRowEditing(row) {
|
||||
return Boolean(this.externalEditing[this.externalRowKey(row)]);
|
||||
},
|
||||
cloneExternalEditRow(row) {
|
||||
return {
|
||||
...row,
|
||||
feeItems: { ...(row.feeItems || this.parseFeeItems(row.feeItemsJson)) },
|
||||
};
|
||||
},
|
||||
/** 编辑态绑定草稿,避免输入时实时改写列表导致疑似重复 Tab 刷新 */
|
||||
externalEditTarget(row) {
|
||||
const key = this.externalRowKey(row);
|
||||
return this.externalEditDrafts[key] || row;
|
||||
},
|
||||
clearExternalEditState(key) {
|
||||
const editing = { ...this.externalEditing };
|
||||
const drafts = { ...this.externalEditDrafts };
|
||||
const snapshots = { ...this.externalEditSnapshots };
|
||||
delete editing[key];
|
||||
delete drafts[key];
|
||||
delete snapshots[key];
|
||||
this.externalEditing = editing;
|
||||
this.externalEditDrafts = drafts;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
},
|
||||
isExternalEditableColumn(prop) {
|
||||
return !['externalLineNo', 'matchStatus'].includes(prop);
|
||||
},
|
||||
@@ -1672,34 +1732,30 @@ export default {
|
||||
},
|
||||
openExternalAdjust(row) {
|
||||
const key = this.externalRowKey(row);
|
||||
const draft = this.cloneExternalEditRow(row);
|
||||
this.externalEditSnapshots = {
|
||||
...this.externalEditSnapshots,
|
||||
[key]: { ...row },
|
||||
[key]: this.cloneExternalEditRow(row),
|
||||
};
|
||||
this.externalEditDrafts = { ...this.externalEditDrafts, [key]: draft };
|
||||
this.externalEditing = { ...this.externalEditing, [key]: true };
|
||||
},
|
||||
cancelExternalAdjust(row) {
|
||||
const key = this.externalRowKey(row);
|
||||
const snapshot = this.externalEditSnapshots[key];
|
||||
if (snapshot) Object.assign(row, snapshot);
|
||||
const editing = { ...this.externalEditing };
|
||||
const snapshots = { ...this.externalEditSnapshots };
|
||||
delete editing[key];
|
||||
delete snapshots[key];
|
||||
this.externalEditing = editing;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
this.clearExternalEditState(this.externalRowKey(row));
|
||||
},
|
||||
async finishExternalAdjust(row) {
|
||||
row.feeItemsJson = JSON.stringify(row.feeItems || {});
|
||||
const key = this.externalRowKey(row);
|
||||
const editing = { ...this.externalEditing };
|
||||
const snapshots = { ...this.externalEditSnapshots };
|
||||
delete editing[key];
|
||||
delete snapshots[key];
|
||||
this.externalEditing = editing;
|
||||
this.externalEditSnapshots = snapshots;
|
||||
const draft = this.externalEditDrafts[key];
|
||||
if (draft) {
|
||||
const feeItems = { ...(draft.feeItems || {}) };
|
||||
Object.assign(row, draft, { feeItems });
|
||||
row.feeItemsJson = JSON.stringify(feeItems);
|
||||
} else {
|
||||
row.feeItemsJson = JSON.stringify(row.feeItems || {});
|
||||
}
|
||||
this.clearExternalEditState(key);
|
||||
this.refreshLocalStats();
|
||||
this.$message.success('调整已暂存');
|
||||
this.$message.success('调整已暂存,匹配或完成对账后生效');
|
||||
},
|
||||
normalizeExternalDateTime(value) {
|
||||
if (!value) return null;
|
||||
@@ -1743,7 +1799,8 @@ export default {
|
||||
differenceQuantity: Math.abs(internalQuantity - externalQuantity),
|
||||
differenceAmount: Math.abs(internalAmount - externalAmount),
|
||||
matchedCount,
|
||||
unmatchedCount: internalUnmatched + externalUnmatched,
|
||||
// 无法匹配按内部账单条数统计,不叠加外部未匹配数
|
||||
unmatchedCount: internalUnmatched,
|
||||
matchStatus:
|
||||
this.internalDetails.length > 0 &&
|
||||
this.internalDetails.length === this.externalDetails.length &&
|
||||
@@ -1808,7 +1865,10 @@ export default {
|
||||
: Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatMileage(value) {
|
||||
return Number(value) === -1 ? '' : this.displayValue(value);
|
||||
return this.blankOptionalNumber(value) === null ? '' : this.displayValue(value);
|
||||
},
|
||||
formatOptionalMoney(value, currency = 'RMB') {
|
||||
return this.blankOptionalNumber(value) === null ? '-' : this.formatMoney(value, currency);
|
||||
},
|
||||
formatDate(value) {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
@@ -1929,15 +1989,26 @@ export default {
|
||||
}
|
||||
.reconciliation-editor__filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto auto;
|
||||
grid-template-columns: repeat(4, minmax(160px, 1fr)) auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.reconciliation-editor__filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reconciliation-editor__filter-actions .el-button {
|
||||
min-width: 64px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.reconciliation-editor__filter-label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div v-loading="true" class="formal-settlement-redirect" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/settlement/formalSettlement';
|
||||
import { settlementListPath } from '@/utils/settlement-menu';
|
||||
|
||||
/**
|
||||
* 菜单拆分为付款/收款结算单后,旧路径 /settlement/formal-settlement 已无菜单路由。
|
||||
* 本页按结算单号解析类型后 replace 到对应列表页,并保留 query(含 detailNo)。
|
||||
*/
|
||||
export default {
|
||||
name: 'FormalSettlementRedirect',
|
||||
async created() {
|
||||
const detailNo = String(this.$route.query.detailNo || '').trim();
|
||||
let settlementType = '';
|
||||
if (detailNo) {
|
||||
try {
|
||||
const response = await api.getList(1, 10, { formalSettlementNo: detailNo });
|
||||
const body = response?.data || response || {};
|
||||
const data = body?.data || body;
|
||||
const row = (data.records || []).find(item => item.formalSettlementNo === detailNo);
|
||||
settlementType = row?.settlementType || '';
|
||||
} catch (error) {
|
||||
settlementType = '';
|
||||
}
|
||||
}
|
||||
const path = settlementListPath(settlementType);
|
||||
await this.$router.replace({
|
||||
path,
|
||||
query: { ...this.$route.query },
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.formal-settlement-redirect {
|
||||
min-height: 240px;
|
||||
}
|
||||
</style>
|
||||
@@ -140,6 +140,7 @@ export default {
|
||||
rows: [],
|
||||
selection: [],
|
||||
loading: false,
|
||||
approveLoading: false,
|
||||
syncLoading: false,
|
||||
page: { currentPage: 1, pageSize: 10, pageSizes: [10, 20, 50, 100], total: 0 },
|
||||
editor: { visible: false, id: null, readonly: false },
|
||||
@@ -151,6 +152,8 @@ export default {
|
||||
form: { appliedAmount: 0, remark: '' },
|
||||
},
|
||||
mkEnabled: false,
|
||||
// keep-alive 多标签下仅激活实例响应 detailNo,避免缓存页重复弹窗
|
||||
isPageActive: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -221,11 +224,10 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// 标签机制按 fullPath 缓存同页多实例:仅当前路由实例应响应 detailNo 跳转,防止缓存旧实例也弹窗
|
||||
this.ownFullPath = this.$route.fullPath;
|
||||
this.loadOrganizationOptions();
|
||||
},
|
||||
mounted() {
|
||||
this.isPageActive = true;
|
||||
this.openRouteDetail();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
@@ -236,10 +238,18 @@ export default {
|
||||
});
|
||||
},
|
||||
activated() {
|
||||
this.isPageActive = true;
|
||||
if (sessionStorage.getItem('formal-settlement-list-refresh') === '1') {
|
||||
sessionStorage.removeItem('formal-settlement-list-refresh');
|
||||
this.onLoad(this.page);
|
||||
}
|
||||
// 从其他页带 detailNo 跳回时,激活态补开详情
|
||||
if (this.$route.query.detailNo) {
|
||||
this.openRouteDetail();
|
||||
}
|
||||
},
|
||||
deactivated() {
|
||||
this.isPageActive = false;
|
||||
},
|
||||
methods: {
|
||||
hasPermission(actionOrCode) {
|
||||
@@ -319,7 +329,7 @@ export default {
|
||||
},
|
||||
async openRouteDetail(detailNo = this.$route.query.detailNo) {
|
||||
const formalSettlementNo = String(detailNo || '').trim();
|
||||
if (!formalSettlementNo || this.ownFullPath !== this.$route.fullPath) return;
|
||||
if (!formalSettlementNo || !this.isPageActive) return;
|
||||
const response = await api.getList(1, 10, { formalSettlementNo });
|
||||
const data = this.unwrapData(response);
|
||||
const row = (data.records || []).find(item => item.formalSettlementNo === formalSettlementNo);
|
||||
@@ -327,6 +337,18 @@ export default {
|
||||
this.$message.warning(`未找到对应的${this.billLabel}`);
|
||||
return;
|
||||
}
|
||||
// 锁定菜单(付款/收款)下若单号属于另一类型,跳到对应列表页再打开
|
||||
if (
|
||||
this.lockedSettlementType &&
|
||||
row.settlementType &&
|
||||
row.settlementType !== this.lockedSettlementType
|
||||
) {
|
||||
await this.$router.replace({
|
||||
path: settlementListPath(row.settlementType),
|
||||
query: { detailNo: formalSettlementNo },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.lockedSettlementType) {
|
||||
this.activeSettlementType = row.settlementType || this.activeSettlementType;
|
||||
}
|
||||
@@ -371,15 +393,21 @@ export default {
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm(
|
||||
`审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
|
||||
if (this.approveLoading) return;
|
||||
this.approveLoading = true;
|
||||
let loading = null;
|
||||
try {
|
||||
await this.$confirm(
|
||||
`审核通过后${this.billLabel}生效,金额只能通过结算调整单处理,确认通过?`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '审批处理中,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
const res = await api.approve({ id: row.id });
|
||||
// data=false 表示审批已通过但自动同步金蝶失败,msg 带具体原因
|
||||
if (res.data.data === false) {
|
||||
@@ -387,11 +415,11 @@ export default {
|
||||
} else {
|
||||
this.$message.success(res.data.msg || '审批通过');
|
||||
}
|
||||
await this.loadTable();
|
||||
this.onLoad(this.page);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
loading?.close();
|
||||
this.approveLoading = false;
|
||||
}
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回', {
|
||||
@@ -479,6 +507,8 @@ export default {
|
||||
const rows = this.selection;
|
||||
if (rows.some(row => row.approvalStatus !== 'approved' || row.settlementType !== 'payable'))
|
||||
return this.$message.warning('仅审批通过的付款结算单允许发起付款申请');
|
||||
if (rows.some(row => row.adjustStatus === 'adjusted'))
|
||||
return this.$message.warning('已调整的结算单不可再发起付款,请使用调整后的新单');
|
||||
const contractIds = new Set(rows.map(row => String(row.contractId || '')));
|
||||
if (contractIds.size !== 1 || contractIds.has(''))
|
||||
return this.$message.warning('付款申请必须选择同一个合同的付款结算单');
|
||||
|
||||
@@ -334,6 +334,7 @@ export default {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
loading: false,
|
||||
approveLoading: false,
|
||||
query: emptyQuery(),
|
||||
searchFields: preSettlementSearchFields,
|
||||
searchExpanded: false,
|
||||
@@ -691,16 +692,23 @@ export default {
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该预付结算单?', '提示', { type: 'warning' });
|
||||
// 防止重复点击:请求期间表格遮罩,后续点击直接忽略
|
||||
if (this.loading) return;
|
||||
this.loading = true;
|
||||
// 防止重复点击:请求进行中忽略后续点击,避免重复审批和重复提示
|
||||
if (this.approveLoading) return;
|
||||
this.approveLoading = true;
|
||||
let loading = null;
|
||||
try {
|
||||
await this.$confirm('确认审批通过该预付结算单?', '提示', { type: 'warning' });
|
||||
loading = this.$loading({
|
||||
lock: true,
|
||||
text: '审批处理中,请稍候…',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
await approve({ id: row.id });
|
||||
this.$message.success('审批通过');
|
||||
await this.loadTable();
|
||||
this.loadTable();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
loading?.close();
|
||||
this.approveLoading = false;
|
||||
}
|
||||
},
|
||||
async handleReturn(row) {
|
||||
|
||||
@@ -1914,7 +1914,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (this.settlementType === 'payable' && column.prop === 'formalSettlementNo') {
|
||||
this.openSettlementDetail('/settlement/formal-settlement', row.formalSettlementNo);
|
||||
this.openSettlementDetail('/settlement/payment-settlement', row.formalSettlementNo);
|
||||
return;
|
||||
}
|
||||
this.openDetailDialog(row);
|
||||
|
||||
@@ -93,11 +93,15 @@
|
||||
><el-tag v-else-if="column.status" :type="statusType(row.approvalStatus)" class="status-text">{{
|
||||
row[column.prop] || '-'
|
||||
}}</el-tag
|
||||
><el-tag
|
||||
><el-tooltip
|
||||
v-else-if="column.kingdee"
|
||||
:disabled="row.kingdeeSyncStatus !== 'failed'"
|
||||
:content="row.kingdeeFailReason || '推送金蝶失败,请重试'"
|
||||
placement="top"
|
||||
><el-tag
|
||||
:type="kingdeeStatusType(row.kingdeeSyncStatus)"
|
||||
class="status-text"
|
||||
>{{ kingdeeSyncName(row.kingdeeSyncStatus) }}</el-tag
|
||||
></el-tooltip
|
||||
><span v-else-if="column.money">{{ formatMoney(row[column.prop]) }}</span
|
||||
><span v-else>{{ displayValue(row[column.prop]) }}</span></template
|
||||
></el-table-column
|
||||
@@ -182,9 +186,11 @@ import { settlementAdjustmentSearchFields } from '@/option/settlement/settlement
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
|
||||
import { kingdeeSyncStatusOptions } from '@/option/settlement/formalSettlementSearch';
|
||||
import { kingdeeStatusType } from '@/utils/kingdee';
|
||||
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
|
||||
import SettlementTableTools from './components/settlement-table-tools.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
import { settlementListPath } from '@/utils/settlement-menu';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
adjustmentNo: '',
|
||||
@@ -285,20 +291,23 @@ export default {
|
||||
openView(row) {
|
||||
this.editor = { visible: true, id: row.id, readonly: true };
|
||||
},
|
||||
// 单据号→本单查看弹窗;结算单号→跳正式结算列表页并自动打开该单详情(页面支持 ?detailNo=)
|
||||
// 单据号→本单查看弹窗;结算单号→跳付款/收款结算列表页并自动打开该单详情(页面支持 ?detailNo=)
|
||||
handleTableLink(row, column) {
|
||||
if (column.prop === 'formalSettlementNo') {
|
||||
this.openSettlementDetail(row.formalSettlementNo);
|
||||
this.openSettlementDetail(row.formalSettlementNo, row.settlementType);
|
||||
return;
|
||||
}
|
||||
this.openView(row);
|
||||
},
|
||||
openSettlementDetail(detailNo) {
|
||||
openSettlementDetail(detailNo, settlementType) {
|
||||
if (!detailNo) {
|
||||
this.$message.info('当前记录未关联结算单详情');
|
||||
return;
|
||||
}
|
||||
this.$router.push({ path: '/settlement/formal-settlement', query: { detailNo } });
|
||||
this.$router.push({
|
||||
path: settlementListPath(settlementType),
|
||||
query: { detailNo },
|
||||
});
|
||||
},
|
||||
isEditable(row) {
|
||||
return ['draft', 'returned'].includes(row.approvalStatus);
|
||||
@@ -365,9 +374,7 @@ export default {
|
||||
'info'
|
||||
);
|
||||
},
|
||||
kingdeeStatusType(status) {
|
||||
return { synced: 'success', failed: 'danger' }[status] || 'info';
|
||||
},
|
||||
kingdeeStatusType,
|
||||
kingdeeSyncName(value) {
|
||||
return kingdeeSyncStatusOptions.find(item => item.value === value)?.label || value || '-';
|
||||
},
|
||||
|
||||
@@ -6,15 +6,33 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, getCurrentInstance, nextTick, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import SettlementImportPage from './components/settlement-import-page.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const instance = getCurrentInstance();
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (route.query.mode === 'view') return '查看导入结算单';
|
||||
return route.query.id ? '编辑导入结算单' : '新增导入结算单';
|
||||
});
|
||||
|
||||
const syncTagTitle = () => {
|
||||
nextTick(() => {
|
||||
store.commit('SET_TAG', {
|
||||
fullPath: route.fullPath,
|
||||
name: pageTitle.value,
|
||||
});
|
||||
instance?.proxy?.$router?.$avueRouter?.setTitle?.(pageTitle.value);
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
syncTagTitle();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -239,19 +239,22 @@ export default {
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
openCreate() {
|
||||
this.$router.push({ path: '/settlement/settlement-import/form' });
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { name: '新增导入结算单' },
|
||||
});
|
||||
},
|
||||
openDetail(row) {
|
||||
if (!row?.id) return;
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id, mode: 'view' },
|
||||
query: { id: row.id, mode: 'view', name: '查看导入结算单' },
|
||||
});
|
||||
},
|
||||
editDraft(row) {
|
||||
this.$router.push({
|
||||
path: '/settlement/settlement-import/form',
|
||||
query: { id: row.id },
|
||||
query: { id: row.id, name: '编辑导入结算单' },
|
||||
});
|
||||
},
|
||||
async handleDelete(row) {
|
||||
|
||||
@@ -425,11 +425,20 @@ export default {
|
||||
minWidth: 140,
|
||||
order: 61,
|
||||
placeholder: '如 1',
|
||||
// -1 为空占位,列表展示为空
|
||||
formatter: row =>
|
||||
Number(row.contractInitialSerial) === -1 ? '' : row.contractInitialSerial,
|
||||
tip: '该部门当年首个合同时从此流水号起编,后续递增;范围1-999,不填默认从1开始',
|
||||
rules: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
// -1 为空占位,不填时按空处理
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1
|
||||
) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
@@ -526,6 +535,27 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '是否业务部门',
|
||||
prop: 'isBusinessDept',
|
||||
type: 'select',
|
||||
dicData: [
|
||||
{ label: '否', value: 0 },
|
||||
{ label: '是', value: 1 },
|
||||
],
|
||||
dataType: 'number',
|
||||
value: 0,
|
||||
width: 120,
|
||||
order: 34,
|
||||
formatter: row => (Number(row.isBusinessDept) === 1 ? '是' : '否'),
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择是否业务部门',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '排序',
|
||||
prop: 'sort',
|
||||
@@ -1188,7 +1218,13 @@ export default {
|
||||
}
|
||||
},
|
||||
normalizeContractInitialSerial(value) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
// -1 为空占位,不填时不落库、不回显
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const text = String(value).trim();
|
||||
@@ -1198,6 +1234,12 @@ export default {
|
||||
const serial = Number(text);
|
||||
return Number.isInteger(serial) ? serial : value;
|
||||
},
|
||||
clearEmptyContractInitialSerial(form = this.form) {
|
||||
if (!form) return;
|
||||
if (Number(form.contractInitialSerial) === -1) {
|
||||
form.contractInitialSerial = '';
|
||||
}
|
||||
},
|
||||
isRootDept(row) {
|
||||
return row && (row.parentId === 0 || String(row.parentId) === '0');
|
||||
},
|
||||
@@ -1502,6 +1544,10 @@ export default {
|
||||
if (this.form.isPlatformCompany === undefined || this.form.isPlatformCompany === null) {
|
||||
this.form.isPlatformCompany = 0;
|
||||
}
|
||||
if (this.form.isBusinessDept === undefined || this.form.isBusinessDept === null) {
|
||||
this.form.isBusinessDept = 0;
|
||||
}
|
||||
this.clearEmptyContractInitialSerial(this.form);
|
||||
this.initData(this.form.tenantId);
|
||||
this.loadParentDeptCode(this.form.parentId);
|
||||
}
|
||||
@@ -1516,6 +1562,7 @@ export default {
|
||||
if (this.form.parentId === '0') {
|
||||
this.form.parentId = '';
|
||||
}
|
||||
this.clearEmptyContractInitialSerial(this.form);
|
||||
this.loadParentDeptCode(this.form.parentId);
|
||||
if (this.form.hasOwnProperty('leaderId')) {
|
||||
const splitResult = func.split(this.form.leaderId);
|
||||
|
||||
+174
-74
@@ -20,33 +20,29 @@
|
||||
@on-load="onLoad"
|
||||
>
|
||||
<template #menu-left>
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="permission.mp_user_add"
|
||||
@click="$refs.crud.rowAdd()"
|
||||
>
|
||||
<el-button type="primary" v-if="permission.mp_user_add" @click="$refs.crud.rowAdd()">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
v-if="permission.mp_user_reset"
|
||||
@click="handleReset"
|
||||
>
|
||||
<el-button type="primary" plain v-if="permission.mp_user_reset" @click="handleReset">
|
||||
密码重置
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
v-if="permission.mp_user_delete"
|
||||
@click="handleDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</template>
|
||||
<template #identities="{ row }">
|
||||
{{ row.identitiesName || formatIdentities(row.identities) }}
|
||||
</template>
|
||||
<template #deptName="{ row }">
|
||||
<template v-if="formatDeptNames(row.deptName).length">
|
||||
<el-tag
|
||||
v-for="name in formatDeptNames(row.deptName)"
|
||||
:key="name"
|
||||
size="small"
|
||||
class="dept-tag"
|
||||
>
|
||||
{{ name }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||
{{ row.status === 1 ? '启用' : '停用' }}
|
||||
@@ -55,6 +51,33 @@
|
||||
<template #updateUserName="{ row }">
|
||||
{{ formatUpdateUserName(row) }}
|
||||
</template>
|
||||
<template #deptId-form>
|
||||
<el-cascader
|
||||
v-model="deptCascaderValue"
|
||||
:options="permissionDeptTree"
|
||||
:props="deptCascaderProps"
|
||||
:disabled="isViewMode"
|
||||
clearable
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="请选择所属组织(可多选)"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</template>
|
||||
<template #dataScopeDeptIds-form>
|
||||
<el-tree
|
||||
v-if="Number(form.dataScopeRange) === 3"
|
||||
ref="dataScopeTree"
|
||||
:data="permissionDeptTree"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="form.dataScopeDeptIds || []"
|
||||
:props="{ label: 'title', children: 'children' }"
|
||||
@check="handleDataScopeCheck"
|
||||
/>
|
||||
<span v-else class="form-tip">选择「自定义」后可勾选数据权限组织</span>
|
||||
</template>
|
||||
<template #menu="{ row, index }">
|
||||
<el-link
|
||||
type="primary"
|
||||
@@ -70,27 +93,12 @@
|
||||
>
|
||||
编辑
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_status"
|
||||
@click="handleStatus(row)"
|
||||
>
|
||||
<el-link type="primary" v-if="permission.mp_user_status" @click="handleStatus(row)">
|
||||
{{ row.status === 1 ? '停用' : '启用' }}
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="permission.mp_user_reset"
|
||||
@click="handleResetForRow(row)"
|
||||
>
|
||||
<el-link type="primary" v-if="permission.mp_user_reset" @click="handleResetForRow(row)">
|
||||
重置密码
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
v-if="permission.mp_user_delete"
|
||||
@click="rowDel(row)"
|
||||
>
|
||||
删除
|
||||
</el-link>
|
||||
</template>
|
||||
<template #menu-form="{ size }">
|
||||
<el-button
|
||||
@@ -107,10 +115,13 @@
|
||||
|
||||
<script>
|
||||
import { h } from 'vue';
|
||||
import { getList, getDetail, submit, remove, changeStatus, resetPassword } from '@/api/system/mpUser';
|
||||
import { getList, getDetail, submit, changeStatus, resetPassword } from '@/api/system/mpUser';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import option from '@/option/system/mpUser';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { sensitive } from '@/utils/sensitive';
|
||||
import func from '@/utils/func';
|
||||
|
||||
const IDENTITY_LABEL = {
|
||||
driver: '司机',
|
||||
@@ -132,21 +143,62 @@ export default {
|
||||
selectionList: [],
|
||||
option,
|
||||
data: [],
|
||||
permissionDeptTree: [],
|
||||
sensitiveManager: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission']),
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.mp_user_add, false),
|
||||
viewBtn: this.validData(this.permission.mp_user_view, false),
|
||||
delBtn: this.validData(this.permission.mp_user_delete, false),
|
||||
editBtn: this.validData(this.permission.mp_user_edit, false),
|
||||
};
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
isViewMode() {
|
||||
return this.dialogType === 'view';
|
||||
},
|
||||
deptCascaderProps() {
|
||||
return {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
emitPath: false,
|
||||
multiple: true,
|
||||
checkStrictly: true,
|
||||
expandTrigger: 'click',
|
||||
disabled: 'disabled',
|
||||
};
|
||||
},
|
||||
deptCascaderValue: {
|
||||
get() {
|
||||
const ids = Array.isArray(this.form.deptId)
|
||||
? this.form.deptId
|
||||
: this.form.deptId
|
||||
? String(this.form.deptId).split(',').filter(Boolean)
|
||||
: [];
|
||||
return ids
|
||||
.filter(item => item !== undefined && item !== null && item !== '')
|
||||
.map(item => (typeof item === 'number' ? item : String(item)));
|
||||
},
|
||||
set(value) {
|
||||
this.form.deptId = Array.isArray(value)
|
||||
? value.filter(item => item !== undefined && item !== null && item !== '')
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
},
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.sensitiveManager = sensitive.create({
|
||||
fields: ['phone', 'email'],
|
||||
});
|
||||
this.loadDeptTree();
|
||||
},
|
||||
methods: {
|
||||
formatUpdateUserName,
|
||||
@@ -158,19 +210,59 @@ export default {
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
},
|
||||
normalizeSubmitRow(row = {}) {
|
||||
const payload = { ...row };
|
||||
formatDeptNames(deptName) {
|
||||
if (!deptName) {
|
||||
return [];
|
||||
}
|
||||
return String(deptName)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
loadDeptTree() {
|
||||
const tenantId = this.userInfo?.tenantId;
|
||||
if (!tenantId) {
|
||||
return;
|
||||
}
|
||||
getDeptTree(tenantId).then(res => {
|
||||
this.permissionDeptTree = res.data.data || [];
|
||||
});
|
||||
},
|
||||
handleDataScopeCheck(_, checked) {
|
||||
if (this.isViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.dataScopeDeptIds = checked.checkedKeys || [];
|
||||
},
|
||||
normalizeSubmitRow(row = {}, isUpdate = false) {
|
||||
let payload = { ...row };
|
||||
// 编辑时剔除未改动的脱敏字段,避免把 138****8000 写回库
|
||||
if (isUpdate && this.sensitiveManager) {
|
||||
payload = this.sensitiveManager.getSubmitData(payload);
|
||||
}
|
||||
if (Array.isArray(payload.identities)) {
|
||||
payload.identities = payload.identities.filter(Boolean).join(',');
|
||||
}
|
||||
payload.deptId = func.join(payload.deptId);
|
||||
if (payload.phone) {
|
||||
payload.phone = String(payload.phone).trim();
|
||||
}
|
||||
if (!payload.account && payload.phone) {
|
||||
// 脱敏手机号不能作为账号默认值
|
||||
if (!payload.account && payload.phone && !String(payload.phone).includes('*')) {
|
||||
payload.account = payload.phone;
|
||||
}
|
||||
payload.dataScopeRange = Number(payload.dataScopeRange || 1);
|
||||
payload.dataLevelRange = Number(payload.dataLevelRange || 1);
|
||||
if (payload.dataScopeRange === 3) {
|
||||
payload.dataScopeDeptIds = Array.isArray(payload.dataScopeDeptIds)
|
||||
? payload.dataScopeDeptIds
|
||||
: [];
|
||||
} else {
|
||||
payload.dataScopeDeptIds = [];
|
||||
}
|
||||
delete payload.password;
|
||||
delete payload.identitiesName;
|
||||
delete payload.deptName;
|
||||
delete payload.sexName;
|
||||
delete payload.createUserName;
|
||||
delete payload.updateUserName;
|
||||
@@ -178,13 +270,25 @@ export default {
|
||||
},
|
||||
beforeOpen(done, type) {
|
||||
this.dialogType = type;
|
||||
this.loadDeptTree();
|
||||
if (['edit', 'view'].includes(type) && this.form.id) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
const detail = res.data.data || {};
|
||||
if (detail.identities && typeof detail.identities === 'string') {
|
||||
detail.identities = detail.identities.split(',').filter(Boolean);
|
||||
}
|
||||
this.form = detail;
|
||||
const deptIds = func.split(detail.deptId || '');
|
||||
this.form = {
|
||||
...detail,
|
||||
deptId: Array.isArray(deptIds) ? deptIds : [],
|
||||
dataScopeRange: detail.dataScopeRange || 1,
|
||||
dataLevelRange: detail.dataLevelRange || 1,
|
||||
dataScopeDeptIds: detail.dataScopeDeptIds || [],
|
||||
};
|
||||
this.sensitiveManager?.saveInitialData(this.form);
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dataScopeTree?.setCheckedKeys?.(this.form.dataScopeDeptIds || []);
|
||||
});
|
||||
done();
|
||||
});
|
||||
return;
|
||||
@@ -193,12 +297,23 @@ export default {
|
||||
this.form = {
|
||||
status: 1,
|
||||
identities: ['driver'],
|
||||
deptId: [],
|
||||
dataScopeRange: 1,
|
||||
dataLevelRange: 1,
|
||||
dataScopeDeptIds: [],
|
||||
};
|
||||
this.sensitiveManager?.saveInitialData({});
|
||||
}
|
||||
done();
|
||||
},
|
||||
rowSave(row, done, loading) {
|
||||
submit(this.normalizeSubmitRow(row)).then(
|
||||
const payload = this.normalizeSubmitRow(row, false);
|
||||
if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) {
|
||||
this.$message.warning('请选择自定义数据权限组织');
|
||||
loading();
|
||||
return;
|
||||
}
|
||||
submit(payload).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
@@ -208,7 +323,13 @@ export default {
|
||||
);
|
||||
},
|
||||
rowUpdate(row, index, done, loading) {
|
||||
submit(this.normalizeSubmitRow(row)).then(
|
||||
const payload = this.normalizeSubmitRow(row, true);
|
||||
if (payload.dataScopeRange === 3 && (!payload.dataScopeDeptIds || !payload.dataScopeDeptIds.length)) {
|
||||
this.$message.warning('请选择自定义数据权限组织');
|
||||
loading();
|
||||
return;
|
||||
}
|
||||
submit(payload).then(
|
||||
() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
@@ -217,35 +338,6 @@ export default {
|
||||
() => loading()
|
||||
);
|
||||
},
|
||||
rowDel(row) {
|
||||
this.$confirm('确定删除该小程序用户?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => remove(row.id))
|
||||
.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
});
|
||||
},
|
||||
handleDelete() {
|
||||
if (this.selectionList.length === 0) {
|
||||
this.$message.warning('请选择至少一条数据');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确定删除选中的小程序用户?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => remove(this.ids))
|
||||
.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message.success('操作成功');
|
||||
this.$refs.crud.toggleSelection();
|
||||
});
|
||||
},
|
||||
handleStatus(row) {
|
||||
const status = row.status === 1 ? 0 : 1;
|
||||
const text = status === 1 ? '启用' : '停用';
|
||||
@@ -351,5 +443,13 @@ export default {
|
||||
:deep(.avue-crud__menu) {
|
||||
min-height: auto;
|
||||
}
|
||||
.dept-tag {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.form-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+105
-45
@@ -115,9 +115,10 @@
|
||||
<template #deptName="{ row }">
|
||||
<template v-if="formatDeptNames(row.deptName).length">
|
||||
<el-tag
|
||||
v-for="name in formatDeptNames(row.deptName)"
|
||||
:key="name"
|
||||
v-for="(name, index) in formatDeptNames(row.deptName)"
|
||||
:key="`${row.id}-${index}-${name}`"
|
||||
class="user-dept-tag"
|
||||
@click.stop="handleDeptTagClick(row, index)"
|
||||
>{{ name }}</el-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
@@ -174,8 +175,6 @@
|
||||
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" :disabled="isUserViewMode" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="OA登录名"><el-input v-model="form.oaLoginId" :disabled="isUserViewMode" maxlength="128" placeholder="请输入OA登录名" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory" :disabled="isUserViewMode"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange" :disabled="isUserViewMode"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange" :disabled="isUserViewMode"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
@@ -201,13 +200,6 @@
|
||||
<el-table-column v-if="!isUserViewMode" label="操作" width="90"><template #default="{ row }"><el-link type="danger" @click="removeFormRole(row.id)">删除</el-link></template></el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="form.dataScopeRange === 3" label="数据权限">
|
||||
<el-tree :data="permissionDeptTree" show-checkbox node-key="id" :default-checked-keys="form.dataScopeDeptIds || []" :props="{ label: 'title', children: 'children' }" @check="handleDataScopeCheck" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="客商权限">
|
||||
<div class="permission-toolbar customer-toolbar"><el-checkbox v-model="form.includeNewCustomer" :true-value="1" :false-value="0" :disabled="isUserViewMode">包含新增的客商</el-checkbox></div>
|
||||
<el-table ref="customerTable" :data="customerList" border @selection-change="handleCustomerSelection"><el-table-column type="selection" width="55" :selectable="isCustomerSelectable" /><el-table-column prop="customerCode" label="客商编码" /><el-table-column prop="fullName" label="客商名称" /></el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<span v-if="isUserViewMode" class="dialog-footer">
|
||||
@@ -406,7 +398,6 @@ import {
|
||||
auditRefuse,
|
||||
setLeader,
|
||||
getLeaderList,
|
||||
getCustomerOptions,
|
||||
} from '@/api/system/user';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
@@ -476,7 +467,6 @@ export default {
|
||||
treeData: [],
|
||||
permissionDeptTree: [],
|
||||
postList: [],
|
||||
customerList: [],
|
||||
treeOption: treeOption,
|
||||
userOption: userOption(this),
|
||||
data: [],
|
||||
@@ -666,6 +656,104 @@ export default {
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
formatDeptIds(deptId) {
|
||||
if (!deptId && deptId !== 0) {
|
||||
return [];
|
||||
}
|
||||
return String(deptId)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
handleDeptTagClick(row, index) {
|
||||
const deptIds = this.formatDeptIds(row.deptId);
|
||||
let deptId = deptIds[index];
|
||||
if (!deptId) {
|
||||
const names = this.formatDeptNames(row.deptName);
|
||||
deptId = this.findDeptIdByName(names[index]);
|
||||
}
|
||||
if (!deptId) {
|
||||
this.$message.warning('未找到对应组织');
|
||||
return;
|
||||
}
|
||||
this.selectDeptOnTree(deptId);
|
||||
},
|
||||
findDeptIdByName(name, tree = this.permissionDeptTree) {
|
||||
if (!name) return null;
|
||||
for (const node of tree || []) {
|
||||
if (node.title === name || node.deptName === name || node.label === name) {
|
||||
return node.id;
|
||||
}
|
||||
const matched = this.findDeptIdByName(name, node.children);
|
||||
if (matched != null) return matched;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* 展开左侧懒加载组织树到目标节点并设为选中,同时按该组织筛选用户列表
|
||||
*/
|
||||
async selectDeptOnTree(deptId) {
|
||||
if (deptId == null || deptId === '') return;
|
||||
if (!this.permissionDeptTree?.length) {
|
||||
const tenantId = this.userInfo.tenantId || this.website.tenantId;
|
||||
try {
|
||||
const res = await getDeptTree(tenantId);
|
||||
this.permissionDeptTree = res.data.data || [];
|
||||
} catch (error) {
|
||||
this.$message.error('组织树加载失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const path = this.findDeptPath(deptId);
|
||||
if (!path?.length) {
|
||||
this.$message.warning('未在组织树中找到该部门');
|
||||
return;
|
||||
}
|
||||
const tree = this.$refs.deptTree;
|
||||
if (!tree) return;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
await this.expandLazyTreeNode(path[i]);
|
||||
}
|
||||
await this.$nextTick();
|
||||
const targetId = path[path.length - 1];
|
||||
tree.setCurrentKey?.(targetId);
|
||||
this.$nextTick(() => {
|
||||
const currentEl = tree.$el?.querySelector?.('.el-tree-node.is-current');
|
||||
currentEl?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
|
||||
});
|
||||
if (String(this.treeDeptId) === String(targetId)) {
|
||||
return;
|
||||
}
|
||||
this.treeDeptId = targetId;
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
},
|
||||
expandLazyTreeNode(key) {
|
||||
return new Promise(resolve => {
|
||||
const tree = this.$refs.deptTree;
|
||||
if (!tree?.getNode) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const tryExpand = (retries = 40) => {
|
||||
const node = tree.getNode(key);
|
||||
if (!node) {
|
||||
if (retries <= 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
setTimeout(() => tryExpand(retries - 1), 50);
|
||||
return;
|
||||
}
|
||||
if (node.expanded || node.isLeaf) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
node.expand(() => resolve());
|
||||
};
|
||||
tryExpand();
|
||||
});
|
||||
},
|
||||
handleDropdownCommand(command, row, index) {
|
||||
if (command === 'setLeader') {
|
||||
this.handleSetLeader(row);
|
||||
@@ -853,9 +941,6 @@ export default {
|
||||
const column = this.findColumn(this.userOption.group, 'leaderId');
|
||||
column.dicData = res.data.data;
|
||||
});
|
||||
getCustomerOptions().then(res => {
|
||||
this.customerList = res.data.data || [];
|
||||
});
|
||||
},
|
||||
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
||||
findDeptPath(id, tree = this.permissionDeptTree, parents = []) {
|
||||
@@ -901,21 +986,6 @@ export default {
|
||||
.split(this.form.roleId || '')
|
||||
.filter(id => String(id) !== String(roleId));
|
||||
},
|
||||
handleDataScopeCheck(_, checked) {
|
||||
if (this.isUserViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.dataScopeDeptIds = checked.checkedKeys;
|
||||
},
|
||||
handleCustomerSelection(rows) {
|
||||
if (this.isUserViewMode) {
|
||||
return;
|
||||
}
|
||||
this.form.customerIds = rows.map(row => row.id);
|
||||
},
|
||||
isCustomerSelectable() {
|
||||
return !this.isUserViewMode;
|
||||
},
|
||||
normalizeSexDisplay(sex) {
|
||||
if (sex === -1 || sex === '-1' || sex === '' || sex === null || sex === undefined) {
|
||||
return undefined;
|
||||
@@ -966,13 +1036,8 @@ export default {
|
||||
deptId: [],
|
||||
code: '',
|
||||
personCategory: 1,
|
||||
dataScopeRange: 1,
|
||||
dataLevelRange: 1,
|
||||
remark: '',
|
||||
roleId: '',
|
||||
dataScopeDeptIds: [],
|
||||
customerIds: [],
|
||||
includeNewCustomer: 0,
|
||||
};
|
||||
this.userDialog = true;
|
||||
this.$nextTick(() => this.$refs.userFormRef && this.$refs.userFormRef.clearValidate());
|
||||
@@ -986,22 +1051,13 @@ export default {
|
||||
deptId: Array.isArray(deptIds) ? deptIds : [],
|
||||
postId: func.split(user.postId || '')[0] || '',
|
||||
roleId: func.join(user.roleId || ''),
|
||||
dataScopeDeptIds: user.dataScopeDeptIds || [],
|
||||
customerIds: user.customerIds || [],
|
||||
includeNewCustomer: user.includeNewCustomer || 0,
|
||||
personCategory: user.personCategory || 1,
|
||||
dataScopeRange: user.dataScopeRange || 1,
|
||||
dataLevelRange: user.dataLevelRange || 1,
|
||||
sex: this.normalizeSexDisplay(user.sex),
|
||||
};
|
||||
this.sensitiveManager.saveInitialData(this.form);
|
||||
this.userDialog = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.userFormRef && this.$refs.userFormRef.clearValidate();
|
||||
(this.form.customerIds || []).forEach(id => {
|
||||
const customer = this.customerList.find(item => String(item.id) === String(id));
|
||||
if (customer) this.$refs.customerTable && this.$refs.customerTable.toggleRowSelection(customer, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -1537,6 +1593,10 @@ export default {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.user-dept-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-dept-tag + .user-dept-tag {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,11 @@
|
||||
<span>{{ row.idCardNo || '-' }}</span>
|
||||
</template>
|
||||
<template #organizationName="{ row }">
|
||||
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
|
||||
<span
|
||||
v-if="isBlankOrganizationName(row.organizationName)"
|
||||
class="organization-name--empty"
|
||||
>未分配组织</span>
|
||||
<span v-else>{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}</span>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||
@@ -143,7 +147,7 @@
|
||||
maxlength="18"
|
||||
show-word-limit
|
||||
placeholder="请输入"
|
||||
@input="handleIdCardInput"
|
||||
@blur="handleIdCardBlur"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -153,7 +157,7 @@
|
||||
v-model="driverForm.birthday"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择"
|
||||
placeholder="请选择出生年月"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -201,12 +205,16 @@
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="住址" prop="addressRegion">
|
||||
<!-- 只读直接显示库里那串区划名称:级联要先按名称反解 path,区划改名后反解不出来,
|
||||
同一串文字就会在灰字 placeholder 与黑字标签之间摇摆 -->
|
||||
<el-input v-if="readonly" :model-value="driverForm.addressRegion || '—'" readonly />
|
||||
<el-cascader
|
||||
v-else
|
||||
ref="addressRegionCascader"
|
||||
v-model="driverForm.addressRegionPath"
|
||||
:options="regionOptions"
|
||||
:props="regionCascaderProps"
|
||||
:placeholder="driverForm.addressRegion || '请选择'"
|
||||
:placeholder="driverForm.addressRegion || '请选择省/市/区'"
|
||||
clearable
|
||||
filterable
|
||||
@change="handleAddressRegionChange"
|
||||
@@ -219,7 +227,7 @@
|
||||
v-model="driverForm.address"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入"
|
||||
placeholder="请输入详细地址(街道、门牌号)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -295,6 +303,42 @@
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="18">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="身份证有效期" prop="idCardValidEnd">
|
||||
<div class="date-range">
|
||||
<el-date-picker
|
||||
v-model="driverForm.idCardValidStart"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="起"
|
||||
:disabled="driverForm.idCardLongTerm === 1"
|
||||
@change="validateFormField('idCardValidEnd')"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="driverForm.idCardValidEnd"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="止"
|
||||
:disabled="driverForm.idCardLongTerm === 1"
|
||||
@change="validateFormField('idCardValidEnd')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="长期有效" prop="idCardLongTerm">
|
||||
<el-checkbox
|
||||
v-model="driverForm.idCardLongTerm"
|
||||
:true-label="1"
|
||||
:false-label="0"
|
||||
@change="handleIdCardLongTermChange"
|
||||
>
|
||||
是
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section-card>
|
||||
|
||||
<section-card title="驾驶证信息">
|
||||
@@ -592,7 +636,11 @@
|
||||
<span>{{ row.plateNo || '-' }}</span>
|
||||
</template>
|
||||
<template #organizationName="{ row }">
|
||||
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
|
||||
<span
|
||||
v-if="isBlankOrganizationName(row.organizationName)"
|
||||
class="organization-name--empty"
|
||||
>未分配组织</span>
|
||||
<span v-else>{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}</span>
|
||||
</template>
|
||||
<template #boundDriver="{ row }">
|
||||
<span>{{ row.boundDriver || '-' }}</span>
|
||||
@@ -689,6 +737,9 @@ const emptyForm = () => ({
|
||||
posts: '',
|
||||
idCardFront: '',
|
||||
idCardBack: '',
|
||||
idCardValidStart: '',
|
||||
idCardValidEnd: '',
|
||||
idCardLongTerm: 0,
|
||||
headPhoto: '',
|
||||
drivingType: '',
|
||||
drivingLicenseNo: '',
|
||||
@@ -743,6 +794,8 @@ export default {
|
||||
expired: 0,
|
||||
},
|
||||
driverBox: false,
|
||||
// 搜索栏组织级联追加「未分配组织」一项(哨兵值见 organization-search mixin)
|
||||
enableUnassignedOrgFilter: true,
|
||||
vehicleSelectorVisible: false,
|
||||
vehicleSelectorLoading: false,
|
||||
vehicleSelectorOption,
|
||||
@@ -857,7 +910,7 @@ export default {
|
||||
emergencyContactMobile: [
|
||||
{ required: true, message: '请输入紧急联系人手机号', trigger: 'blur' },
|
||||
],
|
||||
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
|
||||
// 后端已允许所属组织为空,这里不做必填校验
|
||||
idCardFront: [{ required: true, message: '请上传身份证正面照', trigger: 'change' }],
|
||||
idCardBack: [{ required: true, message: '请上传身份证反面照', trigger: 'change' }],
|
||||
drivingLicenseFront: [{ required: true, message: '请上传驾驶证主页', trigger: 'change' }],
|
||||
@@ -1122,19 +1175,21 @@ export default {
|
||||
removeDigits(value = '') {
|
||||
return String(value).replace(/\d/g, '');
|
||||
},
|
||||
handleIdCardInput(value) {
|
||||
const idCardNo = String(value || '').toUpperCase();
|
||||
/**
|
||||
* 身份证号失焦后补出生年月与性别,与小程序 person.vue 的 onIdCardNoChange 同口径:
|
||||
* 只在字段还空着时补——用户手改过的值不被他随后修改身份证号的动作冲掉。
|
||||
* 不回写空值:18 位不合法时清空生日会把已选日期当垃圾丢掉。
|
||||
* OCR 识别结果仍直接覆盖(见 applyIdCardRecognition),那是「重新采一次证件」,与手输联动不是一回事。
|
||||
*/
|
||||
handleIdCardBlur() {
|
||||
const idCardNo = String(this.driverForm.idCardNo || '').toUpperCase();
|
||||
this.driverForm.idCardNo = idCardNo;
|
||||
const idCardInfo = this.parseIdCardInfo(idCardNo);
|
||||
if (idCardInfo.birthday) {
|
||||
if (!this.driverForm.birthday && idCardInfo.birthday) {
|
||||
this.driverForm.birthday = idCardInfo.birthday;
|
||||
} else {
|
||||
this.driverForm.birthday = '';
|
||||
}
|
||||
if (idCardInfo.gender) {
|
||||
if (!this.driverForm.gender && idCardInfo.gender) {
|
||||
this.driverForm.gender = idCardInfo.gender;
|
||||
} else {
|
||||
this.driverForm.gender = '';
|
||||
}
|
||||
if (this.submitAttempted) {
|
||||
this.$nextTick(() => {
|
||||
@@ -1244,12 +1299,13 @@ export default {
|
||||
},
|
||||
handleIdCardUploadSuccess(prop, url) {
|
||||
this.setImage(prop, url);
|
||||
if (prop !== 'idCardFront') {
|
||||
return;
|
||||
this.recognizeIdCard(url, prop);
|
||||
},
|
||||
handleIdCardLongTermChange() {
|
||||
if (this.driverForm.idCardLongTerm === 1) {
|
||||
this.driverForm.idCardValidEnd = '';
|
||||
}
|
||||
this.recognizeBaiduOcr(url, 'id_card', 'front', '身份证', data => {
|
||||
this.applyIdCardRecognition(data);
|
||||
});
|
||||
this.validateFormField('idCardValidEnd');
|
||||
},
|
||||
handleDrivingLicenseUploadSuccess(prop, url) {
|
||||
this.setImage(prop, url);
|
||||
@@ -1292,7 +1348,7 @@ export default {
|
||||
loading.close();
|
||||
});
|
||||
},
|
||||
recognizeIdCard(url = '') {
|
||||
recognizeIdCard(url = '', prop = 'idCardFront') {
|
||||
if (!url) {
|
||||
this.$message.warning('身份证图片上传成功,未获取到图片地址,无法自动识别');
|
||||
return;
|
||||
@@ -1305,7 +1361,7 @@ export default {
|
||||
recognitionIDCard(url)
|
||||
.then(res => {
|
||||
const data = res.data.data || {};
|
||||
this.applyIdCardRecognition(data);
|
||||
this.applyIdCardRecognition(data, prop);
|
||||
this.$message.success('身份证识别完成');
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -1428,45 +1484,62 @@ export default {
|
||||
this.driverForm.drivingLicenseLongTerm = 0;
|
||||
}
|
||||
},
|
||||
applyIdCardRecognition(data = {}) {
|
||||
applyIdCardRecognition(data = {}, prop = 'idCardFront') {
|
||||
// 正反面都走同一 ShenFenZheng 接口;反面结果可能也带姓名/民族等字段,
|
||||
// 反面只补空、不覆盖正面已填值,避免国徽页误识别冲掉正面信息。
|
||||
const fromBack = prop === 'idCardBack';
|
||||
const canWrite = (current, next) => !!next && (!fromBack || !current);
|
||||
const driverName = data.name || data.driverName || this.getOcrValue(data, ['name', '姓名']);
|
||||
const idCardNo = String(
|
||||
data.idNo ||
|
||||
data.idCardNo ||
|
||||
data.idCard ||
|
||||
this.getOcrValue(data, ['idNo', 'idCardNo', 'number', '公民身份号码', '身份证号'])
|
||||
).toUpperCase();
|
||||
this.getOcrValue(data, ['idNo', 'idCardNo', 'number', '公民身份号码', '身份证号']) ||
|
||||
''
|
||||
)
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
const ocrGender = data.gender || this.getOcrValue(data, ['gender', '性别']);
|
||||
const ocrNation =
|
||||
data.nation ||
|
||||
data.ethnicGroup ||
|
||||
this.getOcrValue(data, ['ethnic_group', 'nation', '民族']);
|
||||
const ocrAddress = data.address || this.getOcrValue(data, ['address', '住址', '地址']);
|
||||
if (driverName) {
|
||||
const ocrBirthday =
|
||||
data.birthday || this.getOcrValue(data, ['birthday', '出生', '出生年月', '出生日期']);
|
||||
if (canWrite(this.driverForm.driverName, driverName)) {
|
||||
this.driverForm.driverName = driverName;
|
||||
}
|
||||
if (ocrGender) {
|
||||
if (canWrite(this.driverForm.gender, ocrGender)) {
|
||||
this.driverForm.gender = ocrGender;
|
||||
}
|
||||
if (ocrNation) {
|
||||
if (canWrite(this.driverForm.nation, ocrNation)) {
|
||||
this.driverForm.nation = this.normalizeNation(ocrNation);
|
||||
}
|
||||
if (ocrAddress) {
|
||||
if (canWrite(this.driverForm.address, ocrAddress)) {
|
||||
this.driverForm.address = String(ocrAddress).trim();
|
||||
}
|
||||
if (idCardNo) {
|
||||
if (ocrBirthday) {
|
||||
const birthday = this.normalizeBirthday(ocrBirthday);
|
||||
if (canWrite(this.driverForm.birthday, birthday)) {
|
||||
this.driverForm.birthday = birthday;
|
||||
}
|
||||
}
|
||||
if (idCardNo && canWrite(this.driverForm.idCardNo, idCardNo)) {
|
||||
this.driverForm.idCardNo = idCardNo;
|
||||
if (!this.driverForm.qualificationNo) {
|
||||
this.driverForm.qualificationNo = idCardNo;
|
||||
}
|
||||
const idCardInfo = this.parseIdCardInfo(idCardNo);
|
||||
if (idCardInfo.birthday) {
|
||||
if (!this.driverForm.birthday && idCardInfo.birthday) {
|
||||
this.driverForm.birthday = idCardInfo.birthday;
|
||||
}
|
||||
if (!this.driverForm.gender && idCardInfo.gender) {
|
||||
this.driverForm.gender = idCardInfo.gender;
|
||||
}
|
||||
}
|
||||
// 有效期:正反面都可回填;反面通常才有,识别到就覆盖
|
||||
this.applyIdCardValidity(data);
|
||||
this.$nextTick(() => {
|
||||
[
|
||||
'driverName',
|
||||
@@ -1476,11 +1549,46 @@ export default {
|
||||
'nation',
|
||||
'qualificationNo',
|
||||
'address',
|
||||
].forEach(prop => {
|
||||
this.$refs.driverForm?.validateField(prop);
|
||||
'idCardValidEnd',
|
||||
].forEach(field => {
|
||||
this.$refs.driverForm?.validateField(field);
|
||||
});
|
||||
});
|
||||
},
|
||||
applyIdCardValidity(data = {}) {
|
||||
const validStart =
|
||||
data.validDateStart ||
|
||||
data.idCardValidStart ||
|
||||
this.getOcrValue(data, ['签发日期', '有效起始日期', '有效期限起', '身份证_签发日期']);
|
||||
const validEndRaw =
|
||||
data.validDateEnd ||
|
||||
data.idCardValidEnd ||
|
||||
this.getOcrValue(data, [
|
||||
'失效日期',
|
||||
'有效期限',
|
||||
'有效期限止',
|
||||
'身份证_有效期限',
|
||||
'身份证_失效日期',
|
||||
]);
|
||||
const longTerm = data.longTerm ?? data.idCardLongTerm;
|
||||
const validEndText = String(validEndRaw || '').trim();
|
||||
if (longTerm === 1 || longTerm === '1' || /长期|永久/.test(validEndText)) {
|
||||
this.driverForm.idCardLongTerm = 1;
|
||||
this.driverForm.idCardValidEnd = '';
|
||||
} else if (validEndText) {
|
||||
const endDate = this.normalizeBirthday(validEndText);
|
||||
if (endDate) {
|
||||
this.driverForm.idCardValidEnd = endDate;
|
||||
this.driverForm.idCardLongTerm = 0;
|
||||
}
|
||||
}
|
||||
if (validStart) {
|
||||
const startDate = this.normalizeBirthday(validStart);
|
||||
if (startDate) {
|
||||
this.driverForm.idCardValidStart = startDate;
|
||||
}
|
||||
}
|
||||
},
|
||||
getOcrValue(data = {}, keys = []) {
|
||||
const keyInfos = data.keyInfos || data.keyInfo || data.ocrKeyInfos || [];
|
||||
const list = Array.isArray(keyInfos) ? keyInfos : [];
|
||||
@@ -1659,6 +1767,8 @@ export default {
|
||||
const row = {
|
||||
...this.driverForm,
|
||||
birthday: this.normalizeBirthday(this.driverForm.birthday),
|
||||
idCardValidStart: this.normalizeBirthday(this.driverForm.idCardValidStart),
|
||||
idCardValidEnd: this.normalizeBirthday(this.driverForm.idCardValidEnd),
|
||||
drivingLicenseStartDate: this.normalizeBirthday(this.driverForm.drivingLicenseStartDate),
|
||||
drivingLicenseEndDate: this.normalizeBirthday(this.driverForm.drivingLicenseEndDate),
|
||||
qualificationStartDate: this.normalizeBirthday(this.driverForm.qualificationStartDate),
|
||||
@@ -1952,6 +2062,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 空组织记录在列表里的占位文案
|
||||
.organization-name--empty {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.driver-page__date-expiring {
|
||||
color: #d40000;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -59,7 +59,22 @@
|
||||
</el-button>
|
||||
</template>
|
||||
<template #organizationName="{ row }">
|
||||
{{ String(row.organizationName || '').replace(/^[\s\u3000]+/, '') }}
|
||||
<el-tooltip
|
||||
:content="formatOrgDisplayName(row.organizationName)"
|
||||
placement="top"
|
||||
:disabled="!formatOrgDisplayName(row.organizationName)"
|
||||
>
|
||||
<span class="org-cell-text">{{ formatOrgDisplayName(row.organizationName) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #useDepartment="{ row }">
|
||||
<el-tooltip
|
||||
:content="formatOrgDisplayName(row.useDepartment)"
|
||||
placement="top"
|
||||
:disabled="!formatOrgDisplayName(row.useDepartment)"
|
||||
>
|
||||
<span class="org-cell-text">{{ formatOrgDisplayName(row.useDepartment) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
|
||||
@@ -315,26 +330,58 @@
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="所有组织" prop="organizationName">
|
||||
<el-cascader
|
||||
v-model="organizationCascaderValue"
|
||||
:options="organizationTreeOptions"
|
||||
:props="organizationCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-tooltip
|
||||
:content="
|
||||
formatOrgCascaderPath(organizationCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.organizationName)
|
||||
"
|
||||
placement="top"
|
||||
:disabled="
|
||||
!(
|
||||
formatOrgCascaderPath(organizationCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.organizationName)
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="org-cascader-tooltip">
|
||||
<el-cascader
|
||||
v-model="organizationCascaderValue"
|
||||
:options="organizationFormTreeOptions"
|
||||
:props="formOrgCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="使用部门" prop="useDepartment">
|
||||
<el-cascader
|
||||
v-model="useDepartmentCascaderValue"
|
||||
:options="organizationTreeOptions"
|
||||
:props="organizationCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
<el-tooltip
|
||||
:content="
|
||||
formatOrgCascaderPath(useDepartmentCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.useDepartment)
|
||||
"
|
||||
placement="top"
|
||||
:disabled="
|
||||
!(
|
||||
formatOrgCascaderPath(useDepartmentCascaderPath) ||
|
||||
formatOrgDisplayName(vehicleForm.useDepartment)
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="org-cascader-tooltip">
|
||||
<el-cascader
|
||||
v-model="useDepartmentCascaderValue"
|
||||
:options="useDepartmentFormTreeOptions"
|
||||
:props="formOrgCascaderProps"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -652,6 +699,17 @@ export default {
|
||||
vehicleCertificateUploads: {},
|
||||
uploadHeaders: getUploadHeaders(),
|
||||
organizationOptions: [],
|
||||
// 表单两个级联各自维护路径,避免名称回写互相覆盖;不要与搜索栏共用同一棵树节点
|
||||
organizationCascaderPath: [],
|
||||
useDepartmentCascaderPath: [],
|
||||
formOrgCascaderProps: {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
emitPath: true,
|
||||
checkStrictly: true,
|
||||
expandTrigger: 'hover',
|
||||
},
|
||||
vehicleTypeOptions: [
|
||||
{ label: '高栏车', value: '高栏' },
|
||||
{ label: '低栏车', value: '低栏' },
|
||||
@@ -697,22 +755,31 @@ export default {
|
||||
: [{ required: true, message: '请选择行驶证有效期', trigger: 'change' }],
|
||||
};
|
||||
},
|
||||
// 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换
|
||||
// 两个级联必须使用克隆后的独立树,否则 checkStrictly 会在节点上互相抢选中态
|
||||
organizationFormTreeOptions() {
|
||||
return this.cloneOrgTree(this.organizationTreeOptions);
|
||||
},
|
||||
useDepartmentFormTreeOptions() {
|
||||
return this.cloneOrgTree(this.organizationTreeOptions);
|
||||
},
|
||||
// 所属组织级联:本地保存 id 路径,名称同步到表单字段供后端使用
|
||||
organizationCascaderValue: {
|
||||
get() {
|
||||
return this.resolveOrgPath(this.vehicleForm.organizationName);
|
||||
return this.organizationCascaderPath;
|
||||
},
|
||||
set(path) {
|
||||
this.vehicleForm.organizationName = this.resolveOrgName(path);
|
||||
this.organizationCascaderPath = Array.isArray(path) ? [...path] : [];
|
||||
this.vehicleForm.organizationName = this.resolveOrgName(this.organizationCascaderPath);
|
||||
},
|
||||
},
|
||||
// 使用部门级联:与所属组织同一套部门树
|
||||
// 使用部门级联:独立路径状态,避免与所属组织共用计算回写导致选不中
|
||||
useDepartmentCascaderValue: {
|
||||
get() {
|
||||
return this.resolveOrgPath(this.vehicleForm.useDepartment);
|
||||
return this.useDepartmentCascaderPath;
|
||||
},
|
||||
set(path) {
|
||||
this.vehicleForm.useDepartment = this.resolveOrgName(path);
|
||||
this.useDepartmentCascaderPath = Array.isArray(path) ? [...path] : [];
|
||||
this.vehicleForm.useDepartment = this.resolveOrgName(this.useDepartmentCascaderPath);
|
||||
},
|
||||
},
|
||||
isAdmin() {
|
||||
@@ -745,10 +812,49 @@ export default {
|
||||
this.initDeptTree();
|
||||
this.loadOrganizationOptions();
|
||||
},
|
||||
watch: {
|
||||
organizationTreeOptions() {
|
||||
if (this.vehicleBox) {
|
||||
this.syncOrgCascaderPaths();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.permission?.[code] === true;
|
||||
},
|
||||
/** 去掉组织名称前导空白,供列表与提示展示 */
|
||||
formatOrgDisplayName(name) {
|
||||
return String(name || '').replace(/^[\s\u3000]+/, '');
|
||||
},
|
||||
/** 级联路径还原完整组织层级文案,悬停展示用 */
|
||||
formatOrgCascaderPath(path) {
|
||||
const ids = Array.isArray(path)
|
||||
? path.filter(item => item !== undefined && item !== null && item !== '')
|
||||
: [];
|
||||
if (!ids.length) return '';
|
||||
const labels = [];
|
||||
let nodes = this.organizationTreeOptions || [];
|
||||
for (const id of ids) {
|
||||
const node = nodes.find(item => String(item.id) === String(id));
|
||||
if (!node) break;
|
||||
labels.push(node.rawLabel || node.label || '');
|
||||
nodes = node.children || [];
|
||||
}
|
||||
return labels.filter(Boolean).join(' / ');
|
||||
},
|
||||
cloneOrgTree(tree = []) {
|
||||
return (tree || []).map(item => ({
|
||||
id: String(item.id),
|
||||
label: item.label,
|
||||
rawLabel: item.rawLabel,
|
||||
children: item.children?.length ? this.cloneOrgTree(item.children) : undefined,
|
||||
}));
|
||||
},
|
||||
syncOrgCascaderPaths() {
|
||||
this.organizationCascaderPath = this.resolveOrgPath(this.vehicleForm.organizationName);
|
||||
this.useDepartmentCascaderPath = this.resolveOrgPath(this.vehicleForm.useDepartment);
|
||||
},
|
||||
initDeptTree() {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.organizationOptions = this.flattenDeptOptions(res.data.data || []);
|
||||
@@ -760,6 +866,9 @@ export default {
|
||||
if (this.vehicleBox && !this.vehicleForm.useDepartment) {
|
||||
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
|
||||
}
|
||||
if (this.vehicleBox) {
|
||||
this.syncOrgCascaderPaths();
|
||||
}
|
||||
});
|
||||
},
|
||||
flattenDeptOptions(tree = [], level = 0) {
|
||||
@@ -854,6 +963,7 @@ export default {
|
||||
this.vehicleForm = emptyForm();
|
||||
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
|
||||
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
|
||||
this.syncOrgCascaderPaths();
|
||||
this.syncPlateNo(false);
|
||||
this.vehicleBox = true;
|
||||
return;
|
||||
@@ -867,6 +977,7 @@ export default {
|
||||
if (!this.vehicleForm.useDepartment) {
|
||||
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
|
||||
}
|
||||
this.syncOrgCascaderPaths();
|
||||
this.splitPlateNo();
|
||||
this.syncPlateNo(false);
|
||||
this.vehicleBox = true;
|
||||
@@ -878,6 +989,8 @@ export default {
|
||||
},
|
||||
resetVehicle() {
|
||||
this.vehicleForm = emptyForm();
|
||||
this.organizationCascaderPath = [];
|
||||
this.useDepartmentCascaderPath = [];
|
||||
this.vehicleCertificateUploads = {};
|
||||
this.readonly = false;
|
||||
this.submitLoading = false;
|
||||
@@ -1530,7 +1643,20 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.org-cell-text {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.vehicle-form {
|
||||
.org-cascader-tooltip {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-date-editor.el-input),
|
||||
:deep(.el-date-editor.el-input__wrapper),
|
||||
:deep(.el-select),
|
||||
|
||||
@@ -101,6 +101,12 @@
|
||||
style="width: 500px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="减分最高扣分">
|
||||
<el-input :model-value="HARDCODED_MAX_MINUS_SCORE" disabled style="width: 500px">
|
||||
<template #append>分</template>
|
||||
</el-input>
|
||||
<div class="score-config__hint">暂定最高扣 15 分,加分合计同样最高按 15 分计</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="configForm.remark"
|
||||
@@ -489,9 +495,15 @@ const standardNumberFields = [
|
||||
'creditLimitIncrease',
|
||||
];
|
||||
|
||||
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
|
||||
const HARDCODED_MAX_PLUS_SCORE = 15;
|
||||
const HARDCODED_MAX_MINUS_SCORE = 15;
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
HARDCODED_MAX_PLUS_SCORE,
|
||||
HARDCODED_MAX_MINUS_SCORE,
|
||||
form: {},
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -628,6 +640,7 @@ export default {
|
||||
name: '',
|
||||
remark: '',
|
||||
standardDescription: '',
|
||||
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
|
||||
status: 3,
|
||||
categories: this.defaultCategories(),
|
||||
standards: [],
|
||||
@@ -684,6 +697,8 @@ export default {
|
||||
});
|
||||
return {
|
||||
...detail,
|
||||
// 暂定写死最高扣分,不读取历史配置值
|
||||
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
|
||||
categories: categories.map(category => ({
|
||||
...(map[category.categoryCode] || category),
|
||||
categoryName: category.categoryName,
|
||||
@@ -816,6 +831,7 @@ export default {
|
||||
},
|
||||
prepareSubmitConfig(config) {
|
||||
const payload = JSON.parse(JSON.stringify(config || {}));
|
||||
payload.maxMinusScore = this.toScoreValue(HARDCODED_MAX_MINUS_SCORE);
|
||||
(payload.categories || []).forEach(category => {
|
||||
(category.items || []).forEach(item => {
|
||||
item.score = this.toScoreValue(item.score);
|
||||
@@ -1072,6 +1088,8 @@ export default {
|
||||
this.$message.warning('请输入评定表名称');
|
||||
return false;
|
||||
}
|
||||
// 减分最高扣分暂定写死为 15,提交前强制回填,避免历史脏数据触发校验
|
||||
this.configForm.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
|
||||
const basic = this.configForm.categories.find(item => item.categoryCode === 'basic');
|
||||
if (!basic || basic.items.length === 0) {
|
||||
this.$message.warning('至少配置1个基础得分项目');
|
||||
@@ -1554,6 +1572,13 @@ export default {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.score-config__hint {
|
||||
margin-top: 6px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:deep(.score-config__name-input) {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user