Compare commits
10
Commits
cce252851d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b909dd693e | ||
|
|
3ded1868a3 | ||
|
|
c36003db2f | ||
|
|
a6e4e74ad1 | ||
|
|
fc969390ae | ||
|
|
6f20fec8de | ||
|
|
e03e019f58 | ||
|
|
2de0a12d44 | ||
|
|
fa7be38e7f | ||
|
|
c5b744737d |
@@ -10,6 +10,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.16.0",
|
||||
"dayjs": "^1.11.20",
|
||||
|
||||
Generated
+488
@@ -11,6 +11,15 @@ importers:
|
||||
'@ant-design/icons-vue':
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1(vue@3.5.34)
|
||||
'@wangeditor/editor':
|
||||
specifier: ^5.1.23
|
||||
version: 5.1.23
|
||||
'@wangeditor/editor-for-vue':
|
||||
specifier: ^5.1.12
|
||||
version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)
|
||||
'@xmldom/xmldom':
|
||||
specifier: ^0.8.10
|
||||
version: 0.8.13
|
||||
ant-design-vue:
|
||||
specifier: ^4.2.6
|
||||
version: 4.2.6(vue@3.5.34)
|
||||
@@ -402,12 +411,35 @@ packages:
|
||||
'@simonwep/pickr@1.8.2':
|
||||
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
|
||||
|
||||
'@transloadit/prettier-bytes@0.0.7':
|
||||
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/event-emitter@0.3.5':
|
||||
resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
|
||||
|
||||
'@types/node@25.9.1':
|
||||
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
|
||||
|
||||
'@uppy/core@2.3.4':
|
||||
resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==}
|
||||
|
||||
'@uppy/store-default@2.1.1':
|
||||
resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==}
|
||||
|
||||
'@uppy/utils@4.1.3':
|
||||
resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==}
|
||||
|
||||
'@uppy/xhr-upload@2.1.3':
|
||||
resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.3.3
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4':
|
||||
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -452,6 +484,97 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
|
||||
'@wangeditor/basic-modules@1.1.7':
|
||||
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.throttle: ^4.1.1
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/code-highlight@1.0.3':
|
||||
resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/core@1.1.19':
|
||||
resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.1.1
|
||||
'@uppy/xhr-upload': ^2.0.3
|
||||
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.isequal: ^4.5.0
|
||||
lodash.throttle: ^4.1.1
|
||||
lodash.toarray: ^4.4.0
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/editor-for-vue@5.1.12':
|
||||
resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==}
|
||||
peerDependencies:
|
||||
'@wangeditor/editor': '>=5.1.0'
|
||||
vue: ^3.0.5
|
||||
|
||||
'@wangeditor/editor@5.1.23':
|
||||
resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==}
|
||||
|
||||
'@wangeditor/list-module@1.0.5':
|
||||
resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/table-module@1.1.4':
|
||||
resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.isequal: ^4.5.0
|
||||
lodash.throttle: ^4.1.1
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/upload-image-module@1.0.2':
|
||||
resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.0.3
|
||||
'@uppy/xhr-upload': ^2.0.3
|
||||
'@wangeditor/basic-modules': 1.x
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.foreach: ^4.5.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/video-module@1.1.4':
|
||||
resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.1.4
|
||||
'@uppy/xhr-upload': ^2.0.7
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@xmldom/xmldom@0.8.13':
|
||||
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@xmldom/xmldom@0.9.10':
|
||||
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
|
||||
engines: {node: '>=14.6'}
|
||||
@@ -498,6 +621,10 @@ packages:
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
d@1.0.2:
|
||||
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
dayjs@1.11.20:
|
||||
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
|
||||
|
||||
@@ -534,6 +661,9 @@ packages:
|
||||
dom-zindex@1.0.6:
|
||||
resolution: {integrity: sha512-FKWIhiU96bi3xpP9ewRMgANsoVmMUBnMnmpCT6dPMZOunVYJQmJhSRruoI0XSPoHeIif3kyEuiHbFrOJwEJaEA==}
|
||||
|
||||
dom7@3.0.0:
|
||||
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -558,14 +688,35 @@ packages:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es5-ext@0.10.64:
|
||||
resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
es6-iterator@2.0.3:
|
||||
resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
|
||||
|
||||
es6-symbol@3.1.4:
|
||||
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
|
||||
engines: {node: '>=0.12'}
|
||||
|
||||
esbuild@0.25.12:
|
||||
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esniff@2.0.1:
|
||||
resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
event-emitter@0.3.5:
|
||||
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
|
||||
|
||||
ext@1.7.0:
|
||||
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -626,20 +777,39 @@ packages:
|
||||
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
html-void-elements@2.0.1:
|
||||
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
i18next@20.6.1:
|
||||
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
immer@9.0.21:
|
||||
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
is-hotkey@0.2.0:
|
||||
resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==}
|
||||
|
||||
is-plain-object@3.0.1:
|
||||
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-url@1.2.4:
|
||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
@@ -655,6 +825,28 @@ packages:
|
||||
lodash-es@4.18.1:
|
||||
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
|
||||
|
||||
lodash.camelcase@4.3.0:
|
||||
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||
|
||||
lodash.clonedeep@4.5.0:
|
||||
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||
|
||||
lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
|
||||
lodash.foreach@4.5.0:
|
||||
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
|
||||
|
||||
lodash.isequal@4.5.0:
|
||||
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
|
||||
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
|
||||
|
||||
lodash.throttle@4.1.1:
|
||||
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
|
||||
|
||||
lodash.toarray@4.4.0:
|
||||
resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
|
||||
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
@@ -673,6 +865,9 @@ packages:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-match@1.0.2:
|
||||
resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -683,6 +878,9 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
namespace-emitter@2.0.1:
|
||||
resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
|
||||
|
||||
nanoid@3.3.12:
|
||||
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -696,6 +894,9 @@ packages:
|
||||
nanopop@2.4.2:
|
||||
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
|
||||
|
||||
next-tick@1.1.0:
|
||||
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
@@ -725,6 +926,18 @@ packages:
|
||||
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
preact@10.29.7:
|
||||
resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==}
|
||||
peerDependencies:
|
||||
preact-render-to-string: '>=5'
|
||||
peerDependenciesMeta:
|
||||
preact-render-to-string:
|
||||
optional: true
|
||||
|
||||
prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
@@ -759,10 +972,25 @@ packages:
|
||||
shallow-equal@1.2.1:
|
||||
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
|
||||
|
||||
slate-history@0.66.0:
|
||||
resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==}
|
||||
peerDependencies:
|
||||
slate: '>=0.65.3'
|
||||
|
||||
slate@0.72.8:
|
||||
resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==}
|
||||
|
||||
snabbdom@3.6.4:
|
||||
resolution: {integrity: sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==}
|
||||
engines: {node: '>=12.17.0'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
ssr-window@3.0.0:
|
||||
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
@@ -773,10 +1001,16 @@ packages:
|
||||
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
|
||||
engines: {node: '>=12.22'}
|
||||
|
||||
tiny-warning@1.0.3:
|
||||
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
type@2.7.3:
|
||||
resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
|
||||
|
||||
undici-types@7.24.6:
|
||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||
|
||||
@@ -862,6 +1096,9 @@ packages:
|
||||
warning@4.0.3:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
|
||||
wildcard@1.1.2:
|
||||
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
|
||||
|
||||
xe-utils@4.0.8:
|
||||
resolution: {integrity: sha512-l+p++9iPtSxpf/5ZCzkPXsyV0avE34KzieAC9oAhi+3e98PLJaBQHNiY2V8Lnbiux4JFAbU10p8AT6oujXkP9A==}
|
||||
|
||||
@@ -1067,12 +1304,47 @@ snapshots:
|
||||
core-js: 3.49.0
|
||||
nanopop: 2.4.2
|
||||
|
||||
'@transloadit/prettier-bytes@0.0.7': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/event-emitter@0.3.5': {}
|
||||
|
||||
'@types/node@25.9.1':
|
||||
dependencies:
|
||||
undici-types: 7.24.6
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
dependencies:
|
||||
'@uppy/utils': 4.1.3
|
||||
namespace-emitter: 2.0.1
|
||||
|
||||
'@uppy/core@2.3.4':
|
||||
dependencies:
|
||||
'@transloadit/prettier-bytes': 0.0.7
|
||||
'@uppy/store-default': 2.1.1
|
||||
'@uppy/utils': 4.1.3
|
||||
lodash.throttle: 4.1.1
|
||||
mime-match: 1.0.2
|
||||
namespace-emitter: 2.0.1
|
||||
nanoid: 3.3.12
|
||||
preact: 10.29.7
|
||||
transitivePeerDependencies:
|
||||
- preact-render-to-string
|
||||
|
||||
'@uppy/store-default@2.1.1': {}
|
||||
|
||||
'@uppy/utils@4.1.3':
|
||||
dependencies:
|
||||
lodash.throttle: 4.1.1
|
||||
|
||||
'@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)':
|
||||
dependencies:
|
||||
'@uppy/companion-client': 2.2.2
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/utils': 4.1.3
|
||||
nanoid: 3.3.12
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@25.9.1))(vue@3.5.34)':
|
||||
dependencies:
|
||||
vite: 6.4.2(@types/node@25.9.1)
|
||||
@@ -1140,6 +1412,118 @@ snapshots:
|
||||
vue: 3.5.34
|
||||
xe-utils: 4.0.8
|
||||
|
||||
'@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
is-url: 1.2.4
|
||||
lodash.throttle: 4.1.1
|
||||
nanoid: 3.3.12
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
prismjs: 1.30.0
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
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
|
||||
event-emitter: 0.3.5
|
||||
html-void-elements: 2.0.1
|
||||
i18next: 20.6.1
|
||||
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.isequal: 4.5.0
|
||||
lodash.throttle: 4.1.1
|
||||
lodash.toarray: 4.4.0
|
||||
nanoid: 3.3.12
|
||||
scroll-into-view-if-needed: 2.2.31
|
||||
slate: 0.72.8
|
||||
slate-history: 0.66.0(slate@0.72.8)
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)':
|
||||
dependencies:
|
||||
'@wangeditor/editor': 5.1.23
|
||||
vue: 3.5.34
|
||||
|
||||
'@wangeditor/editor@5.1.23':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/core': 1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.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.isequal: 4.5.0
|
||||
lodash.throttle: 4.1.1
|
||||
lodash.toarray: 4.4.0
|
||||
nanoid: 3.3.12
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
transitivePeerDependencies:
|
||||
- preact-render-to-string
|
||||
|
||||
'@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
lodash.isequal: 4.5.0
|
||||
lodash.throttle: 4.1.1
|
||||
nanoid: 3.3.12
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/core': 1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
lodash.foreach: 4.5.0
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor/core': 1.1.19(@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.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
nanoid: 3.3.12
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@xmldom/xmldom@0.8.13': {}
|
||||
|
||||
'@xmldom/xmldom@0.9.10': {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
@@ -1207,6 +1591,11 @@ snapshots:
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
d@1.0.2:
|
||||
dependencies:
|
||||
es5-ext: 0.10.64
|
||||
type: 2.7.3
|
||||
|
||||
dayjs@1.11.20: {}
|
||||
|
||||
debug@4.4.3:
|
||||
@@ -1238,6 +1627,10 @@ snapshots:
|
||||
|
||||
dom-zindex@1.0.6: {}
|
||||
|
||||
dom7@3.0.0:
|
||||
dependencies:
|
||||
ssr-window: 3.0.0
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -1261,6 +1654,24 @@ snapshots:
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.3
|
||||
|
||||
es5-ext@0.10.64:
|
||||
dependencies:
|
||||
es6-iterator: 2.0.3
|
||||
es6-symbol: 3.1.4
|
||||
esniff: 2.0.1
|
||||
next-tick: 1.1.0
|
||||
|
||||
es6-iterator@2.0.3:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
es5-ext: 0.10.64
|
||||
es6-symbol: 3.1.4
|
||||
|
||||
es6-symbol@3.1.4:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
ext: 1.7.0
|
||||
|
||||
esbuild@0.25.12:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.12
|
||||
@@ -1290,8 +1701,24 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.25.12
|
||||
'@esbuild/win32-x64': 0.25.12
|
||||
|
||||
esniff@2.0.1:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
es5-ext: 0.10.64
|
||||
event-emitter: 0.3.5
|
||||
type: 2.7.3
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
event-emitter@0.3.5:
|
||||
dependencies:
|
||||
d: 1.0.2
|
||||
es5-ext: 0.10.64
|
||||
|
||||
ext@1.7.0:
|
||||
dependencies:
|
||||
type: 2.7.3
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.4):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.4
|
||||
@@ -1348,6 +1775,8 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
html-void-elements@2.0.1: {}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
@@ -1355,12 +1784,24 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
i18next@20.6.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.2
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immer@9.0.21: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
is-hotkey@0.2.0: {}
|
||||
|
||||
is-plain-object@3.0.1: {}
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
||||
is-url@1.2.4: {}
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -1378,6 +1819,20 @@ snapshots:
|
||||
|
||||
lodash-es@4.18.1: {}
|
||||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
lodash.clonedeep@4.5.0: {}
|
||||
|
||||
lodash.debounce@4.0.8: {}
|
||||
|
||||
lodash.foreach@4.5.0: {}
|
||||
|
||||
lodash.isequal@4.5.0: {}
|
||||
|
||||
lodash.throttle@4.1.1: {}
|
||||
|
||||
lodash.toarray@4.4.0: {}
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
@@ -1392,6 +1847,10 @@ snapshots:
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-match@1.0.2:
|
||||
dependencies:
|
||||
wildcard: 1.1.2
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
@@ -1400,12 +1859,16 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
namespace-emitter@2.0.1: {}
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
|
||||
nanoid@5.1.11: {}
|
||||
|
||||
nanopop@2.4.2: {}
|
||||
|
||||
next-tick@1.1.0: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
pako@2.1.0: {}
|
||||
@@ -1432,6 +1895,10 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
preact@10.29.7: {}
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
proxy-from-env@2.1.0: {}
|
||||
@@ -1491,8 +1958,23 @@ snapshots:
|
||||
|
||||
shallow-equal@1.2.1: {}
|
||||
|
||||
slate-history@0.66.0(slate@0.72.8):
|
||||
dependencies:
|
||||
is-plain-object: 5.0.0
|
||||
slate: 0.72.8
|
||||
|
||||
slate@0.72.8:
|
||||
dependencies:
|
||||
immer: 9.0.21
|
||||
is-plain-object: 5.0.0
|
||||
tiny-warning: 1.0.3
|
||||
|
||||
snabbdom@3.6.4: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
ssr-window@3.0.0: {}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
@@ -1501,11 +1983,15 @@ snapshots:
|
||||
|
||||
throttle-debounce@5.0.2: {}
|
||||
|
||||
tiny-warning@1.0.3: {}
|
||||
|
||||
tinyglobby@0.2.16:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
|
||||
type@2.7.3: {}
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
@@ -1560,6 +2046,8 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
wildcard@1.1.2: {}
|
||||
|
||||
xe-utils@4.0.8: {}
|
||||
|
||||
xml-js@1.6.11:
|
||||
|
||||
@@ -106,6 +106,44 @@ export function finishLaunchSeal(id, data) {
|
||||
});
|
||||
}
|
||||
|
||||
export function initLaunchSealPosition(id) {
|
||||
return request({
|
||||
url: `/contract/launch/${id}/seal-position/init`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
export function getLaunchSealPositionPage(id) {
|
||||
return request({
|
||||
url: `/contract/launch/${id}/seal-position/list`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
export function saveLaunchSealPositions(id, data = {}) {
|
||||
return request({
|
||||
url: `/contract/launch/${id}/seal-position/save`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function batchUpdateLaunchSealPositionStatus(id, data) {
|
||||
return request({
|
||||
url: `/contract/launch/${id}/seal-position/batch-status`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function checkLaunchSealPositions(id, data = {}) {
|
||||
return request({
|
||||
url: `/contract/launch/${id}/seal-position/check`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function queryApproveViewUrl(data) {
|
||||
return request({
|
||||
url: "/contract/push/settlement/approve/view-url",
|
||||
@@ -206,6 +244,15 @@ export function downloadLaunchFile(fileName) {
|
||||
});
|
||||
}
|
||||
|
||||
export function uploadWatermarkFile(id, formData) {
|
||||
return request({
|
||||
url: `/contract/launch/${id}/watermark/upload`,
|
||||
method: "post",
|
||||
data: formData,
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
}
|
||||
|
||||
export function queryContractLogList(data) {
|
||||
return request({
|
||||
url: "/contract/log/list",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import request from '@/utils/request'
|
||||
import {
|
||||
DEFAULT_MOCK_USER,
|
||||
MOCK_USER_HEADER,
|
||||
MOCK_USER_STORAGE_KEY,
|
||||
getStoredMockUser,
|
||||
setStoredMockUser,
|
||||
} from '@/utils/mockUserStorage'
|
||||
|
||||
function getResult(res) {
|
||||
return res?.result
|
||||
}
|
||||
|
||||
function isSuccessResponse(res) {
|
||||
return String(res?.code || '') === '0000'
|
||||
}
|
||||
|
||||
export function listMockUsers() {
|
||||
return request({
|
||||
url: '/mock/security/users',
|
||||
method: 'get',
|
||||
}).then((res) => (isSuccessResponse(res) ? getResult(res) || [] : []))
|
||||
}
|
||||
|
||||
export function getCurrentMockUser() {
|
||||
return request({
|
||||
url: '/mock/security/current',
|
||||
method: 'get',
|
||||
}).then((res) => (isSuccessResponse(res) ? getResult(res) || null : null))
|
||||
}
|
||||
+9
-1
@@ -175,7 +175,7 @@ export function downloadContract(contractId) {
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 回调轮询
|
||||
// 回调轮询 / 测试回调
|
||||
// ========================================
|
||||
export function pollCallback(requestId) {
|
||||
return request({
|
||||
@@ -183,3 +183,11 @@ export function pollCallback(requestId) {
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function mockPreSignSuccessCallback(data) {
|
||||
return request({
|
||||
url: '/seal/callback/presign/mock-success',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<a-layout-sider width="244" class="contract-sider">
|
||||
<div class="sider-brand">
|
||||
<div class="brand-badge">合同管理</div>
|
||||
|
||||
</div>
|
||||
|
||||
<a-menu
|
||||
@@ -16,6 +15,10 @@
|
||||
<form-outlined />
|
||||
<span>发起合同</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/contract-console/sign">
|
||||
<file-done-outlined />
|
||||
<span>合同签署</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/contract-console/manifest">
|
||||
<file-text-outlined />
|
||||
<span>模板管理</span>
|
||||
@@ -32,6 +35,21 @@
|
||||
|
||||
<div class="sider-footer">
|
||||
<a-button block @click="router.push('/')">返回首页</a-button>
|
||||
<div class="mock-user-panel">
|
||||
<div class="mock-user-label">测试用户切换</div>
|
||||
<a-select
|
||||
v-model:value="selectedMockUserKey"
|
||||
class="mock-user-select"
|
||||
:options="mockUserOptions"
|
||||
:loading="mockUserLoading"
|
||||
placeholder="请选择测试用户"
|
||||
@change="handleMockUserChange"
|
||||
/>
|
||||
<div v-if="currentMockUser" class="mock-user-meta">
|
||||
{{ currentMockUser.systemType === 'manage' ? '管理端' : currentMockUser.systemType === 'business' ? '业务端' : '平台' }}
|
||||
· {{ currentMockUser.orgName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
|
||||
@@ -39,9 +57,7 @@
|
||||
<a-layout-header class="contract-header">
|
||||
<div>
|
||||
<div class="header-title">{{ currentTitle }}</div>
|
||||
|
||||
</div>
|
||||
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content class="contract-content">
|
||||
@@ -52,14 +68,34 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { FileTextOutlined, FormOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
getCurrentMockUser,
|
||||
listMockUsers,
|
||||
} from '@/api/mockUser'
|
||||
import { getStoredMockUser, setStoredMockUser } from '@/utils/mockUserStorage'
|
||||
import {
|
||||
FileDoneOutlined,
|
||||
FileTextOutlined,
|
||||
FormOutlined,
|
||||
ProfileOutlined,
|
||||
TeamOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const mockUserLoading = ref(false)
|
||||
const mockUserList = ref([])
|
||||
const currentMockUser = ref(null)
|
||||
const selectedMockUserKey = ref(getStoredMockUser())
|
||||
|
||||
const selectedKey = computed(() => {
|
||||
if (route.path.startsWith('/contract-console/sign')) {
|
||||
return '/contract-console/sign'
|
||||
}
|
||||
|
||||
if (route.path.startsWith('/contract-console/manifest')) {
|
||||
return '/contract-console/manifest'
|
||||
}
|
||||
@@ -77,9 +113,49 @@ const selectedKey = computed(() => {
|
||||
|
||||
const currentTitle = computed(() => route.meta?.title || '合同管理')
|
||||
|
||||
const mockUserOptions = computed(() =>
|
||||
mockUserList.value.map((item) => ({
|
||||
label: `${item.displayName} (${item.orgId})`,
|
||||
value: item.key,
|
||||
})),
|
||||
)
|
||||
|
||||
function handleMenuClick({ key }) {
|
||||
router.push(key)
|
||||
}
|
||||
|
||||
async function loadMockUsers() {
|
||||
mockUserLoading.value = true
|
||||
try {
|
||||
mockUserList.value = await listMockUsers()
|
||||
const availableKeys = new Set(mockUserList.value.map((item) => item.key))
|
||||
if (!availableKeys.has(selectedMockUserKey.value) && mockUserList.value.length > 0) {
|
||||
selectedMockUserKey.value = mockUserList.value[0].key
|
||||
setStoredMockUser(selectedMockUserKey.value)
|
||||
}
|
||||
} finally {
|
||||
mockUserLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCurrentMockUser() {
|
||||
currentMockUser.value = await getCurrentMockUser()
|
||||
if (currentMockUser.value?.key) {
|
||||
selectedMockUserKey.value = currentMockUser.value.key
|
||||
setStoredMockUser(currentMockUser.value.key)
|
||||
}
|
||||
}
|
||||
|
||||
function handleMockUserChange(value) {
|
||||
setStoredMockUser(value)
|
||||
message.success('测试用户已切换,正在刷新页面数据')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMockUsers()
|
||||
await loadCurrentMockUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -114,19 +190,6 @@ function handleMenuClick({ key }) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sider-brand h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sider-brand p {
|
||||
color: #64748b;
|
||||
line-height: 1.8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contract-menu {
|
||||
border-inline-end: none;
|
||||
background: transparent;
|
||||
@@ -151,6 +214,33 @@ function handleMenuClick({ key }) {
|
||||
padding: 16px 20px 20px;
|
||||
}
|
||||
|
||||
.mock-user-panel {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid rgba(37, 99, 235, 0.12);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.mock-user-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mock-user-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mock-user-meta {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.contract-header {
|
||||
height: auto;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
@@ -169,12 +259,6 @@ function handleMenuClick({ key }) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.header-desc {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.contract-content {
|
||||
padding: 20px;
|
||||
min-height: 0;
|
||||
|
||||
@@ -19,6 +19,14 @@ const routes = [
|
||||
title: '发起合同',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'sign',
|
||||
name: 'ContractSignManage',
|
||||
component: () => import('@/views/contract/sign/index.vue'),
|
||||
meta: {
|
||||
title: '合同签署',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'manifest',
|
||||
name: 'ContractTemplateManage',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const MOCK_USER_STORAGE_KEY = 'stamp_mock_user'
|
||||
export const DEFAULT_MOCK_USER = 'admin'
|
||||
export const MOCK_USER_HEADER = 'X-Mock-User'
|
||||
|
||||
export function getStoredMockUser() {
|
||||
return localStorage.getItem(MOCK_USER_STORAGE_KEY) || DEFAULT_MOCK_USER
|
||||
}
|
||||
|
||||
export function setStoredMockUser(userKey) {
|
||||
if (!userKey) {
|
||||
localStorage.removeItem(MOCK_USER_STORAGE_KEY)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(MOCK_USER_STORAGE_KEY, userKey)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import { DEFAULT_MOCK_USER, getStoredMockUser, MOCK_USER_HEADER } from '@/utils/mockUserStorage'
|
||||
import { message, Modal, notification } from 'ant-design-vue'
|
||||
|
||||
const service = axios.create({
|
||||
@@ -12,6 +13,8 @@ service.interceptors.request.use(
|
||||
if (token) {
|
||||
config.headers['Authorization'] = 'Bearer ' + token
|
||||
}
|
||||
const mockUser = getStoredMockUser() || DEFAULT_MOCK_USER
|
||||
config.headers[MOCK_USER_HEADER] = mockUser
|
||||
return config
|
||||
},
|
||||
(error) => Promise.reject(error),
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
ok-text="确认采集"
|
||||
cancel-text="取消"
|
||||
@ok="handleSubmit"
|
||||
centered
|
||||
>
|
||||
<div class="collect-body">
|
||||
<a-descriptions :column="2" size="small" bordered class="contract-brief">
|
||||
|
||||
@@ -707,15 +707,15 @@
|
||||
<a-radio-group
|
||||
v-model:value="detailState.isInvolveHongKongMacaoTaiwan"
|
||||
>
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
<a-radio value="1">是</a-radio>
|
||||
<a-radio value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="boolean-row">
|
||||
<span>是否公开招标</span>
|
||||
<a-radio-group v-model:value="detailState.isPublicTender">
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
<a-radio value="1">是</a-radio>
|
||||
<a-radio value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="boolean-row">
|
||||
@@ -723,8 +723,8 @@
|
||||
<a-radio-group
|
||||
v-model:value="detailState.isForeignRelatedSubject"
|
||||
>
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
<a-radio value="1">是</a-radio>
|
||||
<a-radio value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div
|
||||
@@ -735,22 +735,22 @@
|
||||
<a-radio-group
|
||||
v-model:value="detailState.isIncludedInCurrentYearBudget"
|
||||
>
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
<a-radio value="1">是</a-radio>
|
||||
<a-radio value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="boolean-row">
|
||||
<span>是否共享</span>
|
||||
<a-radio-group v-model:value="detailState.isShared">
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
<a-radio value="1">是</a-radio>
|
||||
<a-radio value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="boolean-row">
|
||||
<span>是否重大合同</span>
|
||||
<a-radio-group v-model:value="detailState.isMajorContract">
|
||||
<a-radio :value="true">是</a-radio>
|
||||
<a-radio :value="false">否</a-radio>
|
||||
<a-radio value="1">是</a-radio>
|
||||
<a-radio value="0">否</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div class="boolean-row">
|
||||
@@ -1043,12 +1043,9 @@ function normalizeFileItem(item = {}) {
|
||||
}
|
||||
|
||||
function normalizeBooleanValue(value) {
|
||||
if (value === true || value === false) return value;
|
||||
if (value === 1 || value === "1" || value === "Y" || value === "true")
|
||||
return true;
|
||||
if (value === 0 || value === "0" || value === "N" || value === "false")
|
||||
return false;
|
||||
return false;
|
||||
if (value === 1 || value === "1" || value === true || value === "Y" || value === "true")
|
||||
return "1";
|
||||
return "0";
|
||||
}
|
||||
|
||||
function pickBooleanSource(source = {}, key) {
|
||||
@@ -1093,21 +1090,24 @@ function splitFilesByCategory(source = {}) {
|
||||
|
||||
return {
|
||||
contractBodyFileName:
|
||||
getFileNameFromDetail(source, "body") || bodyFile.fileName || "",
|
||||
source.contractBodyFileName ||
|
||||
bodyFile.fileName ||
|
||||
getFileNameFromDetail(source, "body") ||
|
||||
"",
|
||||
contractBodyFilePath:
|
||||
source.contractBodyFilePath ||
|
||||
source.contractTextFilePath ||
|
||||
bodyFile.filePath ||
|
||||
source.contractTextFilePath ||
|
||||
"",
|
||||
contractBodyFileType:
|
||||
source.contractBodyFileType ||
|
||||
source.contractTextFileType ||
|
||||
bodyFile.fileType ||
|
||||
source.contractTextFileType ||
|
||||
"",
|
||||
contractBodyFileSize:
|
||||
source.contractBodyFileSize ??
|
||||
source.contractTextFileSize ??
|
||||
bodyFile.fileSize ??
|
||||
source.contractTextFileSize ??
|
||||
null,
|
||||
otherAttachmentList: source.otherAttachmentList?.length
|
||||
? source.otherAttachmentList.map(normalizeFileItem)
|
||||
@@ -1184,6 +1184,9 @@ function createEmptyParty(roleCode, index = 0) {
|
||||
oppositeId: "",
|
||||
companyName: "",
|
||||
companyType: "",
|
||||
companyNature: "",
|
||||
orgNature: "",
|
||||
orgNatureLevel: "",
|
||||
oppositeCharacter: "",
|
||||
oppCharacter: "",
|
||||
contactName: "",
|
||||
@@ -1193,6 +1196,8 @@ function createEmptyParty(roleCode, index = 0) {
|
||||
registeredAddress: "",
|
||||
legalRepresentative: "",
|
||||
creditCode: "",
|
||||
registeredCapitalText: "",
|
||||
countryName: "",
|
||||
naturalPersonIdType: "",
|
||||
naturalPersonIdNumber: "",
|
||||
isGroupInternal: false,
|
||||
@@ -1405,12 +1410,12 @@ function createContractFormData() {
|
||||
counterpartyList: [],
|
||||
},
|
||||
isSingleAgreement: "0",
|
||||
isInvolveHongKongMacaoTaiwan: false,
|
||||
isPublicTender: false,
|
||||
isForeignRelatedSubject: false,
|
||||
isIncludedInCurrentYearBudget: false,
|
||||
isShared: false,
|
||||
isMajorContract: false,
|
||||
isInvolveHongKongMacaoTaiwan: "0",
|
||||
isPublicTender: "0",
|
||||
isForeignRelatedSubject: "0",
|
||||
isIncludedInCurrentYearBudget: "0",
|
||||
isShared: "0",
|
||||
isMajorContract: "0",
|
||||
needAmendments: "0",
|
||||
extraAmounts: {
|
||||
depositEnabled: false,
|
||||
@@ -1734,6 +1739,9 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
|
||||
oppositeId: raw.oppositeId || "",
|
||||
companyName: raw.customerNameC || companyName || "",
|
||||
companyType: getRelativeTypeLabel(raw.relativeType),
|
||||
companyNature: raw.oppositeCharacter || "",
|
||||
orgNature: raw.natureOfEnterpriseOrg || "",
|
||||
orgNatureLevel: raw.natureOfEnterpriseOrgLev || "",
|
||||
oppositeCharacter: raw.oppositeCharacter || "",
|
||||
oppCharacter: raw.oppositeCharacter || "",
|
||||
creditCode: raw.customerTaxno || "",
|
||||
@@ -1743,6 +1751,8 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
|
||||
contactPhone: firstContact.contactPhone || raw.officephone || "",
|
||||
contactEmail: firstContact.email || raw.emailAddress || "",
|
||||
contactAddress: firstContact.postalAddress || raw.customerAddress || "",
|
||||
registeredCapitalText: raw.regCapital || raw.registeredCapital || "",
|
||||
countryName: raw.countryName || raw.country || "",
|
||||
naturalPersonIdType: raw.certificateType || "",
|
||||
naturalPersonIdNumber: raw.certificateNum || "",
|
||||
isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1",
|
||||
@@ -1757,12 +1767,19 @@ function mapOrgNodeToParty(raw = {}) {
|
||||
...createEmptyParty(PARTY_ROLE_OUR),
|
||||
subjectCode: raw.orgId == null ? "" : String(raw.orgId),
|
||||
companyName: raw.orgName || "",
|
||||
companyNature: raw.companyNature || "1",
|
||||
orgNature: raw.orgNature || "QY",
|
||||
orgNatureLevel: raw.orgNatureLevel || "",
|
||||
contactPhone: raw.contactPhone || "",
|
||||
contactEmail: raw.contactEmail || "",
|
||||
contactAddress: raw.contactAddress || "",
|
||||
registeredAddress: raw.registeredAddress || "",
|
||||
legalRepresentative: raw.legalRepresentative || "",
|
||||
creditCode: raw.creditCode || "",
|
||||
registeredCapitalText: raw.regCapital || raw.registeredCapital || "",
|
||||
countryName: raw.countryName || raw.country || "",
|
||||
isGroupInternal: true,
|
||||
isHongKongMacaoTaiwan: false,
|
||||
roleCode: PARTY_ROLE_OUR,
|
||||
roleName: "我方",
|
||||
};
|
||||
@@ -2172,9 +2189,9 @@ function validateSubmit() {
|
||||
// 以下为 pushContract 必填项补充校验(接口文档要求)
|
||||
if (detailState.value.isMajorContract === undefined || detailState.value.isMajorContract === null || detailState.value.isMajorContract === "")
|
||||
return "请选择是否重大合同";
|
||||
if (detailState.value.isPublicTender === undefined || detailState.value.isPublicTender === null)
|
||||
if (detailState.value.isPublicTender === undefined || detailState.value.isPublicTender === null || detailState.value.isPublicTender === "")
|
||||
return "请选择是否公开招标";
|
||||
if (detailState.value.isInvolveHongKongMacaoTaiwan === undefined || detailState.value.isInvolveHongKongMacaoTaiwan === null)
|
||||
if (detailState.value.isInvolveHongKongMacaoTaiwan === undefined || detailState.value.isInvolveHongKongMacaoTaiwan === null || detailState.value.isInvolveHongKongMacaoTaiwan === "")
|
||||
return "请选择是否涉及港澳台";
|
||||
if (!detailState.value.paymentDirection) return "请选择收支方向";
|
||||
if (!detailState.value.contractTypeCode) return "请选择合同类型编码";
|
||||
@@ -2376,12 +2393,12 @@ function buildLaunchRecord() {
|
||||
isElectron: detailState.value.isElectron || "1",
|
||||
isSingleAgreement: detailState.value.isSingleAgreement || "0",
|
||||
paymentDirection: detailState.value.paymentDirection || "0",
|
||||
isMajorContract: detailState.value.isMajorContract ? 1 : 0,
|
||||
isOpenBidding: detailState.value.isPublicTender ? 1 : 0,
|
||||
isYearBudget: detailState.value.isIncludedInCurrentYearBudget ? 1 : 0,
|
||||
isShare: detailState.value.isShared ? "1" : "0",
|
||||
contractNature: detailState.value.isForeignRelatedSubject ? "1" : "0",
|
||||
isSgat: detailState.value.isInvolveHongKongMacaoTaiwan ? "1" : "0",
|
||||
isMajorContract: detailState.value.isMajorContract,
|
||||
isOpenBidding: detailState.value.isPublicTender,
|
||||
isYearBudget: detailState.value.isIncludedInCurrentYearBudget,
|
||||
isShare: detailState.value.isShared,
|
||||
contractNature: detailState.value.isForeignRelatedSubject,
|
||||
isSgat: detailState.value.isInvolveHongKongMacaoTaiwan,
|
||||
biddingNo: detailState.value.isPublicTender
|
||||
? detailState.value.biddingNo || ""
|
||||
: "",
|
||||
|
||||
@@ -71,12 +71,34 @@
|
||||
v-for="field in launchFieldList"
|
||||
:key="field.key"
|
||||
:label="field.label"
|
||||
:required="!field.readonly"
|
||||
:required="field.required"
|
||||
:extra="field.remark || undefined"
|
||||
:class="{ 'grid-span-2': field.fieldType === 'rich_text' }"
|
||||
>
|
||||
<a-input
|
||||
<a-select
|
||||
v-if="field.fieldType === 'select'"
|
||||
:value="getLaunchFieldValue(field.key)"
|
||||
:placeholder="`请输入${field.label}`"
|
||||
:readonly="field.readonly"
|
||||
:options="getFieldDictOptions(field)"
|
||||
:placeholder="field.placeholder"
|
||||
:loading="isDictLoading(field.dictType)"
|
||||
:disabled="isViewMode"
|
||||
allow-clear
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@update:value="setLaunchFieldValue(field.key, $event)"
|
||||
/>
|
||||
<TemplateRichTextEditor
|
||||
v-else-if="field.fieldType === 'rich_text'"
|
||||
:model-value="getLaunchFieldValue(field.key)"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="isViewMode"
|
||||
@update:model-value="setLaunchFieldValue(field.key, $event)"
|
||||
/>
|
||||
<a-input
|
||||
v-else
|
||||
:value="getLaunchFieldValue(field.key)"
|
||||
:placeholder="field.placeholder"
|
||||
:readonly="isViewMode"
|
||||
@update:value="setLaunchFieldValue(field.key, $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
@@ -177,7 +199,6 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import Docxtemplater from "docxtemplater";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
FileWordOutlined,
|
||||
@@ -185,9 +206,9 @@ import {
|
||||
SettingOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import PizZip from "pizzip";
|
||||
import { renderAsync } from "docx-preview";
|
||||
import { saveAs } from "file-saver";
|
||||
import { getDictDataByType } from "@/api/dict.js";
|
||||
import {
|
||||
downloadTemplateFile,
|
||||
queryPublishedManifestList,
|
||||
@@ -197,6 +218,10 @@ import {
|
||||
buildTemplateFieldLabelMap,
|
||||
loadTemplateFieldList,
|
||||
} from "../../shared/templateFieldDict.js";
|
||||
import { renderTemplateDocxBlob } from "../../shared/templateDocxRenderer.js";
|
||||
import { buildTemplateLaunchFieldList } from "../../shared/templateVariableConfig.js";
|
||||
import { buildOurPartyAutoFillValues } from "../../shared/templateAutoFillMappings.js";
|
||||
import TemplateRichTextEditor from "./TemplateRichTextEditor.vue";
|
||||
import {
|
||||
extraAmountFieldOptions,
|
||||
} from "../contractLaunchOptions.js";
|
||||
@@ -293,6 +318,21 @@ function createParty(roleCode = PARTY_ROLE_OPPOSITE, index = 0) {
|
||||
: `相对方${index + 1}`,
|
||||
subjectCode: "",
|
||||
companyName: "",
|
||||
companyType: "",
|
||||
companyNature: "",
|
||||
orgNature: "",
|
||||
orgNatureLevel: "",
|
||||
isGroupInternal: false,
|
||||
isHongKongMacaoTaiwan: false,
|
||||
creditCode: "",
|
||||
registeredAddress: "",
|
||||
legalRepresentative: "",
|
||||
registeredCapitalText: "",
|
||||
contactName: "",
|
||||
contactPhone: "",
|
||||
contactEmail: "",
|
||||
contactAddress: "",
|
||||
countryName: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -311,6 +351,9 @@ function mapLaunchPartyToFormParty(item = {}) {
|
||||
oppositeId: item.oppositeId || "",
|
||||
companyName: item.companyName || "",
|
||||
companyType: item.companyType || "",
|
||||
companyNature: item.companyNature || item.oppositeCharacter || "",
|
||||
orgNature: item.orgNature || item.natureOfEnterpriseOrg || "",
|
||||
orgNatureLevel: item.orgNatureLevel || item.natureOfEnterpriseOrgLev || "",
|
||||
oppCharacter: item.oppositeCharacter || "",
|
||||
contactName: item.contactName || "",
|
||||
contactPhone: item.contactPhone || "",
|
||||
@@ -319,6 +362,9 @@ function mapLaunchPartyToFormParty(item = {}) {
|
||||
registeredAddress: item.registeredAddress || "",
|
||||
legalRepresentative: item.legalRepresentative || "",
|
||||
creditCode: item.creditCode || "",
|
||||
registeredCapitalText:
|
||||
item.registeredCapitalText || item.regCapital || item.registeredCapital || "",
|
||||
countryName: item.countryName || item.country || "",
|
||||
bankName: item.bankName || "",
|
||||
bankAccount: item.bankAccount || "",
|
||||
naturalPersonIdType: item.naturalPersonIdType || "",
|
||||
@@ -455,12 +501,12 @@ function createContractFormData() {
|
||||
isSingleAgreement: "0",
|
||||
|
||||
// 合同属性
|
||||
isInvolveHongKongMacaoTaiwan: false, // 是否涉及港澳台
|
||||
isPublicTender: false, // 是否公开招标
|
||||
isForeignRelatedSubject: false, // 签约主体是否涉外
|
||||
isIncludedInCurrentYearBudget: false, // 是否列入当年预算
|
||||
isShared: false, // 是否共享
|
||||
isMajorContract: false, // 是否重大合同
|
||||
isInvolveHongKongMacaoTaiwan: "0", // 是否涉及港澳台
|
||||
isPublicTender: "0", // 是否公开招标
|
||||
isForeignRelatedSubject: "0", // 签约主体是否涉外
|
||||
isIncludedInCurrentYearBudget: "0", // 是否列入当年预算
|
||||
isShared: "0", // 是否共享
|
||||
isMajorContract: "0", // 是否重大合同
|
||||
|
||||
// 额外金额
|
||||
extraAmounts: {
|
||||
@@ -599,16 +645,9 @@ function copyContractFormData(data = {}) {
|
||||
return form;
|
||||
}
|
||||
|
||||
function buildTemplateRenderData(templateFieldValues = {}) {
|
||||
const data = {};
|
||||
(templateFieldList.value || []).forEach((item) => {
|
||||
data[item.value] = templateFieldValues?.[item.value] ?? "";
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
const emit = defineEmits(["save"]);
|
||||
|
||||
const formRef = ref(null);
|
||||
const visible = ref(false);
|
||||
const previewRef = ref(null);
|
||||
const modalMode = ref("create");
|
||||
@@ -622,6 +661,8 @@ const previewBlob = ref(null);
|
||||
const manualBodyFileList = ref([]);
|
||||
const publishedTemplates = ref([]);
|
||||
const templateFieldList = ref([]);
|
||||
const dictOptionMap = ref({});
|
||||
const dictLoadingMap = ref({});
|
||||
|
||||
const templateBufferCache = new Map();
|
||||
let previewTimer = null;
|
||||
@@ -648,19 +689,10 @@ const templateFieldLabelMap = computed(() =>
|
||||
|
||||
const launchFieldList = computed(() => {
|
||||
if (creationMode.value === "manual") return [];
|
||||
const mappings = activeTemplate.value?.variableMappings || [];
|
||||
const keys = [];
|
||||
mappings.forEach((item) => {
|
||||
const fieldKey = item.bindField || item.variableKey;
|
||||
if (fieldKey && !keys.includes(fieldKey)) {
|
||||
keys.push(fieldKey);
|
||||
}
|
||||
});
|
||||
return keys.map((key) => ({
|
||||
key,
|
||||
label: templateFieldLabelMap.value[key] || key,
|
||||
readonly: false,
|
||||
}));
|
||||
return buildTemplateLaunchFieldList(
|
||||
activeTemplate.value?.variableMappings || [],
|
||||
templateFieldLabelMap.value,
|
||||
);
|
||||
});
|
||||
|
||||
function getLaunchFieldValue(fieldKey) {
|
||||
@@ -671,15 +703,111 @@ function setLaunchFieldValue(fieldKey, value) {
|
||||
if (!contractDetailState.value.templateFieldValues) {
|
||||
contractDetailState.value.templateFieldValues = {};
|
||||
}
|
||||
contractDetailState.value.templateFieldValues[fieldKey] = value;
|
||||
contractDetailState.value.templateFieldValues[fieldKey] = value ?? "";
|
||||
if (visible.value) {
|
||||
schedulePreview();
|
||||
}
|
||||
}
|
||||
|
||||
const templateVariables = computed(() =>
|
||||
buildTemplateRenderData(contractDetailState.value.templateFieldValues || {}),
|
||||
);
|
||||
function normalizeDictOptions(list = []) {
|
||||
return (list || [])
|
||||
.map((item) => ({
|
||||
label: item?.k || item?.label || "",
|
||||
value: item?.v || item?.value || "",
|
||||
remark: item?.remark || "",
|
||||
}))
|
||||
.filter((item) => item.label && item.value);
|
||||
}
|
||||
|
||||
async function loadDictOptionsByType(dictType) {
|
||||
const key = String(dictType || "").trim();
|
||||
if (!key) {
|
||||
return [];
|
||||
}
|
||||
if (dictOptionMap.value[key]?.length) {
|
||||
return dictOptionMap.value[key];
|
||||
}
|
||||
if (dictLoadingMap.value[key]) {
|
||||
return dictOptionMap.value[key] || [];
|
||||
}
|
||||
dictLoadingMap.value = { ...dictLoadingMap.value, [key]: true };
|
||||
try {
|
||||
const result = await getDictDataByType(key);
|
||||
const options = normalizeDictOptions(result);
|
||||
dictOptionMap.value = { ...dictOptionMap.value, [key]: options };
|
||||
return options;
|
||||
} finally {
|
||||
dictLoadingMap.value = { ...dictLoadingMap.value, [key]: false };
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCurrentTemplateDictOptions() {
|
||||
if (creationMode.value !== "template") {
|
||||
return;
|
||||
}
|
||||
const dictTypes = [...new Set(
|
||||
launchFieldList.value
|
||||
.filter((item) => item.fieldType === "select" && item.dictType)
|
||||
.map((item) => item.dictType),
|
||||
)];
|
||||
await Promise.all(dictTypes.map((item) => loadDictOptionsByType(item)));
|
||||
}
|
||||
|
||||
function getFieldDictOptions(field = {}) {
|
||||
return dictOptionMap.value[String(field.dictType || "").trim()] || [];
|
||||
}
|
||||
|
||||
function isDictLoading(dictType) {
|
||||
return !!dictLoadingMap.value[String(dictType || "").trim()];
|
||||
}
|
||||
|
||||
function getLaunchFieldByKey(fieldKey) {
|
||||
return launchFieldList.value.find((item) => item.key === fieldKey) || null;
|
||||
}
|
||||
|
||||
function getLaunchFieldDisplayValue(field = {}) {
|
||||
const rawValue = getLaunchFieldValue(field.key);
|
||||
if (field.fieldType !== "select") {
|
||||
return rawValue;
|
||||
}
|
||||
const option = getFieldDictOptions(field).find(
|
||||
(item) => String(item.value) === String(rawValue ?? ""),
|
||||
);
|
||||
return option?.label ?? rawValue ?? "";
|
||||
}
|
||||
|
||||
function stripRichTextContent(htmlValue) {
|
||||
return String(htmlValue || "")
|
||||
.replace(/<table[\s\S]*?<\/table>/gi, " [表格] ")
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/ /gi, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isLaunchFieldEmpty(field = {}, value) {
|
||||
if (value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (field.fieldType === "rich_text") {
|
||||
return !stripRichTextContent(value);
|
||||
}
|
||||
return String(value).trim() === "";
|
||||
}
|
||||
|
||||
function buildTemplateRenderFieldValues() {
|
||||
const values = {
|
||||
...(contractDetailState.value.templateFieldValues || {}),
|
||||
};
|
||||
launchFieldList.value.forEach((field) => {
|
||||
if (field.fieldType === "select") {
|
||||
values[field.key] = getLaunchFieldDisplayValue(field);
|
||||
}
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
const hasPreviewSource = computed(() => {
|
||||
return creationMode.value === "manual"
|
||||
@@ -710,9 +838,11 @@ watch(
|
||||
previewError.value = "";
|
||||
if (visible.value && creationMode.value === "template") {
|
||||
prefillFromMainFields();
|
||||
loadCurrentTemplateDictOptions().finally(() => {
|
||||
nextTick(() => {
|
||||
schedulePreview();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -721,15 +851,17 @@ watch(
|
||||
() => visible.value,
|
||||
(open) => {
|
||||
if (open) {
|
||||
loadCurrentTemplateDictOptions().finally(() => {
|
||||
nextTick(() => {
|
||||
refreshPreview();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => JSON.stringify(templateVariables.value),
|
||||
() => JSON.stringify(contractDetailState.value.templateFieldValues || {}),
|
||||
() => {
|
||||
if (visible.value) {
|
||||
schedulePreview();
|
||||
@@ -764,7 +896,9 @@ watch(
|
||||
previewBlob.value = null;
|
||||
previewError.value = "";
|
||||
if (visible.value) {
|
||||
loadCurrentTemplateDictOptions().finally(() => {
|
||||
schedulePreview();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -793,6 +927,22 @@ function loadTemplateFields() {
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => JSON.stringify(
|
||||
launchFieldList.value.map((item) => ({
|
||||
key: item.key,
|
||||
fieldType: item.fieldType,
|
||||
dictType: item.dictType,
|
||||
})),
|
||||
),
|
||||
() => {
|
||||
if (visible.value && creationMode.value === "template") {
|
||||
loadCurrentTemplateDictOptions();
|
||||
}
|
||||
},
|
||||
{ deep: false },
|
||||
);
|
||||
|
||||
function getManifestDetailLocal(templateId) {
|
||||
return (
|
||||
publishedTemplates.value.find((item) => item.id === templateId) || null
|
||||
@@ -869,6 +1019,8 @@ async function buildFilledDocumentBlob() {
|
||||
throw new Error("请选择合同模板");
|
||||
}
|
||||
|
||||
await loadCurrentTemplateDictOptions();
|
||||
|
||||
const buffer = activeTemplate.value.templateArrayBuffer
|
||||
? activeTemplate.value.templateArrayBuffer.slice(0)
|
||||
: await fetchTemplateBuffer(
|
||||
@@ -876,36 +1028,10 @@ async function buildFilledDocumentBlob() {
|
||||
activeTemplate.value.templateFileUrl ||
|
||||
activeTemplate.value.fileUrl,
|
||||
);
|
||||
const zip = new PizZip(buffer);
|
||||
const doc = new Docxtemplater(zip, {
|
||||
paragraphLoop: true,
|
||||
linebreaks: true,
|
||||
nullGetter(part) {
|
||||
return part?.raw ? `[${part.raw}]` : "";
|
||||
},
|
||||
});
|
||||
//这里是生成的测试数据
|
||||
const sourceValues = buildTemplateRenderData(
|
||||
contractDetailState.value.templateFieldValues || {},
|
||||
);
|
||||
const mappings = activeTemplate.value.variableMappings || [];
|
||||
|
||||
const renderData = {};
|
||||
if (mappings.length) {
|
||||
mappings.forEach((item) => {
|
||||
if (!item.variableKey) return;
|
||||
renderData[item.variableKey] = sourceValues[item.bindField] ?? "";
|
||||
});
|
||||
} else {
|
||||
Object.assign(renderData, sourceValues);
|
||||
}
|
||||
|
||||
doc.render(renderData);
|
||||
|
||||
return doc.getZip().generate({
|
||||
type: "blob",
|
||||
mimeType:
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
return await renderTemplateDocxBlob({
|
||||
templateArrayBuffer: buffer,
|
||||
variableMappings: activeTemplate.value.variableMappings || [],
|
||||
templateFieldValues: buildTemplateRenderFieldValues(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1048,12 +1174,11 @@ function validateMainForm() {
|
||||
return "请选择合同模板";
|
||||
|
||||
for (const field of launchFieldList.value) {
|
||||
if (field.readonly) {
|
||||
continue;
|
||||
}
|
||||
const value = getLaunchFieldValue(field.key);
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return `请输入${field.label}`;
|
||||
if (field.required && isLaunchFieldEmpty(field, value)) {
|
||||
return field.fieldType === "select"
|
||||
? `请选择${field.label}`
|
||||
: `请输入${field.label}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1129,6 +1254,7 @@ async function showModal(initialValues = {}, mode = "create") {
|
||||
|
||||
// 预填充:从主合同同名字段自动取值填入模板变量
|
||||
prefillFromMainFields(initialValues.detailState || initialValues);
|
||||
await loadCurrentTemplateDictOptions();
|
||||
|
||||
syncManualBodyFileList();
|
||||
previewBlob.value = null;
|
||||
@@ -1153,6 +1279,16 @@ function prefillFromMainFields(rawSource) {
|
||||
});
|
||||
|
||||
let changed = false;
|
||||
const ourPartyAutoFillValues = buildOurPartyAutoFillValues(contractDetailState.value);
|
||||
Object.entries(ourPartyAutoFillValues).forEach(([key, value]) => {
|
||||
if (!keysToFill.has(key)) return;
|
||||
if (values[key] !== undefined && values[key] !== null && values[key] !== "") return;
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
values[key] = String(value);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
keysToFill.forEach(key => {
|
||||
if (values[key] !== undefined && values[key] !== null && values[key] !== "") return;
|
||||
// 1. 先尝试从当前合同详情直接取(同名字段)
|
||||
@@ -1218,7 +1354,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.contract-template-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 460px minmax(0, 1fr);
|
||||
grid-template-columns: 560px minmax(0, 1fr);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -1274,6 +1410,15 @@ onBeforeUnmount(() => {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.grid-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.config-form :deep(.ant-select),
|
||||
.config-form :deep(.ant-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-tip {
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -0,0 +1,857 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="modalTitle"
|
||||
width="96%"
|
||||
:footer="null"
|
||||
centered
|
||||
:mask-closable="false"
|
||||
:destroy-on-close="false"
|
||||
wrap-class-name="seal-position-config-wrap"
|
||||
:bodyStyle="{ padding: 0, height: 'calc(100vh - 180px)', overflow: 'hidden' }"
|
||||
>
|
||||
<div class="seal-position-config-layout">
|
||||
<div ref="previewPaneRef" class="preview-pane">
|
||||
<div class="preview-toolbar">
|
||||
<a-space wrap>
|
||||
<a-tag color="blue">当前页:第 {{ currentPageNum }} 页</a-tag>
|
||||
<a-tag>签章位 {{ positions.length }} 个</a-tag>
|
||||
<a-button type="primary" :disabled="!pdfImages.length" @click="handleAddPosition">
|
||||
添加签章位
|
||||
</a-button>
|
||||
<a-button :disabled="!currentPosition" @click="handleDeleteCurrent">
|
||||
删除当前框
|
||||
</a-button>
|
||||
<a-button :loading="loading" @click="loadPageData(true)">刷新预览</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="preview-scroll">
|
||||
<div
|
||||
v-for="(imgUrl, pageIndex) in pdfImages"
|
||||
:key="`page_${pageIndex}`"
|
||||
class="pdf-page-card"
|
||||
:class="{ 'is-active': currentPageNum === pageIndex + 1 }"
|
||||
@click="currentPageNum = pageIndex + 1"
|
||||
>
|
||||
<div class="page-card-header">
|
||||
<span>第 {{ pageIndex + 1 }} 页</span>
|
||||
<a-tag v-if="currentPageNum === pageIndex + 1" color="processing">当前页</a-tag>
|
||||
</div>
|
||||
<div
|
||||
class="pdf-page-shell"
|
||||
:style="{ width: `${pageDisplayWidth}px`, height: `${pageDisplayHeight}px` }"
|
||||
>
|
||||
<img
|
||||
:src="imgUrl"
|
||||
class="pdf-page-image"
|
||||
:style="{ width: `${pageDisplayWidth}px`, height: `${pageDisplayHeight}px` }"
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-for="box in getPagePositions(pageIndex + 1)"
|
||||
:key="box.localId"
|
||||
class="seal-position-box"
|
||||
:class="{ 'is-selected': selectedLocalId === box.localId }"
|
||||
:style="getBoxStyle(box)"
|
||||
@click.stop="handleSelectBox(box.localId, box.pageNum)"
|
||||
@mousedown.stop.prevent="startDragBox($event, box)"
|
||||
>
|
||||
<div class="box-label-row">
|
||||
<span class="box-label">{{ box.label || '未命名签章位' }}</span>
|
||||
<span v-if="box.required === 'Y'" class="box-required">必签</span>
|
||||
</div>
|
||||
<div class="box-party">{{ getPartyLabel(box.partyId) }}</div>
|
||||
<div
|
||||
class="resize-handle"
|
||||
title="拖动调整大小"
|
||||
@mousedown.stop.prevent="startResizeBox($event, box)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-empty
|
||||
v-if="!loading && !pdfImages.length"
|
||||
class="empty-preview"
|
||||
description="暂无签署文件预览,请先确认水印文件是否已上传"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-pane">
|
||||
<div class="config-pane-scroll">
|
||||
<div class="side-card">
|
||||
<div class="side-card-title">合同信息</div>
|
||||
<div class="meta-item">
|
||||
<span>合同名称</span>
|
||||
<strong>{{ contractDetail?.contractName || '-' }}</strong>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span>合同状态</span>
|
||||
<strong>{{ contractDetail?.status || '-' }}</strong>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span>签章位配置</span>
|
||||
<strong>{{ positions.length ? '已配置' : '未配置' }}</strong>
|
||||
</div>
|
||||
<div class="meta-note">
|
||||
当前方案采用“当前合同单独配框”。框的位置以后会直接用于在线签订时的吸附和未签提醒。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-card">
|
||||
<div class="side-card-title">签章位属性</div>
|
||||
<template v-if="currentPosition">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="标签">
|
||||
<a-input
|
||||
v-model:value="currentPosition.label"
|
||||
allow-clear
|
||||
placeholder="例如:甲方公章位"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="签署方">
|
||||
<a-select
|
||||
v-model:value="currentPosition.partyId"
|
||||
:options="partyOptions"
|
||||
placeholder="请选择签署方"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否必签">
|
||||
<a-switch
|
||||
:checked="currentPosition.required === 'Y'"
|
||||
checked-children="必签"
|
||||
un-checked-children="可选"
|
||||
@change="(checked) => handleRequiredChange(currentPosition, checked)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所在页">
|
||||
<a-select
|
||||
v-model:value="currentPosition.pageNum"
|
||||
:options="pageOptions"
|
||||
placeholder="请选择页码"
|
||||
@change="(value) => handleBoxPageChange(currentPosition, value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="coordinate-grid">
|
||||
<a-form-item label="X 坐标">
|
||||
<a-input-number
|
||||
v-model:value="currentPosition.x"
|
||||
:min="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
@change="normalizeBox(currentPosition)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="Y 坐标">
|
||||
<a-input-number
|
||||
v-model:value="currentPosition.y"
|
||||
:min="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
@change="normalizeBox(currentPosition)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="宽度">
|
||||
<a-input-number
|
||||
v-model:value="currentPosition.width"
|
||||
:min="60"
|
||||
:step="5"
|
||||
style="width: 100%"
|
||||
@change="normalizeBox(currentPosition)"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="高度">
|
||||
<a-input-number
|
||||
v-model:value="currentPosition.height"
|
||||
:min="60"
|
||||
:step="5"
|
||||
style="width: 100%"
|
||||
@change="normalizeBox(currentPosition)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</a-form>
|
||||
</template>
|
||||
<a-empty v-else description="请先新增并选中一个签章位" />
|
||||
</div>
|
||||
|
||||
<div class="side-card side-card-grow">
|
||||
<div class="side-card-title">签章位列表({{ sortedPositions.length }})</div>
|
||||
<div class="position-list">
|
||||
<a-empty
|
||||
v-if="!sortedPositions.length"
|
||||
class="position-list-empty"
|
||||
description="暂无签章位,请先点击左侧“添加签章位”"
|
||||
/>
|
||||
<div
|
||||
v-for="box in sortedPositions"
|
||||
:key="box.localId"
|
||||
class="position-list-item"
|
||||
:class="{ 'is-selected': selectedLocalId === box.localId }"
|
||||
@click="handleSelectBox(box.localId, box.pageNum)"
|
||||
>
|
||||
<div>
|
||||
<div class="position-item-title">{{ box.label || '未命名签章位' }}</div>
|
||||
<div class="position-item-sub">
|
||||
第 {{ box.pageNum }} 页 · {{ getPartyLabel(box.partyId) }} · {{ box.required === 'Y' ? '必签' : '可选' }}
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="link" danger size="small" @click.stop="handleDeletePosition(box.localId)">
|
||||
删除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-actions">
|
||||
<a-space>
|
||||
<a-button @click="visible = false">取消</a-button>
|
||||
<a-button type="primary" :loading="saving" @click="handleSave">保存签章位</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { initLaunchSealPosition, saveLaunchSealPositions } from '@/api/launch'
|
||||
|
||||
const emit = defineEmits(['saved'])
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const previewPaneRef = ref(null)
|
||||
const contractDetail = ref(null)
|
||||
const pageData = ref(null)
|
||||
const positions = ref([])
|
||||
const selectedLocalId = ref('')
|
||||
const currentPageNum = ref(1)
|
||||
const pageDisplayWidth = ref(760)
|
||||
|
||||
const DEFAULT_IMAGE_WIDTH = 793
|
||||
const DEFAULT_IMAGE_HEIGHT = 1122
|
||||
const DEFAULT_BOX_WIDTH = 160
|
||||
const DEFAULT_BOX_HEIGHT = 160
|
||||
const MIN_BOX_SIZE = 60
|
||||
let localSeed = 0
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
const name = contractDetail.value?.contractName || '合同'
|
||||
return `配置签章位 - ${name}`
|
||||
})
|
||||
|
||||
const pdfImages = computed(() => normalizePdfImages(pageData.value?.pdfToImageList))
|
||||
const imageWidth = computed(() => Number(pageData.value?.imageWidth || DEFAULT_IMAGE_WIDTH))
|
||||
const imageHeight = computed(() => Number(pageData.value?.imageHeight || DEFAULT_IMAGE_HEIGHT))
|
||||
const scaleRatio = computed(() => pageDisplayWidth.value / imageWidth.value)
|
||||
const pageDisplayHeight = computed(() => Math.round(imageHeight.value * scaleRatio.value))
|
||||
const currentPosition = computed(() => positions.value.find((item) => item.localId === selectedLocalId.value) || null)
|
||||
|
||||
const sortedPositions = computed(() =>
|
||||
[...positions.value].sort((a, b) => {
|
||||
if (a.pageNum !== b.pageNum) {
|
||||
return Number(a.pageNum || 0) - Number(b.pageNum || 0)
|
||||
}
|
||||
return String(a.localId).localeCompare(String(b.localId))
|
||||
}),
|
||||
)
|
||||
|
||||
const partyOptions = computed(() =>
|
||||
(contractDetail.value?.partyList || []).map((item) => ({
|
||||
label: `${item.roleName || item.roleCode || '签署方'} - ${item.companyName || item.subjectCode || item.id}`,
|
||||
value: item.id,
|
||||
})),
|
||||
)
|
||||
|
||||
const pageOptions = computed(() =>
|
||||
pdfImages.value.map((_, index) => ({
|
||||
label: `第 ${index + 1} 页`,
|
||||
value: index + 1,
|
||||
})),
|
||||
)
|
||||
|
||||
function isSuccessResponse(res) {
|
||||
return String(res?.code || '') === '0000'
|
||||
}
|
||||
|
||||
function getResult(res) {
|
||||
return res?.result || null
|
||||
}
|
||||
|
||||
function normalizePdfImages(list = []) {
|
||||
return (Array.isArray(list) ? list : [])
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') return item
|
||||
if (!item || typeof item !== 'object') return ''
|
||||
return item.url || item.imageUrl || item.previewUrl || item.fileUrl || item.path || ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function createLocalId(id) {
|
||||
if (id != null) {
|
||||
return `db_${id}`
|
||||
}
|
||||
localSeed += 1
|
||||
return `tmp_${localSeed}`
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
loading.value = false
|
||||
saving.value = false
|
||||
pageData.value = null
|
||||
positions.value = []
|
||||
selectedLocalId.value = ''
|
||||
currentPageNum.value = 1
|
||||
pageDisplayWidth.value = 760
|
||||
}
|
||||
|
||||
function buildLocalPositions(page) {
|
||||
const rawPositions = Array.isArray(page?.positions) ? page.positions : []
|
||||
return rawPositions.map((item) => ({
|
||||
...item,
|
||||
localId: createLocalId(item.id),
|
||||
required: item.required === 'N' ? 'N' : 'Y',
|
||||
pageNum: Number(item.pageNum || 1),
|
||||
x: Number(item.x || 0),
|
||||
y: Number(item.y || 0),
|
||||
width: Number(item.width || DEFAULT_BOX_WIDTH),
|
||||
height: Number(item.height || DEFAULT_BOX_HEIGHT),
|
||||
}))
|
||||
}
|
||||
|
||||
function refreshPreviewSize() {
|
||||
nextTick(() => {
|
||||
const paneWidth = previewPaneRef.value?.clientWidth || 980
|
||||
const availableWidth = Math.max(360, paneWidth - 48)
|
||||
pageDisplayWidth.value = Math.min(imageWidth.value, availableWidth)
|
||||
})
|
||||
}
|
||||
|
||||
function handleRequiredChange(box, checked) {
|
||||
box.required = checked ? 'Y' : 'N'
|
||||
}
|
||||
|
||||
function handleSelectBox(localId, pageNum) {
|
||||
selectedLocalId.value = localId
|
||||
currentPageNum.value = Number(pageNum || 1)
|
||||
}
|
||||
|
||||
function getPagePositions(pageNum) {
|
||||
return positions.value.filter((item) => Number(item.pageNum) === Number(pageNum))
|
||||
}
|
||||
|
||||
function getBoxStyle(box) {
|
||||
return {
|
||||
left: `${Number(box.x || 0) * scaleRatio.value}px`,
|
||||
top: `${Number(box.y || 0) * scaleRatio.value}px`,
|
||||
width: `${Number(box.width || DEFAULT_BOX_WIDTH) * scaleRatio.value}px`,
|
||||
height: `${Number(box.height || DEFAULT_BOX_HEIGHT) * scaleRatio.value}px`,
|
||||
}
|
||||
}
|
||||
|
||||
function getPartyLabel(partyId) {
|
||||
const party = (contractDetail.value?.partyList || []).find((item) => Number(item.id) === Number(partyId))
|
||||
if (!party) {
|
||||
return '未绑定签署方'
|
||||
}
|
||||
return `${party.roleName || party.roleCode || '签署方'} - ${party.companyName || party.subjectCode || party.id}`
|
||||
}
|
||||
|
||||
function clampValue(value, min, max) {
|
||||
const numericValue = Number(value)
|
||||
if (Number.isNaN(numericValue)) return min
|
||||
return Math.min(Math.max(numericValue, min), max)
|
||||
}
|
||||
|
||||
function normalizeBox(box) {
|
||||
if (!box) return
|
||||
box.pageNum = clampValue(box.pageNum || 1, 1, Math.max(pdfImages.value.length, 1))
|
||||
box.width = clampValue(box.width || DEFAULT_BOX_WIDTH, MIN_BOX_SIZE, imageWidth.value)
|
||||
box.height = clampValue(box.height || DEFAULT_BOX_HEIGHT, MIN_BOX_SIZE, imageHeight.value)
|
||||
box.x = clampValue(box.x || 0, 0, Math.max(0, imageWidth.value - box.width))
|
||||
box.y = clampValue(box.y || 0, 0, Math.max(0, imageHeight.value - box.height))
|
||||
box.x = Math.round(box.x * 100) / 100
|
||||
box.y = Math.round(box.y * 100) / 100
|
||||
box.width = Math.round(box.width * 100) / 100
|
||||
box.height = Math.round(box.height * 100) / 100
|
||||
}
|
||||
|
||||
function handleBoxPageChange(box, pageNum) {
|
||||
box.pageNum = Number(pageNum || 1)
|
||||
currentPageNum.value = box.pageNum
|
||||
normalizeBox(box)
|
||||
}
|
||||
|
||||
function handleAddPosition() {
|
||||
if (!pdfImages.value.length) {
|
||||
message.warning('暂无可配置的签署文件预览')
|
||||
return
|
||||
}
|
||||
const partyId = currentPosition.value?.partyId ?? partyOptions.value[0]?.value
|
||||
const box = {
|
||||
id: null,
|
||||
localId: createLocalId(null),
|
||||
partyId,
|
||||
label: `签章位${positions.value.length + 1}`,
|
||||
required: 'Y',
|
||||
pageNum: currentPageNum.value,
|
||||
x: Math.max(0, Math.round((imageWidth.value - DEFAULT_BOX_WIDTH) / 2)),
|
||||
y: Math.max(0, Math.round((imageHeight.value - DEFAULT_BOX_HEIGHT) / 2)),
|
||||
width: DEFAULT_BOX_WIDTH,
|
||||
height: DEFAULT_BOX_HEIGHT,
|
||||
}
|
||||
positions.value.push(box)
|
||||
handleSelectBox(box.localId, box.pageNum)
|
||||
}
|
||||
|
||||
function handleDeletePosition(localId) {
|
||||
positions.value = positions.value.filter((item) => item.localId !== localId)
|
||||
if (selectedLocalId.value === localId) {
|
||||
const nextBox = positions.value[0] || null
|
||||
selectedLocalId.value = nextBox?.localId || ''
|
||||
currentPageNum.value = Number(nextBox?.pageNum || currentPageNum.value || 1)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteCurrent() {
|
||||
if (!currentPosition.value) return
|
||||
handleDeletePosition(currentPosition.value.localId)
|
||||
}
|
||||
|
||||
function startDragBox(event, box) {
|
||||
const startX = event.clientX
|
||||
const startY = event.clientY
|
||||
const originX = Number(box.x || 0)
|
||||
const originY = Number(box.y || 0)
|
||||
|
||||
const onMouseMove = (moveEvent) => {
|
||||
const deltaX = (moveEvent.clientX - startX) / scaleRatio.value
|
||||
const deltaY = (moveEvent.clientY - startY) / scaleRatio.value
|
||||
box.x = originX + deltaX
|
||||
box.y = originY + deltaY
|
||||
normalizeBox(box)
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
function startResizeBox(event, box) {
|
||||
const startX = event.clientX
|
||||
const startY = event.clientY
|
||||
const originWidth = Number(box.width || DEFAULT_BOX_WIDTH)
|
||||
const originHeight = Number(box.height || DEFAULT_BOX_HEIGHT)
|
||||
|
||||
const onMouseMove = (moveEvent) => {
|
||||
const deltaX = (moveEvent.clientX - startX) / scaleRatio.value
|
||||
const deltaY = (moveEvent.clientY - startY) / scaleRatio.value
|
||||
box.width = originWidth + deltaX
|
||||
box.height = originHeight + deltaY
|
||||
normalizeBox(box)
|
||||
}
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove)
|
||||
document.addEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
|
||||
async function loadPageData(showSuccessTip = false) {
|
||||
if (!contractDetail.value?.id) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await initLaunchSealPosition(contractDetail.value.id)
|
||||
if (!isSuccessResponse(res) || !getResult(res)) {
|
||||
message.error(res?.message || res?.msg || '初始化签署文件失败')
|
||||
return
|
||||
}
|
||||
const page = getResult(res)
|
||||
pageData.value = page
|
||||
positions.value = buildLocalPositions(page)
|
||||
if (positions.value.length > 0) {
|
||||
handleSelectBox(positions.value[0].localId, positions.value[0].pageNum)
|
||||
} else {
|
||||
selectedLocalId.value = ''
|
||||
currentPageNum.value = 1
|
||||
}
|
||||
refreshPreviewSize()
|
||||
if (showSuccessTip) {
|
||||
message.success('签署文件预览已刷新')
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error?.message || '初始化签署文件失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function validatePositions() {
|
||||
if (!positions.value.length) {
|
||||
return null
|
||||
}
|
||||
const invalid = positions.value.find(
|
||||
(item) => !item.partyId || !String(item.label || '').trim() || !item.pageNum,
|
||||
)
|
||||
if (invalid) {
|
||||
handleSelectBox(invalid.localId, invalid.pageNum)
|
||||
return '请完整填写签章位标签、签署方和页码'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const validationMessage = validatePositions()
|
||||
if (validationMessage) {
|
||||
message.warning(validationMessage)
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
positions: positions.value.map((item) => {
|
||||
normalizeBox(item)
|
||||
return {
|
||||
partyId: item.partyId,
|
||||
label: String(item.label || '').trim(),
|
||||
required: item.required === 'N' ? 'N' : 'Y',
|
||||
pageNum: Number(item.pageNum || 1),
|
||||
x: Number(item.x || 0),
|
||||
y: Number(item.y || 0),
|
||||
width: Number(item.width || DEFAULT_BOX_WIDTH),
|
||||
height: Number(item.height || DEFAULT_BOX_HEIGHT),
|
||||
}
|
||||
}),
|
||||
}
|
||||
const res = await saveLaunchSealPositions(contractDetail.value.id, payload)
|
||||
if (!isSuccessResponse(res) || !getResult(res)) {
|
||||
message.error(res?.message || res?.msg || '保存签章位失败')
|
||||
return
|
||||
}
|
||||
pageData.value = getResult(res)
|
||||
positions.value = buildLocalPositions(pageData.value)
|
||||
if (positions.value.length > 0) {
|
||||
handleSelectBox(positions.value[0].localId, positions.value[0].pageNum)
|
||||
}
|
||||
emit('saved', pageData.value)
|
||||
message.success('签章位已保存')
|
||||
} catch (error) {
|
||||
message.error(error?.message || '保存签章位失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function showModal(detail) {
|
||||
resetState()
|
||||
contractDetail.value = detail ? { ...detail } : null
|
||||
visible.value = true
|
||||
await nextTick()
|
||||
refreshPreviewSize()
|
||||
await loadPageData(false)
|
||||
}
|
||||
|
||||
function handleWindowResize() {
|
||||
if (visible.value) {
|
||||
refreshPreviewSize()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
})
|
||||
|
||||
defineExpose({ showModal })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.seal-position-config-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: #f4f7fb;
|
||||
}
|
||||
|
||||
.preview-pane {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.preview-toolbar {
|
||||
padding: 14px 18px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.preview-scroll {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.pdf-page-card {
|
||||
margin: 0 auto 18px;
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #dbe4f0;
|
||||
background: #ffffff;
|
||||
width: fit-content;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.pdf-page-card.is-active {
|
||||
border-color: #60a5fa;
|
||||
box-shadow: 0 16px 30px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.page-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pdf-page-shell {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.pdf-page-image {
|
||||
display: block;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.seal-position-box {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
border: 2px dashed #2563eb;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
color: #1d4ed8;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.seal-position-box.is-selected {
|
||||
border-color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.14);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.box-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.box-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.box-required {
|
||||
flex: 0 0 auto;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.box-party {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: inherit;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
background: currentColor;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.config-pane {
|
||||
width: 380px;
|
||||
flex: 0 0 380px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #f5f8fc 100%);
|
||||
}
|
||||
|
||||
.config-pane-scroll {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.side-card {
|
||||
border-radius: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.side-card-grow {
|
||||
flex: 1 1 auto;
|
||||
min-height: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.side-card-title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.meta-item strong {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
color: #0f172a;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.meta-note {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: #eff6ff;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.coordinate-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 12px;
|
||||
}
|
||||
|
||||
.position-list {
|
||||
flex: 1 1 auto;
|
||||
min-height: 160px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.position-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.position-list-item:hover,
|
||||
.position-list-item.is-selected {
|
||||
border-color: #60a5fa;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.position-item-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.position-item-sub {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.position-list-empty {
|
||||
margin: auto 0;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 4px;
|
||||
background: linear-gradient(180deg, rgba(245, 248, 252, 0) 0%, rgba(245, 248, 252, 0.96) 24%, rgba(245, 248, 252, 1) 100%);
|
||||
}
|
||||
|
||||
.empty-preview {
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.seal-position-config-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-pane {
|
||||
width: 100%;
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,991 @@
|
||||
<template>
|
||||
<div class="template-rich-editor" :class="{ 'is-disabled': disabled }">
|
||||
<div class="editor-toolbar">
|
||||
<div class="toolbar-groups">
|
||||
<div v-for="(group, groupIndex) in toolbarGroups" :key="groupIndex" class="toolbar-group">
|
||||
<button
|
||||
v-for="action in group"
|
||||
:key="action.key"
|
||||
type="button"
|
||||
class="toolbar-button"
|
||||
:disabled="disabled"
|
||||
@mousedown.prevent
|
||||
@click="handleToolbarAction(action)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-button toolbar-button-primary"
|
||||
@mousedown.prevent
|
||||
@click="openFullscreen"
|
||||
>
|
||||
{{ disabled ? '宽屏查看' : '宽屏编辑' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-content-shell">
|
||||
<div class="editor-scroll">
|
||||
<div
|
||||
ref="inlineEditorRef"
|
||||
class="editor-surface"
|
||||
:class="{ 'has-table': containsTable }"
|
||||
:contenteditable="String(!disabled)"
|
||||
:data-placeholder="placeholder"
|
||||
@focus="setActiveSurface('inline')"
|
||||
@mouseup="captureSelection('inline')"
|
||||
@keyup="captureSelection('inline')"
|
||||
@input="handleEditorInput('inline')"
|
||||
@paste="handlePaste($event, 'inline')"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-tip">
|
||||
复制 Word 表格时会优先保留原始合并结构。表格较宽或包含合并单元格时,使用“宽屏编辑”可避免内容被压缩成竖排。
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
:open="isFullscreen"
|
||||
:title="disabled ? '富文本宽屏查看' : '富文本宽屏编辑'"
|
||||
width="96vw"
|
||||
:footer="null"
|
||||
centered
|
||||
:mask-closable="false"
|
||||
:body-style="{ padding: '0', height: '82vh', overflow: 'hidden' }"
|
||||
@update:open="handleFullscreenVisibleChange"
|
||||
>
|
||||
<div class="fullscreen-layout">
|
||||
<div class="editor-toolbar fullscreen-toolbar">
|
||||
<div class="toolbar-groups">
|
||||
<div v-for="(group, groupIndex) in toolbarGroups" :key="`fullscreen-${groupIndex}`" class="toolbar-group">
|
||||
<button
|
||||
v-for="action in group"
|
||||
:key="`fullscreen-${action.key}`"
|
||||
type="button"
|
||||
class="toolbar-button"
|
||||
:disabled="disabled"
|
||||
@mousedown.prevent
|
||||
@click="handleToolbarAction(action, 'fullscreen')"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="toolbar-button toolbar-button-primary"
|
||||
@mousedown.prevent
|
||||
@click="isFullscreen = false"
|
||||
>
|
||||
完成
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="fullscreen-body">
|
||||
<div class="editor-scroll fullscreen-scroll">
|
||||
<div
|
||||
ref="fullscreenEditorRef"
|
||||
class="editor-surface fullscreen-surface"
|
||||
:class="{ 'has-table': containsTable }"
|
||||
:contenteditable="String(!disabled)"
|
||||
:data-placeholder="placeholder"
|
||||
@focus="setActiveSurface('fullscreen')"
|
||||
@mouseup="captureSelection('fullscreen')"
|
||||
@keyup="captureSelection('fullscreen')"
|
||||
@input="handleEditorInput('fullscreen')"
|
||||
@paste="handlePaste($event, 'fullscreen')"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入内容,支持插入表格',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const inlineEditorRef = ref(null)
|
||||
const fullscreenEditorRef = ref(null)
|
||||
const isFullscreen = ref(false)
|
||||
const activeSurface = ref('inline')
|
||||
const currentHtml = ref('')
|
||||
|
||||
const TABLE_STYLE_PROPS = [
|
||||
'width',
|
||||
'min-width',
|
||||
'max-width',
|
||||
'text-align',
|
||||
'vertical-align',
|
||||
'background-color',
|
||||
'color',
|
||||
'font-weight',
|
||||
'font-style',
|
||||
'text-decoration',
|
||||
'white-space',
|
||||
'border',
|
||||
'border-top',
|
||||
'border-right',
|
||||
'border-bottom',
|
||||
'border-left',
|
||||
'border-collapse',
|
||||
'table-layout',
|
||||
]
|
||||
|
||||
const INLINE_STYLE_PROPS = [
|
||||
'text-align',
|
||||
'background-color',
|
||||
'color',
|
||||
'font-weight',
|
||||
'font-style',
|
||||
'text-decoration',
|
||||
'white-space',
|
||||
]
|
||||
|
||||
const toolbarGroups = [
|
||||
[
|
||||
{ key: 'bold', label: '加粗', command: 'bold' },
|
||||
{ key: 'italic', label: '斜体', command: 'italic' },
|
||||
{ key: 'underline', label: '下划线', command: 'underline' },
|
||||
{ key: 'removeFormat', label: '清除格式', command: 'removeFormat' },
|
||||
],
|
||||
[
|
||||
{ key: 'justifyLeft', label: '左对齐', command: 'justifyLeft' },
|
||||
{ key: 'justifyCenter', label: '居中', command: 'justifyCenter' },
|
||||
{ key: 'justifyRight', label: '右对齐', command: 'justifyRight' },
|
||||
],
|
||||
[
|
||||
{ key: 'insertUnorderedList', label: '无序列表', command: 'insertUnorderedList' },
|
||||
{ key: 'insertOrderedList', label: '有序列表', command: 'insertOrderedList' },
|
||||
],
|
||||
// [
|
||||
// { key: 'insertTable', label: '插入表格', type: 'insertTable' },
|
||||
// { key: 'undo', label: '撤销', command: 'undo' },
|
||||
// { key: 'redo', label: '重做', command: 'redo' },
|
||||
// ],
|
||||
]
|
||||
|
||||
const savedRanges = {
|
||||
inline: null,
|
||||
fullscreen: null,
|
||||
}
|
||||
|
||||
const containsTable = computed(() => /<table[\s>]/i.test(currentHtml.value))
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const nextHtml = String(value || '')
|
||||
if (nextHtml === currentHtml.value) {
|
||||
syncEditorHtml()
|
||||
return
|
||||
}
|
||||
currentHtml.value = nextHtml
|
||||
syncEditorHtml()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(isFullscreen, async (value) => {
|
||||
if (!value) {
|
||||
activeSurface.value = 'inline'
|
||||
await nextTick()
|
||||
syncEditorHtml()
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
syncEditorHtml()
|
||||
if (!props.disabled) {
|
||||
focusSurface('fullscreen', true)
|
||||
}
|
||||
})
|
||||
|
||||
function getEditorElement(surface = activeSurface.value) {
|
||||
return surface === 'fullscreen' ? fullscreenEditorRef.value : inlineEditorRef.value
|
||||
}
|
||||
|
||||
function syncEditorHtml(skipSurface = '') {
|
||||
syncSurfaceHtml('inline', skipSurface)
|
||||
syncSurfaceHtml('fullscreen', skipSurface)
|
||||
}
|
||||
|
||||
function syncSurfaceHtml(surface, skipSurface = '') {
|
||||
if (surface === skipSurface) {
|
||||
return
|
||||
}
|
||||
const editor = getEditorElement(surface)
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
const html = currentHtml.value || ''
|
||||
if (editor.innerHTML !== html) {
|
||||
editor.innerHTML = html
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveSurface(surface) {
|
||||
activeSurface.value = surface
|
||||
}
|
||||
|
||||
function focusSurface(surface, placeAtEnd = false) {
|
||||
const editor = getEditorElement(surface)
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
editor.focus()
|
||||
if (placeAtEnd) {
|
||||
placeCaretAtEnd(editor)
|
||||
captureSelection(surface)
|
||||
}
|
||||
}
|
||||
|
||||
function placeCaretAtEnd(editor) {
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
const selection = window.getSelection()
|
||||
if (!selection) {
|
||||
return
|
||||
}
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(editor)
|
||||
range.collapse(false)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
|
||||
function captureSelection(surface = activeSurface.value) {
|
||||
const editor = getEditorElement(surface)
|
||||
const selection = window.getSelection()
|
||||
if (!editor || !selection || !selection.rangeCount) {
|
||||
return
|
||||
}
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!editor.contains(range.commonAncestorContainer)) {
|
||||
return
|
||||
}
|
||||
savedRanges[surface] = range.cloneRange()
|
||||
}
|
||||
|
||||
function restoreSelection(surface = activeSurface.value) {
|
||||
const editor = getEditorElement(surface)
|
||||
if (!editor) {
|
||||
return false
|
||||
}
|
||||
editor.focus()
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (!selection) {
|
||||
return false
|
||||
}
|
||||
|
||||
const range = savedRanges[surface]
|
||||
selection.removeAllRanges()
|
||||
if (range && editor.contains(range.commonAncestorContainer)) {
|
||||
selection.addRange(range)
|
||||
return true
|
||||
}
|
||||
|
||||
placeCaretAtEnd(editor)
|
||||
return true
|
||||
}
|
||||
|
||||
function handleEditorInput(surface) {
|
||||
const editor = getEditorElement(surface)
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
activeSurface.value = surface
|
||||
currentHtml.value = normalizeEditableHtml(editor.innerHTML)
|
||||
syncEditorHtml(surface)
|
||||
emit('update:modelValue', currentHtml.value)
|
||||
captureSelection(surface)
|
||||
}
|
||||
|
||||
function normalizeEditableHtml(html) {
|
||||
const normalized = String(html || '')
|
||||
.replace(/<div><br\s*\/?><\/div>/gi, '')
|
||||
.replace(/<p><br\s*\/?><\/p>/gi, '')
|
||||
.trim()
|
||||
return hasMeaningfulHtml(normalized) ? normalized : ''
|
||||
}
|
||||
|
||||
function hasMeaningfulHtml(html) {
|
||||
if (!html) {
|
||||
return false
|
||||
}
|
||||
if (/<(table|img|hr|video|audio)[\s>]/i.test(html)) {
|
||||
return true
|
||||
}
|
||||
return normalizeTextContent(html) !== ''
|
||||
}
|
||||
|
||||
function normalizeTextContent(html) {
|
||||
return String(html || '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function handleToolbarAction(action, targetSurface = '') {
|
||||
if (!action) {
|
||||
return
|
||||
}
|
||||
if (action.type === 'insertTable') {
|
||||
insertDefaultTable(targetSurface)
|
||||
return
|
||||
}
|
||||
if (['justifyLeft', 'justifyCenter', 'justifyRight'].includes(action.command)) {
|
||||
applyTextAlignment(action.command, targetSurface)
|
||||
return
|
||||
}
|
||||
runDocumentCommand(action.command, action.value, targetSurface)
|
||||
}
|
||||
|
||||
function resolveTargetSurface(targetSurface = '') {
|
||||
if (targetSurface) {
|
||||
return targetSurface
|
||||
}
|
||||
return isFullscreen.value ? 'fullscreen' : activeSurface.value || 'inline'
|
||||
}
|
||||
|
||||
function runDocumentCommand(command, value = null, targetSurface = '') {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
const surface = resolveTargetSurface(targetSurface)
|
||||
activeSurface.value = surface
|
||||
restoreSelection(surface)
|
||||
document.execCommand(command, false, value)
|
||||
captureSelection(surface)
|
||||
handleEditorInput(surface)
|
||||
}
|
||||
|
||||
function insertDefaultTable(targetSurface = '') {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
const html = buildDefaultTableHtml(4, 4)
|
||||
insertHtmlAtCursor(html, targetSurface)
|
||||
}
|
||||
|
||||
function insertHtmlAtCursor(html, targetSurface = '') {
|
||||
const surface = resolveTargetSurface(targetSurface)
|
||||
activeSurface.value = surface
|
||||
restoreSelection(surface)
|
||||
const inserted = document.execCommand('insertHTML', false, html)
|
||||
if (!inserted) {
|
||||
const editor = getEditorElement(surface)
|
||||
insertHtmlByRange(editor, html)
|
||||
}
|
||||
if (/<table[\s>]/i.test(html)) {
|
||||
focusFirstTableCell(surface)
|
||||
}
|
||||
captureSelection(surface)
|
||||
handleEditorInput(surface)
|
||||
}
|
||||
|
||||
function insertHtmlByRange(editor, html) {
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
const selection = window.getSelection()
|
||||
if (!selection || !selection.rangeCount) {
|
||||
editor.insertAdjacentHTML('beforeend', html)
|
||||
placeCaretAtEnd(editor)
|
||||
return
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0)
|
||||
if (!editor.contains(range.commonAncestorContainer)) {
|
||||
editor.insertAdjacentHTML('beforeend', html)
|
||||
placeCaretAtEnd(editor)
|
||||
return
|
||||
}
|
||||
|
||||
range.deleteContents()
|
||||
const fragment = range.createContextualFragment(html)
|
||||
const lastNode = fragment.lastChild
|
||||
range.insertNode(fragment)
|
||||
|
||||
if (lastNode) {
|
||||
const nextRange = document.createRange()
|
||||
nextRange.setStartAfter(lastNode)
|
||||
nextRange.collapse(true)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(nextRange)
|
||||
}
|
||||
}
|
||||
|
||||
function buildDefaultTableHtml(rows = 4, cols = 4) {
|
||||
const minWidth = Math.max(640, cols * 140)
|
||||
const rowHtml = Array.from({ length: rows }, () => {
|
||||
const cellHtml = Array.from({ length: cols }, () => '<td> </td>').join('')
|
||||
return `<tr>${cellHtml}</tr>`
|
||||
}).join('')
|
||||
return `<table style="width:100%;min-width:${minWidth}px;border-collapse:collapse;table-layout:auto;margin:8px 0;border:1px solid #1f2937;">${rowHtml}</table><p><br></p>`
|
||||
}
|
||||
|
||||
function handlePaste(event, surface) {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const html = event?.clipboardData?.getData('text/html') || ''
|
||||
const plainText = event?.clipboardData?.getData('text/plain') || ''
|
||||
const normalizedHtml = html ? normalizePastedHtml(html) : ''
|
||||
const fallbackTableHtml = !normalizedHtml ? buildPlainTextTableHtml(plainText) : ''
|
||||
|
||||
if (!normalizedHtml && !fallbackTableHtml) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
activeSurface.value = surface
|
||||
insertHtmlAtCursor(normalizedHtml || fallbackTableHtml, surface)
|
||||
}
|
||||
|
||||
function normalizePastedHtml(html) {
|
||||
const parser = new window.DOMParser()
|
||||
const doc = parser.parseFromString(`<html><body>${html}</body></html>`, 'text/html')
|
||||
const body = doc.body
|
||||
if (!body) {
|
||||
return ''
|
||||
}
|
||||
|
||||
body.querySelectorAll('script, style, meta, link, title').forEach((node) => node.remove())
|
||||
body.querySelectorAll('table').forEach((table) => normalizePastedTable(table, doc))
|
||||
body.querySelectorAll('*').forEach((node) => cleanupPastedNode(node))
|
||||
|
||||
return body.innerHTML.trim()
|
||||
}
|
||||
|
||||
function normalizePastedTable(table, doc) {
|
||||
const normalizedTable = doc.createElement('table')
|
||||
const columnCount = estimateTableColumnCount(table)
|
||||
const tableWidth = normalizeTableWidth(table) || '100%'
|
||||
normalizedTable.style.width = tableWidth
|
||||
normalizedTable.style.minWidth = `${Math.max(640, columnCount * 140)}px`
|
||||
normalizedTable.style.borderCollapse = 'collapse'
|
||||
normalizedTable.style.tableLayout = 'auto'
|
||||
normalizedTable.style.margin = '8px 0'
|
||||
|
||||
cloneNormalizedColGroups(table, normalizedTable, doc)
|
||||
|
||||
extractPastedTableRows(table).forEach((row) => {
|
||||
const normalizedRow = doc.createElement('tr')
|
||||
Array.from(row.children || []).forEach((cell) => {
|
||||
const tag = String(cell.tagName || '').toLowerCase() === 'th' ? 'th' : 'td'
|
||||
const normalizedCell = doc.createElement(tag)
|
||||
copyTableSpanAttribute(cell, normalizedCell, 'rowspan')
|
||||
copyTableSpanAttribute(cell, normalizedCell, 'colspan')
|
||||
applyNormalizedCellStyle(cell, normalizedCell)
|
||||
normalizedCell.innerHTML = sanitizeCellInnerHtml(cell.innerHTML)
|
||||
normalizedRow.appendChild(normalizedCell)
|
||||
})
|
||||
normalizedTable.appendChild(normalizedRow)
|
||||
})
|
||||
|
||||
table.replaceWith(normalizedTable)
|
||||
}
|
||||
|
||||
function cloneNormalizedColGroups(sourceTable, targetTable, doc) {
|
||||
Array.from(sourceTable.querySelectorAll(':scope > colgroup')).forEach((group) => {
|
||||
const normalizedGroup = doc.createElement('colgroup')
|
||||
Array.from(group.children || []).forEach((child) => {
|
||||
if (String(child.tagName || '').toLowerCase() !== 'col') {
|
||||
return
|
||||
}
|
||||
const normalizedCol = doc.createElement('col')
|
||||
const width = normalizeSizeValue(child.style?.width || child.getAttribute('width') || '')
|
||||
if (width) {
|
||||
normalizedCol.style.width = width
|
||||
}
|
||||
normalizedGroup.appendChild(normalizedCol)
|
||||
})
|
||||
if (normalizedGroup.children.length) {
|
||||
targetTable.appendChild(normalizedGroup)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function estimateTableColumnCount(table) {
|
||||
return extractPastedTableRows(table).reduce((maxCount, row) => {
|
||||
const columnCount = Array.from(row.children || []).reduce((sum, cell) => {
|
||||
const colspan = Number.parseInt(String(cell.getAttribute('colspan') || '').trim(), 10)
|
||||
return sum + (Number.isFinite(colspan) && colspan > 1 ? colspan : 1)
|
||||
}, 0)
|
||||
return Math.max(maxCount, columnCount)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function extractPastedTableRows(table) {
|
||||
const rows = Array.from(table.querySelectorAll(':scope > tr'))
|
||||
if (rows.length) {
|
||||
return rows
|
||||
}
|
||||
|
||||
const sectionRows = ['thead', 'tbody', 'tfoot'].flatMap((tagName) =>
|
||||
Array.from(table.querySelectorAll(`:scope > ${tagName} > tr`)),
|
||||
)
|
||||
if (sectionRows.length) {
|
||||
return sectionRows
|
||||
}
|
||||
|
||||
return Array.from(table.querySelectorAll('tr'))
|
||||
}
|
||||
|
||||
function normalizeTableWidth(table) {
|
||||
const inlineWidth = table.style?.width || table.getAttribute('width') || ''
|
||||
return normalizeSizeValue(inlineWidth) || '100%'
|
||||
}
|
||||
|
||||
function normalizeSizeValue(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
if (/^\d+(\.\d+)?$/.test(normalized)) {
|
||||
return `${normalized}px`
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
function copyTableSpanAttribute(sourceCell, targetCell, attrName) {
|
||||
const rawValue = sourceCell.getAttribute(attrName)
|
||||
const spanValue = Number.parseInt(String(rawValue || '').trim(), 10)
|
||||
if (Number.isFinite(spanValue) && spanValue > 1) {
|
||||
targetCell.setAttribute(attrName, String(spanValue))
|
||||
}
|
||||
}
|
||||
|
||||
function applyNormalizedCellStyle(sourceCell, targetCell) {
|
||||
const textAlign = sourceCell.style?.textAlign || sourceCell.getAttribute('align') || ''
|
||||
const verticalAlign = sourceCell.style?.verticalAlign || sourceCell.getAttribute('valign') || ''
|
||||
const backgroundColor = sourceCell.style?.backgroundColor || sourceCell.getAttribute('bgcolor') || ''
|
||||
const width = normalizeSizeValue(sourceCell.style?.width || sourceCell.getAttribute('width') || '')
|
||||
|
||||
targetCell.style.border = '1px solid #1f2937'
|
||||
targetCell.style.padding = '6px 10px'
|
||||
targetCell.style.verticalAlign = normalizeVerticalAlign(verticalAlign) || 'middle'
|
||||
targetCell.style.wordBreak = 'keep-all'
|
||||
targetCell.style.overflowWrap = 'break-word'
|
||||
targetCell.style.whiteSpace = 'pre-wrap'
|
||||
targetCell.style.lineHeight = '1.7'
|
||||
targetCell.style.minWidth = '120px'
|
||||
|
||||
if (textAlign) {
|
||||
targetCell.style.textAlign = textAlign
|
||||
}
|
||||
if (backgroundColor) {
|
||||
targetCell.style.backgroundColor = backgroundColor
|
||||
}
|
||||
if (width) {
|
||||
targetCell.style.width = width
|
||||
}
|
||||
|
||||
if (String(sourceCell.tagName || '').toLowerCase() === 'th') {
|
||||
targetCell.style.fontWeight = '700'
|
||||
targetCell.style.textAlign = targetCell.style.textAlign || 'center'
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeCellInnerHtml(html) {
|
||||
const parser = new window.DOMParser()
|
||||
const doc = parser.parseFromString(`<div>${html || ''}</div>`, 'text/html')
|
||||
const wrapper = doc.body.firstElementChild
|
||||
if (!wrapper) {
|
||||
return ''
|
||||
}
|
||||
|
||||
wrapper.querySelectorAll('script, style, meta, link, title').forEach((node) => node.remove())
|
||||
Array.from(wrapper.querySelectorAll('*')).forEach((node) => {
|
||||
cleanupPastedNode(node)
|
||||
if (String(node.tagName || '').toLowerCase() === 'table') {
|
||||
normalizePastedTable(node, doc)
|
||||
}
|
||||
})
|
||||
return wrapper.innerHTML
|
||||
}
|
||||
|
||||
function cleanupPastedNode(node) {
|
||||
if (!(node instanceof window.HTMLElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
node.removeAttribute('class')
|
||||
node.removeAttribute('lang')
|
||||
node.removeAttribute('dir')
|
||||
node.removeAttribute('data-mce-style')
|
||||
node.removeAttribute('data-mce-selected')
|
||||
|
||||
const tag = String(node.tagName || '').toLowerCase()
|
||||
if (tag === 'table' || tag === 'td' || tag === 'th' || tag === 'tr' || tag === 'col' || tag === 'colgroup') {
|
||||
retainInlineStyle(node, TABLE_STYLE_PROPS)
|
||||
return
|
||||
}
|
||||
|
||||
retainInlineStyle(node, INLINE_STYLE_PROPS)
|
||||
}
|
||||
|
||||
function retainInlineStyle(node, allowedProperties = []) {
|
||||
const preservedEntries = allowedProperties
|
||||
.map((propertyName) => {
|
||||
const value = node.style?.getPropertyValue(propertyName) || ''
|
||||
return value ? `${propertyName}:${value}` : ''
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (preservedEntries.length) {
|
||||
node.setAttribute('style', preservedEntries.join(';'))
|
||||
} else {
|
||||
node.removeAttribute('style')
|
||||
}
|
||||
}
|
||||
|
||||
function buildPlainTextTableHtml(text) {
|
||||
const normalizedText = String(text || '').replace(/\r/g, '')
|
||||
if (!normalizedText.includes('\t')) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const rows = normalizedText
|
||||
.split('\n')
|
||||
.map((line) => line.split('\t'))
|
||||
.filter((cells) => cells.some((cell) => String(cell || '').trim() !== ''))
|
||||
|
||||
const columnCount = rows.reduce((maxCount, row) => Math.max(maxCount, row.length), 0)
|
||||
if (rows.length < 2 || columnCount < 2) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const minWidth = Math.max(640, columnCount * 140)
|
||||
const rowHtml = rows
|
||||
.map((cells) => {
|
||||
const cellHtml = Array.from({ length: columnCount }, (_, index) => {
|
||||
const value = escapeHtml(String(cells[index] || '').trim())
|
||||
return `<td>${value || ' '}</td>`
|
||||
}).join('')
|
||||
return `<tr>${cellHtml}</tr>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return `<table style="width:100%;min-width:${minWidth}px;border-collapse:collapse;table-layout:auto;margin:8px 0;border:1px solid #1f2937;">${rowHtml}</table><p><br></p>`
|
||||
}
|
||||
|
||||
function applyTextAlignment(command, targetSurface = '') {
|
||||
if (props.disabled) {
|
||||
return
|
||||
}
|
||||
const surface = resolveTargetSurface(targetSurface)
|
||||
activeSurface.value = surface
|
||||
restoreSelection(surface)
|
||||
const editor = getEditorElement(surface)
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
const alignValue = resolveAlignValue(command)
|
||||
if (!alignValue) {
|
||||
return
|
||||
}
|
||||
const selection = window.getSelection()
|
||||
if (!selection || !selection.rangeCount) {
|
||||
editor.style.textAlign = alignValue
|
||||
handleEditorInput(surface)
|
||||
return
|
||||
}
|
||||
const range = selection.getRangeAt(0)
|
||||
const block = findAlignmentTarget(range.startContainer, editor)
|
||||
;(block || editor).style.textAlign = alignValue
|
||||
captureSelection(surface)
|
||||
handleEditorInput(surface)
|
||||
}
|
||||
|
||||
function resolveAlignValue(command) {
|
||||
if (command === 'justifyLeft') return 'left'
|
||||
if (command === 'justifyCenter') return 'center'
|
||||
if (command === 'justifyRight') return 'right'
|
||||
return ''
|
||||
}
|
||||
|
||||
function findAlignmentTarget(node, editor) {
|
||||
let current = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement || null
|
||||
while (current && current !== editor) {
|
||||
const tag = String(current.tagName || '').toLowerCase()
|
||||
if (['p', 'div', 'li', 'td', 'th'].includes(tag)) {
|
||||
return current
|
||||
}
|
||||
current = current.parentElement
|
||||
}
|
||||
return editor
|
||||
}
|
||||
|
||||
function focusFirstTableCell(surface) {
|
||||
const editor = getEditorElement(surface)
|
||||
if (!editor) {
|
||||
return
|
||||
}
|
||||
const firstCell = editor.querySelector('td, th')
|
||||
if (!firstCell) {
|
||||
return
|
||||
}
|
||||
const selection = window.getSelection()
|
||||
if (!selection) {
|
||||
return
|
||||
}
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(firstCell)
|
||||
range.collapse(true)
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
savedRanges[surface] = range.cloneRange()
|
||||
}
|
||||
|
||||
function openFullscreen() {
|
||||
isFullscreen.value = true
|
||||
}
|
||||
|
||||
function handleFullscreenVisibleChange(value) {
|
||||
isFullscreen.value = !!value
|
||||
}
|
||||
|
||||
function normalizeVerticalAlign(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
if (['top', 'middle', 'center', 'bottom'].includes(normalized)) {
|
||||
return normalized === 'center' ? 'middle' : normalized
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.template-rich-editor {
|
||||
border: 1px solid #d9e1ec;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.template-rich-editor.is-disabled {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #edf2f7;
|
||||
background: linear-gradient(180deg, #fcfdff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.toolbar-groups {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid #e5edf7;
|
||||
}
|
||||
|
||||
.toolbar-group:last-child {
|
||||
padding-right: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
appearance: none;
|
||||
border: 1px solid #d5deea;
|
||||
background: #fff;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.toolbar-button:hover:not(:disabled) {
|
||||
border-color: #93c5fd;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.toolbar-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.toolbar-button-primary {
|
||||
border-color: #3b82f6;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-content-shell {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.editor-scroll {
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.editor-surface {
|
||||
min-height: 260px;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
outline: none;
|
||||
padding: 14px 16px 18px;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
line-height: 1.7;
|
||||
color: #0f172a;
|
||||
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.editor-surface.has-table {
|
||||
min-width: 760px;
|
||||
}
|
||||
|
||||
.editor-surface[contenteditable='false'] {
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.editor-surface:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.editor-surface p,
|
||||
.editor-surface div,
|
||||
.editor-surface li {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.editor-surface table {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 8px 0;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
border: 1px solid #1f2937;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.editor-surface td,
|
||||
.editor-surface th {
|
||||
min-width: 120px;
|
||||
border: 1px solid #1f2937 !important;
|
||||
padding: 6px 10px;
|
||||
vertical-align: middle;
|
||||
word-break: keep-all;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.editor-surface th {
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.editor-tip {
|
||||
padding: 8px 12px 10px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #edf2f7;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.fullscreen-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: #f4f7fb;
|
||||
}
|
||||
|
||||
.fullscreen-toolbar {
|
||||
border-bottom-color: rgba(148, 163, 184, 0.16);
|
||||
}
|
||||
|
||||
.fullscreen-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.fullscreen-scroll {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.fullscreen-surface {
|
||||
min-height: 100%;
|
||||
min-width: 960px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.editor-toolbar,
|
||||
.fullscreen-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.toolbar-groups {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolbar-button-primary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-surface.has-table,
|
||||
.fullscreen-surface {
|
||||
min-width: 720px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -228,7 +228,7 @@
|
||||
<a v-if="canMockReject(row)" @click="handleMockReject(row)">{{
|
||||
isSingleAgreementRecord(row) ? "审批驳回" : "测试驳回"
|
||||
}}</a>
|
||||
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">{{
|
||||
<a v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canOnlineSign(row)" @click="handleOnlineSign(row)">{{
|
||||
isSingleAgreementRecord(row) ? "发起盖章" : "在线签订"
|
||||
}}</a>
|
||||
<a
|
||||
@@ -236,11 +236,11 @@
|
||||
@click="handleDownloadSigned(row)"
|
||||
>下载签署文件</a
|
||||
>
|
||||
<a v-if="canUploadSigned(row)" @click="handleUploadSigned(row)">{{
|
||||
<a v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canUploadSigned(row)" @click="handleUploadSigned(row)">{{
|
||||
isSingleAgreementRecord(row) ? "上传盖章版" : "上传已签署版本"
|
||||
}}</a>
|
||||
<a
|
||||
v-if="canMockFinishSign(row)"
|
||||
v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canMockFinishSign(row)"
|
||||
@click="handleMockFinishSign(row)"
|
||||
>{{
|
||||
isSingleAgreementRecord(row) ? "测试盖章完成" : "测试签署完成"
|
||||
@@ -323,6 +323,7 @@ import {
|
||||
getLaunchDetail,
|
||||
getLawApproveCallbackSample,
|
||||
mockApproveLaunch,
|
||||
uploadWatermarkFile,
|
||||
mockRejectLaunch,
|
||||
finishLaunchSeal,
|
||||
queryApproveViewUrl,
|
||||
@@ -345,6 +346,9 @@ const contractRecordStatusOptions = [
|
||||
{ label: "已终止", value: "terminated" },
|
||||
];
|
||||
|
||||
// 合同签署已拆到独立菜单,这里只隐藏入口,保留原有逻辑方便后续恢复。
|
||||
const SHOW_SIGN_ACTIONS_IN_LAUNCH = false;
|
||||
|
||||
const formRef = ref(null);
|
||||
const queryContainer = ref(null);
|
||||
const pageWrapperRef = ref(null);
|
||||
@@ -1039,12 +1043,16 @@ function handleUploadSigned(row) {
|
||||
}
|
||||
|
||||
function handleOnlineSign(row) {
|
||||
getLaunchDetail(row.id).then((res) => {
|
||||
getLaunchDetail(row.id).then(async (res) => {
|
||||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||||
message.error(getResponseMessage(res, "获取合同详情失败"));
|
||||
return;
|
||||
}
|
||||
const detail = getDataResult(res);
|
||||
if (!detail.contractTextFilePath) {
|
||||
await handleWatermarkUpload(row);
|
||||
return;
|
||||
}
|
||||
mixedSignModalRef.value?.showModal({
|
||||
...detail,
|
||||
contractId: detail.sealContractId,
|
||||
@@ -1052,6 +1060,60 @@ function handleOnlineSign(row) {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleWatermarkUpload(row) {
|
||||
let file
|
||||
const ok = await new Promise((resolve) => {
|
||||
Modal.confirm({
|
||||
title: "法务水印文件缺失",
|
||||
content: h("div", [
|
||||
h("p", { style: "margin-bottom: 12px" }, "法务审批回调未带回带水印的合同文件,请手动上传带水印的合同文件后继续在线签订。"),
|
||||
h("input", {
|
||||
type: "file",
|
||||
accept: ".pdf,.doc,.docx",
|
||||
style: "margin-top: 8px; width: 100%",
|
||||
onChange(e) {
|
||||
file = e.target.files?.[0]
|
||||
},
|
||||
}),
|
||||
]),
|
||||
okText: "上传并继续",
|
||||
cancelText: "取消",
|
||||
async onOk() {
|
||||
if (!file) {
|
||||
message.warning("请先选择文件")
|
||||
return false
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
try {
|
||||
const res = await uploadWatermarkFile(row.id, formData)
|
||||
if (!isSuccessResponse(res)) {
|
||||
message.error(getResponseMessage(res, "上传水印文件失败"))
|
||||
return false
|
||||
}
|
||||
message.success("水印文件上传成功")
|
||||
resolve(true)
|
||||
} catch {
|
||||
message.error("上传水印文件失败")
|
||||
return false
|
||||
}
|
||||
},
|
||||
onCancel() {
|
||||
resolve(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
if (!ok) return
|
||||
getLaunchDetail(row.id).then((res) => {
|
||||
if (!isSuccessResponse(res) || !getDataResult(res)) return
|
||||
const detail = getDataResult(res)
|
||||
mixedSignModalRef.value?.showModal({
|
||||
...detail,
|
||||
contractId: detail.sealContractId,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleSignBusinessUpdated() {
|
||||
getList();
|
||||
}
|
||||
|
||||
@@ -180,6 +180,15 @@ const actionTypeOptions = [
|
||||
{ label: "合同归档", value: "archive" },
|
||||
{ label: "终止合同", value: "terminate" },
|
||||
{ label: "删除合同", value: "delete" },
|
||||
// ===== 签章平台操作日志 =====
|
||||
{ label: "创建签署流程", value: "seal_create" },
|
||||
{ label: "签署确认", value: "seal_sign" },
|
||||
{ label: "签署流程完结", value: "seal_complete" },
|
||||
{ label: "取消签署", value: "seal_cancel" },
|
||||
{ label: "时间戳签章", value: "seal_timestamp" },
|
||||
{ label: "批注签章", value: "seal_comment" },
|
||||
{ label: "签章回调通知", value: "seal_callback" },
|
||||
{ label: "上传线下签署文件", value: "seal_upload" },
|
||||
];
|
||||
|
||||
const actionStatusOptions = [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h3>合同模板配置中心</h3>
|
||||
<p>在线维护模板配置与变量映射,推荐在本地 Word 编辑正文后回传系统。</p>
|
||||
<p>上传模板后自动识别 `{中文变量}` 占位符,并为每个变量配置录入类型、展示名称和数据源。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,18 +33,11 @@
|
||||
<a-card size="small" title="基本信息" class="panel-card">
|
||||
<div class="field-grid field-grid-2">
|
||||
<a-form-item label="范本名称" name="templateName">
|
||||
<a-input
|
||||
v-model:value="formState.templateName"
|
||||
placeholder="请输入范本名称"
|
||||
/>
|
||||
<a-input v-model:value="formState.templateName" placeholder="请输入范本名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="范本编号" name="templateCode">
|
||||
<a-input
|
||||
v-model:value="formState.templateCode"
|
||||
placeholder="保存后自动生成"
|
||||
readonly
|
||||
/>
|
||||
<a-input v-model:value="formState.templateCode" placeholder="保存后自动生成" readonly />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="范本类型" name="source">
|
||||
@@ -62,11 +55,9 @@
|
||||
placeholder="请选择合同类型"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="版本号" name="version">
|
||||
<a-input
|
||||
v-model:value="formState.version"
|
||||
placeholder="例如 V1.0"
|
||||
/>
|
||||
<a-input v-model:value="formState.version" placeholder="例如 V1.0" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="发布范围" name="publishOrgIds" class="field-span-2">
|
||||
@@ -102,20 +93,19 @@
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" title="模板文件与配置预览" class="panel-card">
|
||||
<a-card size="small" title="模板文件与识别结果" class="panel-card">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
class="workflow-alert"
|
||||
message="推荐维护方式"
|
||||
description="下载当前模板到本地 Word 编辑正文与占位符,保存后重新上传回传;平台负责变量识别、映射、预览与发布。"
|
||||
message="维护建议"
|
||||
description="富文本变量建议单独占一行或单独放在表格单元格内,这样后续渲染表格和多段文字会更稳。"
|
||||
/>
|
||||
|
||||
<div class="upload-meta">
|
||||
<div>
|
||||
<div class="meta-label">当前模板文件</div>
|
||||
<div class="meta-value">
|
||||
{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}
|
||||
</div>
|
||||
<div class="meta-value">{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}</div>
|
||||
</div>
|
||||
|
||||
<a-space v-if="!isViewMode">
|
||||
@@ -123,7 +113,7 @@
|
||||
<DownloadOutlined />
|
||||
下载当前模板
|
||||
</a-button>
|
||||
<a-button @click="handleReDetectVariables">
|
||||
<a-button @click="handleReDetectVariables" :disabled="!templateArrayBuffer">
|
||||
<ReloadOutlined />
|
||||
重新识别变量
|
||||
</a-button>
|
||||
@@ -148,25 +138,29 @@
|
||||
<FileWordOutlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">上传 Word 模板文件(支持 `.docx`)</p>
|
||||
<p class="ant-upload-hint">
|
||||
上传后会自动识别花括号变量,如 `{contractName}`、`{partyAName}`,并生成变量匹配清单。
|
||||
</p>
|
||||
<p class="ant-upload-hint">
|
||||
如需修改正文内容,建议先下载模板到本地 Word 编辑后再重新上传回传。
|
||||
</p>
|
||||
<p class="ant-upload-hint">上传后自动识别 `{合同名称}`、`{附件一内容}` 这类花括号变量。</p>
|
||||
<p class="ant-upload-hint">推荐先在本地 Word 调整正文,再上传到平台做变量配置。</p>
|
||||
</a-upload-dragger>
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" title="占位符工具栏与变量映射" class="panel-card">
|
||||
<a-card size="small" title="变量映射与录入类型" class="panel-card">
|
||||
<div class="variable-header">
|
||||
<div>
|
||||
<div class="meta-label">已识别变量</div>
|
||||
<div class="meta-value">{{ detectedVariableKeys.length ? detectedVariableKeys.join('、') : '暂未识别到变量' }}</div>
|
||||
<div class="meta-value">
|
||||
{{ detectedVariableKeys.length ? detectedVariableKeys.join('、') : '暂未识别到变量' }}
|
||||
</div>
|
||||
<div class="catalog-help">
|
||||
点击下方字段标签可直接复制占位符,点击“加入映射”可快速补充变量绑定。
|
||||
每个变量都要配置展示名称和录入类型。下拉变量需要填写字典类型,富文本变量支持文字和表格。
|
||||
</div>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-tooltip title="查看甲方信息自动带出规则">
|
||||
<a-button size="small" @click="autoFillGuideVisible = true">
|
||||
<InfoCircleOutlined />
|
||||
甲方带出说明
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-button @click="handleDownloadVariableGuide">
|
||||
<DownloadOutlined />
|
||||
下载变量说明
|
||||
@@ -178,33 +172,13 @@
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- <div class="catalog-wrap">
|
||||
<div class="catalog-title">占位符工具栏</div>
|
||||
<div class="catalog-list">
|
||||
<div
|
||||
v-for="item in templateFieldList"
|
||||
:key="item.value"
|
||||
class="catalog-item"
|
||||
>
|
||||
<a-tag
|
||||
color="blue"
|
||||
class="catalog-tag"
|
||||
@click="handleCopyPlaceholder(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-tag>
|
||||
<a-button
|
||||
v-if="!isViewMode"
|
||||
type="link"
|
||||
size="small"
|
||||
class="catalog-action"
|
||||
@click.stop="handleQuickAddCatalogField(item.value)"
|
||||
>
|
||||
加入映射
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
class="workflow-alert"
|
||||
message="富文本变量说明"
|
||||
description="富文本变量最终会替换为 Word 正文内容,支持多段文本和表格。请尽量把富文本占位符单独放在一行。"
|
||||
/>
|
||||
|
||||
<div v-if="variableMappings.length" class="mapping-list">
|
||||
<div
|
||||
@@ -230,31 +204,81 @@
|
||||
<a-input
|
||||
v-model:value="item.variableKey"
|
||||
:disabled="isViewMode"
|
||||
placeholder="例如 contractName"
|
||||
placeholder="例如 合同名称、附件一内容"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="匹配业务字段">
|
||||
<a-select
|
||||
<a-auto-complete
|
||||
v-model:value="item.bindField"
|
||||
:options="templateFieldOptions"
|
||||
:options="templateFieldAutoOptions"
|
||||
:disabled="isViewMode"
|
||||
placeholder="请选择业务字段"
|
||||
placeholder="请选择或直接输入业务字段编码"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="展示名称">
|
||||
<a-input
|
||||
v-model:value="item.fieldLabel"
|
||||
:disabled="isViewMode"
|
||||
placeholder="合同发起页表单展示名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="录入类型">
|
||||
<a-select
|
||||
v-model:value="item.fieldType"
|
||||
:options="fieldTypeOptions"
|
||||
:disabled="isViewMode"
|
||||
placeholder="请选择录入类型"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="是否必填">
|
||||
<a-switch
|
||||
v-model:checked="item.required"
|
||||
:disabled="isViewMode"
|
||||
checked-children="必填"
|
||||
un-checked-children="选填"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="输入提示">
|
||||
<a-input
|
||||
v-model:value="item.placeholder"
|
||||
:disabled="isViewMode"
|
||||
placeholder="可选,例如 请输入附件一内容"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="item.fieldType === 'select'" label="字典类型" class="field-span-2">
|
||||
<a-auto-complete
|
||||
v-model:value="item.dictType"
|
||||
:options="dictTypePresetOptions"
|
||||
:disabled="isViewMode"
|
||||
placeholder="例如 orgBusType、contractPayMethod"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" class="field-span-2">
|
||||
<a-input
|
||||
v-model:value="item.remark"
|
||||
:disabled="isViewMode"
|
||||
placeholder="可选,说明该变量的填写要求"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-empty
|
||||
v-else
|
||||
description="暂无变量匹配项"
|
||||
/>
|
||||
<a-empty v-else description="暂无变量匹配项" />
|
||||
</a-card>
|
||||
|
||||
<a-card size="small" title="维护信息" class="panel-card">
|
||||
<div class="field-grid field-grid-2">
|
||||
<a-form-item label="创建人">
|
||||
<a-form-item label="维护人">
|
||||
<a-input v-model:value="formState.updatedBy" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="创建时间">
|
||||
@@ -269,12 +293,8 @@
|
||||
<a-space>
|
||||
<a-button @click="visible = false">关闭</a-button>
|
||||
<template v-if="!isViewMode">
|
||||
<a-button @click="handleSubmit('draft')">
|
||||
保存
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleSubmit('published')">
|
||||
发布
|
||||
</a-button>
|
||||
<a-button @click="handleSubmit('draft')">保存</a-button>
|
||||
<a-button type="primary" @click="handleSubmit('published')">发布</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
</div>
|
||||
@@ -284,7 +304,7 @@
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h3>模板预览</h3>
|
||||
<p>右侧基于当前变量映射渲染模板效果,用于检查占位符位置与展示结果,本页不直接在线修改 Word 正文。</p>
|
||||
<p>右侧用变量展示名模拟填充效果,帮助检查占位符是否识别正确。</p>
|
||||
</div>
|
||||
<a-space size="small">
|
||||
<a-tag color="geekblue">{{ formState.source || '未分类' }}</a-tag>
|
||||
@@ -295,7 +315,7 @@
|
||||
<div class="preview-shell">
|
||||
<div v-if="!templateArrayBuffer" class="preview-state">
|
||||
<FileWordOutlined style="font-size: 42px; color: #94a3b8" />
|
||||
<p>上传 `.docx` 模板文件后,这里会显示填充后的预览;正文修改建议在本地 Word 完成后回传。</p>
|
||||
<p>上传 `.docx` 模板文件后,这里会显示填充预览。</p>
|
||||
</div>
|
||||
<div v-else ref="previewRef" class="preview-stage"></div>
|
||||
<div v-if="rendering" class="preview-state preview-mask">
|
||||
@@ -310,6 +330,43 @@
|
||||
</section>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="autoFillGuideVisible"
|
||||
title="甲方自动带出变量"
|
||||
width="760px"
|
||||
:footer="null"
|
||||
>
|
||||
<div class="auto-fill-guide">
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="使用规则"
|
||||
description="模板中写入下列中文变量后,合同发起选择甲方并进入合同正文配置时,系统会从甲方信息自动填入。只有一个甲方时会填“甲方名称”这类变量;多个甲方时使用“甲方1名称”“甲方名称列表”。"
|
||||
/>
|
||||
|
||||
<div class="guide-section">
|
||||
<h4>单甲方变量</h4>
|
||||
<div class="guide-tag-list">
|
||||
<a-tag v-for="name in ourPartySingleVariables" :key="name">{{ name }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-section">
|
||||
<h4>多甲方变量(支持甲方1到甲方{{ OUR_PARTY_AUTO_FILL_MAX_COUNT }})</h4>
|
||||
<div class="guide-tag-list">
|
||||
<a-tag v-for="name in ourPartyIndexedVariables" :key="name">{{ name }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="guide-section">
|
||||
<h4>汇总变量</h4>
|
||||
<div class="guide-tag-list">
|
||||
<a-tag v-for="name in ourPartyListVariables" :key="name">{{ name }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -320,6 +377,7 @@ import {
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
FileWordOutlined,
|
||||
InfoCircleOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
@@ -334,16 +392,30 @@ import {
|
||||
import { contractTypeTreeOptions } from '../contractTypeOptions.js'
|
||||
import {
|
||||
buildTemplateFieldLabelMap,
|
||||
buildTemplateFieldOptions,
|
||||
loadTemplateFieldList,
|
||||
} from '../../shared/templateFieldDict.js'
|
||||
import {
|
||||
buildTemplatePreviewData,
|
||||
} from '../../shared/templateDocxRenderer.js'
|
||||
import {
|
||||
createEmptyTemplateVariableMapping,
|
||||
normalizeTemplateVariableMapping,
|
||||
TEMPLATE_FIELD_TYPE_OPTIONS,
|
||||
} from '../../shared/templateVariableConfig.js'
|
||||
import {
|
||||
getOurPartyAutoFillFieldOptions,
|
||||
getOurPartyAutoFillIndexedVariableNames,
|
||||
getOurPartyAutoFillListVariableNames,
|
||||
getOurPartyAutoFillSingleVariableNames,
|
||||
OUR_PARTY_AUTO_FILL_MAX_COUNT,
|
||||
} from '../../shared/templateAutoFillMappings.js'
|
||||
import {
|
||||
loadDictTypeOptions,
|
||||
buildDictTypeSelectOptions,
|
||||
} from '../../shared/dictTypeOptions.js'
|
||||
|
||||
// ========== 模板管理字典(后续可由后端接口替换) ==========
|
||||
|
||||
// 合同类型选项
|
||||
const contractTypeOptions = contractTypeTreeOptions
|
||||
|
||||
// 范本类型选项
|
||||
const templateSourceOptions = [
|
||||
{ label: '标准模板', value: '标准模板' },
|
||||
{ label: '推荐文本', value: '推荐文本' },
|
||||
@@ -351,13 +423,8 @@ const templateSourceOptions = [
|
||||
{ label: '对方文本', value: '对方文本' },
|
||||
]
|
||||
|
||||
// 批量创建变量映射
|
||||
function createSimpleVariableList(list = []) {
|
||||
return list.map((item, index) => ({
|
||||
id: item.id || `var_${Date.now()}_${index}`,
|
||||
variableKey: item.variableKey || '',
|
||||
bindField: item.bindField || '',
|
||||
}))
|
||||
return list.map((item, index) => normalizeTemplateVariableMapping(item, index))
|
||||
}
|
||||
|
||||
const emit = defineEmits(['submit'])
|
||||
@@ -372,6 +439,7 @@ const templateArrayBuffer = ref(null)
|
||||
const templateUploadList = ref([])
|
||||
const variableMappings = ref([])
|
||||
const detectedVariableKeys = ref([])
|
||||
const autoFillGuideVisible = ref(false)
|
||||
const orgTreeData = ref([])
|
||||
const SHOW_PARENT = 'SHOW_PARENT'
|
||||
const templateFieldList = ref([])
|
||||
@@ -379,14 +447,28 @@ const templateFieldList = ref([])
|
||||
let previewTimer = null
|
||||
let renderTaskId = 0
|
||||
|
||||
const templateFieldOptions = computed(() =>
|
||||
buildTemplateFieldOptions(templateFieldList.value),
|
||||
)
|
||||
|
||||
const templateFieldLabelMap = computed(() =>
|
||||
buildTemplateFieldLabelMap(templateFieldList.value),
|
||||
)
|
||||
|
||||
const templateFieldAutoOptions = computed(() =>
|
||||
[
|
||||
...(templateFieldList.value || []).map((item) => ({
|
||||
value: item.value,
|
||||
label: `${item.label}(${item.value})`,
|
||||
})),
|
||||
...getOurPartyAutoFillFieldOptions(),
|
||||
].filter((item, index, list) =>
|
||||
item.value && list.findIndex((option) => option.value === item.value) === index,
|
||||
),
|
||||
)
|
||||
|
||||
const fieldTypeOptions = TEMPLATE_FIELD_TYPE_OPTIONS
|
||||
const dictTypePresetOptions = ref([])
|
||||
const ourPartySingleVariables = getOurPartyAutoFillSingleVariableNames()
|
||||
const ourPartyIndexedVariables = getOurPartyAutoFillIndexedVariableNames()
|
||||
const ourPartyListVariables = getOurPartyAutoFillListVariableNames()
|
||||
|
||||
const formRules = {
|
||||
templateName: [{ required: true, message: '请输入范本名称', trigger: 'blur' }],
|
||||
source: [{ required: true, message: '请选择范本类型', trigger: 'change' }],
|
||||
@@ -446,6 +528,11 @@ watch(
|
||||
variableMappings.value.map((item) => ({
|
||||
variableKey: item.variableKey,
|
||||
bindField: item.bindField,
|
||||
fieldLabel: item.fieldLabel,
|
||||
fieldType: item.fieldType,
|
||||
required: item.required,
|
||||
dictType: item.dictType,
|
||||
placeholder: item.placeholder,
|
||||
})),
|
||||
),
|
||||
() => {
|
||||
@@ -520,7 +607,6 @@ function resolveUploadedFilePath(res) {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
function extractVariablesFromText(text) {
|
||||
const result = new Set()
|
||||
const regex = /\{+\s*([^{}\s]+)\s*\}+/g
|
||||
@@ -543,11 +629,17 @@ function mergeDetectedVariables(variableKeys) {
|
||||
|
||||
const detectedRows = nextKeys.map((variableKey) => {
|
||||
const existing = existingMap.get(variableKey)
|
||||
return {
|
||||
return normalizeTemplateVariableMapping({
|
||||
id: existing?.id || `var_${Date.now()}_${variableKey}`,
|
||||
variableKey,
|
||||
bindField: existing?.bindField || variableKey,
|
||||
}
|
||||
fieldLabel: existing?.fieldLabel || variableKey,
|
||||
fieldType: existing?.fieldType || 'input',
|
||||
required: existing?.required !== false,
|
||||
placeholder: existing?.placeholder || '',
|
||||
dictType: existing?.dictType || '',
|
||||
remark: existing?.remark || '',
|
||||
})
|
||||
})
|
||||
|
||||
const manualRows = variableMappings.value
|
||||
@@ -575,7 +667,6 @@ async function detectVariablesFromBuffer(buffer) {
|
||||
})
|
||||
|
||||
mergeDetectedVariables([...allVariables])
|
||||
|
||||
} catch (error) {
|
||||
message.warning('模板文件已上传,但变量自动识别失败,请手动维护变量')
|
||||
}
|
||||
@@ -649,22 +740,8 @@ async function handleReDetectVariables() {
|
||||
}
|
||||
|
||||
function handleAddManualVariable() {
|
||||
variableMappings.value.push({
|
||||
id: `var_${Date.now()}_${variableMappings.value.length + 1}`,
|
||||
variableKey: '',
|
||||
bindField: '',
|
||||
})
|
||||
}
|
||||
|
||||
function handleQuickAddCatalogField(fieldKey) {
|
||||
if (isViewMode.value) return
|
||||
|
||||
variableMappings.value.push(
|
||||
{
|
||||
id: `var_${Date.now()}_${fieldKey}`,
|
||||
variableKey: fieldKey,
|
||||
bindField: fieldKey,
|
||||
},
|
||||
createEmptyTemplateVariableMapping(variableMappings.value.length + 1),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -672,47 +749,12 @@ function buildPlaceholderText(fieldKey) {
|
||||
return `{${fieldKey}}`
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text) {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', 'readonly')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.left = '-9999px'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
|
||||
async function handleCopyPlaceholder(fieldKey) {
|
||||
const placeholder = buildPlaceholderText(fieldKey)
|
||||
try {
|
||||
await copyTextToClipboard(placeholder)
|
||||
message.success(`已复制占位符:${placeholder}`)
|
||||
} catch (error) {
|
||||
message.warning(`复制失败,请手动使用占位符:${placeholder}`)
|
||||
}
|
||||
}
|
||||
|
||||
function removeVariable(id) {
|
||||
variableMappings.value = variableMappings.value.filter((item) => item.id !== id)
|
||||
}
|
||||
|
||||
function buildPreviewData() {
|
||||
const data = {}
|
||||
variableMappings.value.forEach((item) => {
|
||||
if (!item.variableKey) return
|
||||
data[item.variableKey] =
|
||||
templateFieldLabelMap.value[item.bindField] ||
|
||||
item.bindField ||
|
||||
`[${item.variableKey}]`
|
||||
})
|
||||
return data
|
||||
return buildTemplatePreviewData(variableMappings.value, templateFieldLabelMap.value)
|
||||
}
|
||||
|
||||
async function renderPreview() {
|
||||
@@ -796,9 +838,9 @@ function buildRecord(status) {
|
||||
variables: variableMappings.value
|
||||
.filter((item) => item.variableKey)
|
||||
.map((item) => item.variableKey),
|
||||
variableMappings: variableMappings.value.map((item) => ({
|
||||
...item,
|
||||
})),
|
||||
variableMappings: variableMappings.value.map((item, index) =>
|
||||
normalizeTemplateVariableMapping(item, index),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,6 +866,18 @@ function validateVariableMappings() {
|
||||
if (!item.bindField) {
|
||||
return `请为变量【${item.variableKey}】选择匹配业务字段`
|
||||
}
|
||||
|
||||
if (!item.fieldLabel) {
|
||||
return `请为变量【${item.variableKey}】填写展示名称`
|
||||
}
|
||||
|
||||
if (!item.fieldType) {
|
||||
return `请为变量【${item.variableKey}】选择录入类型`
|
||||
}
|
||||
|
||||
if (item.fieldType === 'select' && !item.dictType) {
|
||||
return `请为下拉变量【${item.variableKey}】配置字典类型`
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
@@ -888,14 +942,23 @@ function buildVariableGuideText() {
|
||||
'',
|
||||
'推荐维护方式:',
|
||||
'1. 下载当前模板到本地 Word 编辑正文内容。',
|
||||
'2. 在正文中插入占位符,例如 {contractName}、{partyAName}。',
|
||||
'2. 在正文中插入占位符,例如 {合同名称}、{附件一内容}。',
|
||||
'3. 保存后重新上传 .docx 文件回传系统。',
|
||||
'4. 在平台内完成变量识别、字段映射、预览和发布。',
|
||||
'4. 在平台内为每个变量配置展示名称、录入类型和数据源。',
|
||||
'',
|
||||
'可用占位符清单:',
|
||||
'录入类型说明:',
|
||||
'- 输入框:普通文本输入。',
|
||||
'- 下拉框:需配置 dictType,例如 orgBusType。',
|
||||
'- 富文本:支持多段文本与表格,建议占位符单独成段。',
|
||||
'',
|
||||
'可用业务字段参考:',
|
||||
...templateFieldList.value.map(
|
||||
(item) => `- ${item.label}: ${buildPlaceholderText(item.value)}`,
|
||||
(item) => `- ${item.label}: ${item.value}`,
|
||||
),
|
||||
'',
|
||||
'甲方自动带出变量:',
|
||||
'说明:单甲方变量仅在只有一个甲方时自动填;多甲方请使用甲方1/甲方2变量或列表变量。',
|
||||
...getOurPartyAutoFillFieldOptions().map((item) => `- ${item.value}`),
|
||||
]
|
||||
|
||||
return lines.join('\r\n')
|
||||
@@ -942,9 +1005,6 @@ async function handleDownloadCurrentTemplate() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将后端组织树节点转为 a-tree-select 需要的格式
|
||||
*/
|
||||
function convertOrgTreeToTreeSelect(node) {
|
||||
const treeNode = {
|
||||
value: String(node.id),
|
||||
@@ -956,9 +1016,6 @@ function convertOrgTreeToTreeSelect(node) {
|
||||
return treeNode
|
||||
}
|
||||
|
||||
/**
|
||||
* 从树数据构建 orgId → orgName 的映射(递归遍历)
|
||||
*/
|
||||
function buildOrgNameMap(treeData) {
|
||||
const map = new Map()
|
||||
function walk(list) {
|
||||
@@ -976,6 +1033,10 @@ async function showModal(record = {}, mode = 'create') {
|
||||
resetState()
|
||||
modalMode.value = mode || 'create'
|
||||
await ensureTemplateFieldListLoaded()
|
||||
if (!dictTypePresetOptions.value.length) {
|
||||
const list = await loadDictTypeOptions()
|
||||
dictTypePresetOptions.value = buildDictTypeSelectOptions(list)
|
||||
}
|
||||
if (!orgTreeData.value.length) {
|
||||
const tree = await getOrgTree()
|
||||
orgTreeData.value = (tree || []).map(convertOrgTreeToTreeSelect)
|
||||
@@ -1017,7 +1078,6 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 详情模式下禁用表单文字颜色加深 */
|
||||
.view-form:deep(.ant-select-disabled .ant-select-selection-item) {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
@@ -1028,20 +1088,6 @@ defineExpose({
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
.view-form:deep(.ant-input-number-disabled .ant-input-number-input) {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
.view-form:deep(.ant-picker.ant-picker-disabled .ant-picker-input input) {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
.view-form:deep(.ant-radio-wrapper.ant-radio-wrapper-disabled .ant-radio + span) {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
.view-form:deep(.ant-checkbox-wrapper.ant-checkbox-wrapper-disabled .ant-checkbox + span) {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
.view-form:deep(.ant-input-disabled) {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
|
||||
@@ -1049,7 +1095,7 @@ defineExpose({
|
||||
|
||||
.template-manage-modal {
|
||||
display: grid;
|
||||
grid-template-columns: 560px minmax(0, 1fr);
|
||||
grid-template-columns: 580px minmax(0, 1fr);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -1086,6 +1132,27 @@ defineExpose({
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.auto-fill-guide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 68vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.guide-section h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.guide-tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 4px;
|
||||
}
|
||||
|
||||
.panel-header p {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
@@ -1157,21 +1224,6 @@ defineExpose({
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.catalog-wrap {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #f8fbff;
|
||||
border: 1px solid #e0eaf8;
|
||||
}
|
||||
|
||||
.catalog-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.catalog-help {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
@@ -1179,30 +1231,6 @@ defineExpose({
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.catalog-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.catalog-tag {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.catalog-action {
|
||||
padding-inline: 4px;
|
||||
}
|
||||
|
||||
.mapping-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1290,7 +1318,7 @@ defineExpose({
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.template-manage-modal {
|
||||
grid-template-columns: 520px minmax(0, 1fr);
|
||||
grid-template-columns: 540px minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getDictDataByType } from "@/api/dict.js";
|
||||
|
||||
export const TEMPLATE_DICT_TYPES_KEY = "TEMPLATE_DICT_TYPES";
|
||||
|
||||
let cachedDictTypeOptions = null;
|
||||
|
||||
function normalizeDictTypeList(list = []) {
|
||||
return list
|
||||
.map((item) => ({
|
||||
label: item?.k || item?.label || "",
|
||||
value: item?.v || item?.value || "",
|
||||
remark: item?.remark || "",
|
||||
}))
|
||||
.filter((item) => item.label && item.value);
|
||||
}
|
||||
|
||||
export async function loadDictTypeOptions() {
|
||||
if (cachedDictTypeOptions?.length) {
|
||||
return [...cachedDictTypeOptions];
|
||||
}
|
||||
|
||||
const result = await getDictDataByType(TEMPLATE_DICT_TYPES_KEY);
|
||||
cachedDictTypeOptions = normalizeDictTypeList(result);
|
||||
return [...cachedDictTypeOptions];
|
||||
}
|
||||
|
||||
export function buildDictTypeSelectOptions(list = []) {
|
||||
return list.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
const LIST_SEPARATOR = ";";
|
||||
|
||||
export const OUR_PARTY_AUTO_FILL_MAX_COUNT = 5;
|
||||
|
||||
const RELATIVE_TYPE_LABELS = {
|
||||
client: "客户",
|
||||
supplier: "供应商",
|
||||
Partner: "合作伙伴",
|
||||
clientAndSup: "客户及供应商",
|
||||
other: "其他",
|
||||
};
|
||||
|
||||
const OPPOSITE_CHARACTER_LABELS = {
|
||||
1: "境内组织",
|
||||
2: "境内个人",
|
||||
3: "境外组织",
|
||||
4: "境外个人",
|
||||
};
|
||||
|
||||
const ORG_NATURE_LABELS = {
|
||||
ZF: "政府",
|
||||
ZY: "政府",
|
||||
QY: "企业",
|
||||
QT: "其他组织",
|
||||
};
|
||||
|
||||
const ORG_NATURE_LEVEL_LABELS = {
|
||||
GNQY: "国有企业",
|
||||
MYQY: "民营企业",
|
||||
WZQY: "外资企业",
|
||||
HZQY: "合资企业",
|
||||
GYQZHKGQY: "国有全资和控股企业",
|
||||
QT: "其他",
|
||||
};
|
||||
|
||||
const ID_TYPE_LABELS = {
|
||||
idCard: "身份证",
|
||||
passport: "护照",
|
||||
other: "其他",
|
||||
};
|
||||
|
||||
function normalizeText(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "是" : "否";
|
||||
}
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
function pickLabel(value, labelMap) {
|
||||
const text = normalizeText(value);
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
return labelMap[text] || text;
|
||||
}
|
||||
|
||||
function formatBoolean(value) {
|
||||
if (value === true || value === "true" || value === "1" || value === "Y" || value === "是") {
|
||||
return "是";
|
||||
}
|
||||
if (value === false || value === "false" || value === "0" || value === "N" || value === "否") {
|
||||
return "否";
|
||||
}
|
||||
return normalizeText(value);
|
||||
}
|
||||
|
||||
function formatMoneyLike(value) {
|
||||
const text = normalizeText(value);
|
||||
if (!text) {
|
||||
return "";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export const OUR_PARTY_AUTO_FILL_FIELD_MAPPINGS = [
|
||||
{
|
||||
label: "甲方名称",
|
||||
suffixes: ["名称", "公司名称"],
|
||||
sourceKeys: ["companyName"],
|
||||
},
|
||||
{
|
||||
label: "甲方主体编码",
|
||||
suffixes: ["主体编码", "组织编码"],
|
||||
sourceKeys: ["subjectCode"],
|
||||
},
|
||||
{
|
||||
label: "甲方公司类型",
|
||||
suffixes: ["公司类型"],
|
||||
sourceKeys: ["companyType", "relativeType"],
|
||||
formatter: (value) => pickLabel(value, RELATIVE_TYPE_LABELS),
|
||||
},
|
||||
{
|
||||
label: "甲方公司性质",
|
||||
suffixes: ["公司性质"],
|
||||
sourceKeys: ["companyNature", "oppositeCharacter", "oppCharacter"],
|
||||
formatter: (value) => pickLabel(value, OPPOSITE_CHARACTER_LABELS),
|
||||
},
|
||||
{
|
||||
label: "甲方组织性质",
|
||||
suffixes: ["组织性质"],
|
||||
sourceKeys: ["orgNature", "natureOfEnterpriseOrg"],
|
||||
formatter: (value) => pickLabel(value, ORG_NATURE_LABELS),
|
||||
},
|
||||
{
|
||||
label: "甲方组织性质企业二级",
|
||||
suffixes: ["组织性质企业二级"],
|
||||
sourceKeys: ["orgNatureLevel", "natureOfEnterpriseOrgLev"],
|
||||
formatter: (value) => pickLabel(value, ORG_NATURE_LEVEL_LABELS),
|
||||
},
|
||||
{
|
||||
label: "甲方是否涉港澳台",
|
||||
suffixes: ["是否涉港澳台"],
|
||||
sourceKeys: ["isHongKongMacaoTaiwan", "isSgat", "gatFlag"],
|
||||
formatter: formatBoolean,
|
||||
},
|
||||
{
|
||||
label: "甲方是否集团内",
|
||||
suffixes: ["是否集团内"],
|
||||
sourceKeys: ["isGroupInternal", "isJtn", "groupFlag"],
|
||||
formatter: formatBoolean,
|
||||
},
|
||||
{
|
||||
label: "甲方统一社会信用代码",
|
||||
suffixes: ["统一社会信用代码", "信用代码"],
|
||||
sourceKeys: ["creditCode", "customerTaxno"],
|
||||
},
|
||||
{
|
||||
label: "甲方法定代表人或负责人",
|
||||
suffixes: ["法定代表人或负责人", "法定代表人", "负责人"],
|
||||
sourceKeys: ["legalRepresentative", "legalPersonName"],
|
||||
},
|
||||
{
|
||||
label: "甲方注册地址",
|
||||
suffixes: ["注册地址", "地址"],
|
||||
sourceKeys: ["registeredAddress", "customerRegaddr"],
|
||||
},
|
||||
{
|
||||
label: "甲方注册资本",
|
||||
suffixes: ["注册资本"],
|
||||
sourceKeys: ["registeredCapitalText", "regCapital", "registeredCapital"],
|
||||
formatter: formatMoneyLike,
|
||||
},
|
||||
{
|
||||
label: "甲方联系人",
|
||||
suffixes: ["联系人"],
|
||||
sourceKeys: ["contactName"],
|
||||
},
|
||||
{
|
||||
label: "甲方联系电话",
|
||||
suffixes: ["联系电话", "电话"],
|
||||
sourceKeys: ["contactPhone", "officephone"],
|
||||
},
|
||||
{
|
||||
label: "甲方通讯地址",
|
||||
suffixes: ["通讯地址", "联系地址"],
|
||||
sourceKeys: ["contactAddress", "customerAddress"],
|
||||
},
|
||||
{
|
||||
label: "甲方电子邮箱",
|
||||
suffixes: ["电子邮箱", "邮箱"],
|
||||
sourceKeys: ["contactEmail", "emailAddress"],
|
||||
},
|
||||
{
|
||||
label: "甲方国家",
|
||||
suffixes: ["国家"],
|
||||
sourceKeys: ["countryName", "country"],
|
||||
},
|
||||
{
|
||||
label: "甲方自然人证件类型",
|
||||
suffixes: ["自然人证件类型"],
|
||||
sourceKeys: ["naturalPersonIdType", "certificateType"],
|
||||
formatter: (value) => pickLabel(value, ID_TYPE_LABELS),
|
||||
},
|
||||
{
|
||||
label: "甲方自然人证件号码",
|
||||
suffixes: ["自然人证件号码"],
|
||||
sourceKeys: ["naturalPersonIdNumber", "certificateNum"],
|
||||
},
|
||||
];
|
||||
|
||||
function getPrimarySuffix(mapping) {
|
||||
return mapping.suffixes?.[0] || "";
|
||||
}
|
||||
|
||||
function buildVariableName(prefix, suffix) {
|
||||
return `${prefix}${suffix}`;
|
||||
}
|
||||
|
||||
function getPartyList(source = {}) {
|
||||
const partyState = source.partyState || source;
|
||||
if (Array.isArray(partyState.ourPartyList)) {
|
||||
return partyState.ourPartyList.filter((party) => normalizeText(party?.companyName || party?.subjectCode));
|
||||
}
|
||||
if (Array.isArray(source.partyList)) {
|
||||
return source.partyList.filter((party) => String(party?.roleCode || "").toUpperCase() === "OUR");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getPartyValue(party = {}, mapping = {}) {
|
||||
for (const key of mapping.sourceKeys || []) {
|
||||
const rawValue = party[key];
|
||||
const value = mapping.formatter ? mapping.formatter(rawValue, party) : normalizeText(rawValue);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getOurPartyAutoFillSingleVariableNames() {
|
||||
return OUR_PARTY_AUTO_FILL_FIELD_MAPPINGS.flatMap((mapping) =>
|
||||
(mapping.suffixes || []).map((suffix) => buildVariableName("甲方", suffix)),
|
||||
);
|
||||
}
|
||||
|
||||
export function getOurPartyAutoFillIndexedVariableNames(maxCount = OUR_PARTY_AUTO_FILL_MAX_COUNT) {
|
||||
const names = [];
|
||||
for (let index = 1; index <= maxCount; index += 1) {
|
||||
OUR_PARTY_AUTO_FILL_FIELD_MAPPINGS.forEach((mapping) => {
|
||||
(mapping.suffixes || []).forEach((suffix) => {
|
||||
names.push(buildVariableName(`甲方${index}`, suffix));
|
||||
});
|
||||
});
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
export function getOurPartyAutoFillListVariableNames() {
|
||||
return OUR_PARTY_AUTO_FILL_FIELD_MAPPINGS.map((mapping) =>
|
||||
buildVariableName("甲方", `${getPrimarySuffix(mapping)}列表`),
|
||||
);
|
||||
}
|
||||
|
||||
export function getOurPartyAutoFillVariableNames() {
|
||||
return [
|
||||
...getOurPartyAutoFillSingleVariableNames(),
|
||||
...getOurPartyAutoFillIndexedVariableNames(),
|
||||
...getOurPartyAutoFillListVariableNames(),
|
||||
];
|
||||
}
|
||||
|
||||
export function getOurPartyAutoFillFieldOptions() {
|
||||
return getOurPartyAutoFillVariableNames().map((name) => ({
|
||||
label: `${name}(甲方自动带出)`,
|
||||
value: name,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildOurPartyAutoFillValues(source = {}) {
|
||||
const parties = getPartyList(source);
|
||||
const values = {};
|
||||
|
||||
parties.slice(0, OUR_PARTY_AUTO_FILL_MAX_COUNT).forEach((party, index) => {
|
||||
const prefix = `甲方${index + 1}`;
|
||||
OUR_PARTY_AUTO_FILL_FIELD_MAPPINGS.forEach((mapping) => {
|
||||
const value = getPartyValue(party, mapping);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
(mapping.suffixes || []).forEach((suffix) => {
|
||||
values[buildVariableName(prefix, suffix)] = value;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
OUR_PARTY_AUTO_FILL_FIELD_MAPPINGS.forEach((mapping) => {
|
||||
const listValue = parties
|
||||
.map((party) => getPartyValue(party, mapping))
|
||||
.filter(Boolean)
|
||||
.join(LIST_SEPARATOR);
|
||||
if (listValue) {
|
||||
values[buildVariableName("甲方", `${getPrimarySuffix(mapping)}列表`)] = listValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (parties.length === 1) {
|
||||
OUR_PARTY_AUTO_FILL_FIELD_MAPPINGS.forEach((mapping) => {
|
||||
const value = getPartyValue(parties[0], mapping);
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
(mapping.suffixes || []).forEach((suffix) => {
|
||||
values[buildVariableName("甲方", suffix)] = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
export const TEMPLATE_FIELD_TYPE_OPTIONS = [
|
||||
{ label: "输入框", value: "input" },
|
||||
{ label: "下拉框", value: "select" },
|
||||
{ label: "富文本", value: "rich_text" },
|
||||
];
|
||||
|
||||
export function normalizeTemplateVariableMapping(item = {}, index = 0) {
|
||||
const variableKey = String(item.variableKey || "").trim();
|
||||
const bindField = String(item.bindField || variableKey || "").trim();
|
||||
const fieldType = normalizeFieldType(item.fieldType);
|
||||
return {
|
||||
id: item.id || `var_${Date.now()}_${index}`,
|
||||
variableKey,
|
||||
bindField,
|
||||
fieldLabel: String(item.fieldLabel || variableKey || bindField || "").trim(),
|
||||
fieldType,
|
||||
required: item.required !== false,
|
||||
placeholder: String(item.placeholder || "").trim(),
|
||||
dictType: String(item.dictType || "").trim(),
|
||||
remark: String(item.remark || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyTemplateVariableMapping(index = 0) {
|
||||
return normalizeTemplateVariableMapping({}, index);
|
||||
}
|
||||
|
||||
export function buildTemplateLaunchFieldList(variableMappings = [], templateFieldLabelMap = {}) {
|
||||
const fieldMap = new Map();
|
||||
variableMappings.forEach((item, index) => {
|
||||
const mapping = normalizeTemplateVariableMapping(item, index);
|
||||
const fieldKey = mapping.bindField || mapping.variableKey;
|
||||
if (!fieldKey || fieldMap.has(fieldKey)) {
|
||||
return;
|
||||
}
|
||||
fieldMap.set(fieldKey, {
|
||||
key: fieldKey,
|
||||
label:
|
||||
mapping.fieldLabel ||
|
||||
templateFieldLabelMap[fieldKey] ||
|
||||
mapping.variableKey ||
|
||||
fieldKey,
|
||||
fieldType: mapping.fieldType,
|
||||
required: mapping.required !== false,
|
||||
placeholder: mapping.placeholder || buildDefaultPlaceholder(mapping),
|
||||
dictType: mapping.dictType || "",
|
||||
remark: mapping.remark || "",
|
||||
variableKey: mapping.variableKey || fieldKey,
|
||||
});
|
||||
});
|
||||
return [...fieldMap.values()];
|
||||
}
|
||||
|
||||
export function normalizeFieldType(fieldType) {
|
||||
const value = String(fieldType || "input").trim();
|
||||
if (value === "select") {
|
||||
return "select";
|
||||
}
|
||||
if (value === "rich_text") {
|
||||
return "rich_text";
|
||||
}
|
||||
return "input";
|
||||
}
|
||||
|
||||
export function buildDefaultPlaceholder(mapping = {}) {
|
||||
const label = mapping.fieldLabel || mapping.variableKey || mapping.bindField || "内容";
|
||||
if (mapping.fieldType === "select") {
|
||||
return `请选择${label}`;
|
||||
}
|
||||
if (mapping.fieldType === "rich_text") {
|
||||
return `请输入${label},支持表格`;
|
||||
}
|
||||
return `请输入${label}`;
|
||||
}
|
||||
|
||||
export function stringifyTemplateFieldValue(value, fieldType = "input") {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (fieldType === "rich_text") {
|
||||
return String(value);
|
||||
}
|
||||
return String(value).trim();
|
||||
}
|
||||
@@ -0,0 +1,772 @@
|
||||
<template>
|
||||
<section class="section-container">
|
||||
<a-card :bordered="false" class="query-card" :body-style="queryCardBodyStyle">
|
||||
<div ref="queryContainer" class="section-query-container">
|
||||
<a-form :model="queryParam" layout="inline">
|
||||
<a-row :gutter="16" style="width: 100%">
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="合同名称">
|
||||
<a-input v-model:value="queryParam.contractName" allow-clear placeholder="请输入合同名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="合同编码">
|
||||
<a-input v-model:value="queryParam.contractCode" allow-clear placeholder="请输入合同编码" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="合同类型">
|
||||
<a-select
|
||||
v-model:value="queryParam.contractType"
|
||||
allow-clear
|
||||
placeholder="请选择合同类型"
|
||||
:options="contractTypeOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="6" :sm="24">
|
||||
<a-form-item label="签署状态">
|
||||
<a-select
|
||||
v-model:value="queryParam.status"
|
||||
allow-clear
|
||||
placeholder="全部签署阶段"
|
||||
:options="signStatusOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :md="12" :sm="24">
|
||||
<a-form-item label="发起日期">
|
||||
<a-range-picker
|
||||
v-model:value="queryParam.dateRange"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<a-card :bordered="false" class="table-card" :body-style="tableCardBodyStyle">
|
||||
<vxe-toolbar ref="toolbarRef" custom>
|
||||
<template #buttons>
|
||||
<a-space wrap>
|
||||
<a-button type="primary" :icon="h(SearchOutlined)" @click="handleQuery">查询</a-button>
|
||||
<a-button :icon="h(RedoOutlined)" @click="handleReset">重置</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
border
|
||||
:height="tableHeight"
|
||||
:row-config="{ isHover: true, height: 44 }"
|
||||
:column-config="{ resizable: true, minWidth: 120 }"
|
||||
:custom-config="{ storage: true }"
|
||||
:scroll-y="{ enabled: true, gt: 0, mode: 'wheel' }"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
:loading-config="{ icon: 'vxe-icon-indicator roll', text: '正在拼命加载中...' }"
|
||||
>
|
||||
<vxe-column type="seq" title="序号" width="68" />
|
||||
<vxe-column field="contractCode" title="合同编码" width="160" show-overflow="title" />
|
||||
<vxe-column field="lawContractCode" title="法务合同编号" width="160" show-overflow="title" />
|
||||
<vxe-column field="contractName" title="合同名称" width="220" show-overflow="title" />
|
||||
<vxe-column field="contractType" title="合同类型" width="160" show-overflow="title">
|
||||
<template #default="{ row }">
|
||||
{{ getContractTypeSimpleLabel(row.contractTypeCode || row.contractType) }}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="isElectron" title="签署方式" width="120">
|
||||
<template #default="{ row }">
|
||||
<a-tag :color="String(row.isElectron || '1') === '1' ? 'processing' : 'default'">
|
||||
{{ String(row.isElectron || '1') === '1' ? '电子合同' : '纸质合同' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="counterpartySummary" title="合同相对方" width="260" show-overflow="title" />
|
||||
<vxe-column field="status" title="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<a-tag :color="getStatusColor(row.status)">{{ getStatusLabelByRow(row) }}</a-tag>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="approveMessage" title="审批说明" width="220" show-overflow="title" />
|
||||
<vxe-column field="initiator" title="发起人" width="100" show-overflow="title" />
|
||||
<vxe-column field="initiatorDept" title="所属部门" width="140" show-overflow="title" />
|
||||
<vxe-column field="createTime" title="发起时间" width="170" show-overflow="title" />
|
||||
<vxe-column field="updateTime" title="最后更新时间" width="170" show-overflow="title" />
|
||||
<vxe-column title="操作" width="760" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<a @click="handleView(row)">查看</a>
|
||||
<a v-if="canViewApprove(row)" @click="handleViewApprove(row)">审批记录</a>
|
||||
<a v-if="canViewLawCallback(row)" @click="handleViewLawCallback(row)">回调data</a>
|
||||
<a v-if="canConfigSealPosition(row)" @click="handleConfigSealPosition(row)">配置签章位</a>
|
||||
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">在线签订</a>
|
||||
<a v-if="canUploadSigned(row)" @click="handleUploadSigned(row)">上传已签署版本</a>
|
||||
<a v-if="canMockFinishSign(row)" @click="handleMockFinishSign(row)">测试签署完成</a>
|
||||
<a v-if="canDownloadSigned(row)" @click="handleDownloadSigned(row)">下载签署文件</a>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
|
||||
<a-pagination
|
||||
id="pageWrapper"
|
||||
ref="pageWrapperRef"
|
||||
v-model:pageSize="queryParam.pageSize"
|
||||
v-model:current="queryParam.pageNo"
|
||||
class="table-pagination"
|
||||
show-size-changer
|
||||
:page-size-options="['10', '20', '50', '100']"
|
||||
:total="total"
|
||||
:show-total="(value) => `共 ${value} 条`"
|
||||
@change="onShowSizeChange"
|
||||
@show-size-change="onShowSizeChange"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<ContractPartyPlaceholderModal ref="contractModalRef" />
|
||||
<SealPositionConfigModal ref="sealPositionConfigModalRef" @saved="handleSealPositionSaved" />
|
||||
<MixedSignModal ref="mixedSignModalRef" @business-updated="handleSignBusinessUpdated" />
|
||||
<UploadSignedModal ref="uploadSignedModalRef" @done="handleSignBusinessUpdated" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { RedoOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { getCurrentMockUser } from '@/api/mockUser'
|
||||
import ContractPartyPlaceholderModal from '../launch/components/ContractPartyPlaceholderModal.vue'
|
||||
import SealPositionConfigModal from '../launch/components/SealPositionConfigModal.vue'
|
||||
import MixedSignModal from '../launch/components/mixedSignModal.vue'
|
||||
import UploadSignedModal from '../launch/components/UploadSignedModal.vue'
|
||||
import {
|
||||
contractTypeOptions,
|
||||
getContractTypeSimpleLabel,
|
||||
} from '../launch/contractLaunchOptions.js'
|
||||
import {
|
||||
downloadLaunchFile,
|
||||
finishLaunchSeal,
|
||||
getLaunchDetail,
|
||||
getLaunchSealPositionPage,
|
||||
getLawApproveCallbackSample,
|
||||
queryApproveViewUrl,
|
||||
queryLaunchList,
|
||||
uploadWatermarkFile,
|
||||
} from '@/api/launch.js'
|
||||
|
||||
const SIGN_STAGE_STATUSES = ['pending_sign', 'signing']
|
||||
const SIGN_LIST_QUERY_PAGE_SIZE = 200
|
||||
const signStatusOptions = [
|
||||
{ label: '待签署', value: 'pending_sign' },
|
||||
{ label: '签署中', value: 'signing' },
|
||||
]
|
||||
|
||||
const contractModalRef = ref(null)
|
||||
const sealPositionConfigModalRef = ref(null)
|
||||
const mixedSignModalRef = ref(null)
|
||||
const uploadSignedModalRef = ref(null)
|
||||
const queryContainer = ref(null)
|
||||
const pageWrapperRef = ref(null)
|
||||
const tableRef = ref(null)
|
||||
const toolbarRef = ref(null)
|
||||
|
||||
const loading = ref(false)
|
||||
const currentMockUser = ref(null)
|
||||
const total = ref(0)
|
||||
const tableHeight = ref(420)
|
||||
const tableData = ref([])
|
||||
const allRecords = ref([])
|
||||
|
||||
const queryParam = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
contractName: undefined,
|
||||
contractCode: undefined,
|
||||
contractType: undefined,
|
||||
status: undefined,
|
||||
dateRange: [],
|
||||
})
|
||||
|
||||
const queryCardBodyStyle = {
|
||||
padding: '20px 24px 6px',
|
||||
}
|
||||
|
||||
const tableCardBodyStyle = {
|
||||
display: 'flex',
|
||||
flexFlow: 'column nowrap',
|
||||
minHeight: '0',
|
||||
padding: '8px 12px 6px',
|
||||
}
|
||||
|
||||
const statusLabelMap = {
|
||||
pending_sign: '待签署',
|
||||
signing: '签署中',
|
||||
pending_collect: '待采集',
|
||||
pending_archive: '待归档',
|
||||
completed: '已完成',
|
||||
}
|
||||
|
||||
const statusColorMap = {
|
||||
pending_sign: 'warning',
|
||||
signing: 'processing',
|
||||
pending_collect: 'cyan',
|
||||
pending_archive: 'orange',
|
||||
completed: 'success',
|
||||
}
|
||||
|
||||
function isSuccessResponse(res) {
|
||||
return String(res?.code || '') === '0000'
|
||||
}
|
||||
|
||||
function getPageResult(res) {
|
||||
return res?.result || {}
|
||||
}
|
||||
|
||||
function getDataResult(res) {
|
||||
return res?.result || null
|
||||
}
|
||||
|
||||
function getResponseMessage(res, fallback = '操作失败') {
|
||||
return res?.message || res?.msg || fallback
|
||||
}
|
||||
|
||||
function sliceDateValue(value) {
|
||||
return value ? String(value).slice(0, 10) : ''
|
||||
}
|
||||
|
||||
function todayString() {
|
||||
const now = new Date()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
return `${now.getFullYear()}-${month}-${day}`
|
||||
}
|
||||
|
||||
function formatDateTimeValue(value) {
|
||||
return value ? `${value} 00:00:00` : null
|
||||
}
|
||||
|
||||
function connectToolbar() {
|
||||
const table = tableRef.value
|
||||
const toolbar = toolbarRef.value
|
||||
if (table && toolbar && table.connect) {
|
||||
table.connect(toolbar)
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusLabel(status) {
|
||||
return statusLabelMap[status] || status
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
return statusColorMap[status] || 'default'
|
||||
}
|
||||
|
||||
function getStatusLabelByRow(row) {
|
||||
return getStatusLabel(row?.status)
|
||||
}
|
||||
|
||||
function isSingleAgreementRecord(row) {
|
||||
return String(row?.isSingleAgreement || '0') === '1'
|
||||
}
|
||||
|
||||
function isManageOperator() {
|
||||
if (currentMockUser.value?.superAdmin) {
|
||||
return true
|
||||
}
|
||||
return String(currentMockUser.value?.systemType || '').toLowerCase() === 'manage'
|
||||
}
|
||||
|
||||
function isSignOperator() {
|
||||
if (currentMockUser.value?.superAdmin) {
|
||||
return true
|
||||
}
|
||||
const systemType = String(currentMockUser.value?.systemType || '').toLowerCase()
|
||||
return systemType === 'manage' || systemType === 'business'
|
||||
}
|
||||
|
||||
function canConfigSealPosition(row) {
|
||||
return isManageOperator()
|
||||
&& !isSingleAgreementRecord(row)
|
||||
&& String(row.isElectron || '1') === '1'
|
||||
&& row.status === 'pending_sign'
|
||||
}
|
||||
|
||||
function canOnlineSign(row) {
|
||||
return isSignOperator()
|
||||
&& !isSingleAgreementRecord(row)
|
||||
&& String(row.isElectron || '1') === '1'
|
||||
&& SIGN_STAGE_STATUSES.includes(row.status)
|
||||
&& String(row.sealPosConfigured || 'N') === 'Y'
|
||||
}
|
||||
|
||||
function canUploadSigned(row) {
|
||||
return String(row.isElectron || '1') === '0' && SIGN_STAGE_STATUSES.includes(row.status)
|
||||
}
|
||||
|
||||
function canMockFinishSign(row) {
|
||||
return String(row.isElectron || '1') === '1' && SIGN_STAGE_STATUSES.includes(row.status)
|
||||
}
|
||||
|
||||
function canDownloadSigned(row) {
|
||||
return Boolean(row.signedFilePath)
|
||||
}
|
||||
|
||||
function canViewApprove(row) {
|
||||
return !isSingleAgreementRecord(row) && Boolean(row.lawContractId)
|
||||
}
|
||||
|
||||
function canViewLawCallback(row) {
|
||||
return !isSingleAgreementRecord(row)
|
||||
}
|
||||
|
||||
function buildQueryPayload(status, pageNo = 1, pageSize = SIGN_LIST_QUERY_PAGE_SIZE) {
|
||||
return {
|
||||
pageNo,
|
||||
pageSize,
|
||||
contractName: queryParam.value.contractName,
|
||||
contractCode: queryParam.value.contractCode,
|
||||
contractType: queryParam.value.contractType,
|
||||
status,
|
||||
dateStart: queryParam.value.dateRange?.[0],
|
||||
dateEnd: queryParam.value.dateRange?.[1],
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRecordsByStatus(status) {
|
||||
let pageNo = 1
|
||||
let totalCount = 0
|
||||
const records = []
|
||||
|
||||
while (true) {
|
||||
const res = await queryLaunchList(buildQueryPayload(status, pageNo))
|
||||
if (!isSuccessResponse(res)) {
|
||||
throw new Error(getResponseMessage(res, '合同签署列表查询失败'))
|
||||
}
|
||||
const pageResult = getPageResult(res)
|
||||
const data = Array.isArray(pageResult?.data) ? pageResult.data : []
|
||||
totalCount = Number(pageResult?.totalSize ?? pageResult?.total ?? 0)
|
||||
records.push(...data)
|
||||
if (!data.length) break
|
||||
if (totalCount > 0 && records.length >= totalCount) break
|
||||
if (data.length < SIGN_LIST_QUERY_PAGE_SIZE) break
|
||||
pageNo += 1
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
function compareRecord(a, b) {
|
||||
const aTime = new Date(a?.createTime || a?.updateTime || 0).getTime()
|
||||
const bTime = new Date(b?.createTime || b?.updateTime || 0).getTime()
|
||||
if (aTime !== bTime) {
|
||||
return bTime - aTime
|
||||
}
|
||||
return Number(b?.id || 0) - Number(a?.id || 0)
|
||||
}
|
||||
|
||||
function refreshTableData() {
|
||||
total.value = allRecords.value.length
|
||||
const pageNo = Number(queryParam.value.pageNo || 1)
|
||||
const pageSize = Number(queryParam.value.pageSize || 10)
|
||||
const startIndex = (pageNo - 1) * pageSize
|
||||
tableData.value = allRecords.value.slice(startIndex, startIndex + pageSize)
|
||||
}
|
||||
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const statuses = queryParam.value.status ? [queryParam.value.status] : SIGN_STAGE_STATUSES
|
||||
const mergedMap = new Map()
|
||||
for (const status of statuses) {
|
||||
const records = await fetchRecordsByStatus(status)
|
||||
records.forEach((item) => {
|
||||
if (!mergedMap.has(item.id)) {
|
||||
mergedMap.set(item.id, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
allRecords.value = Array.from(mergedMap.values()).sort(compareRecord)
|
||||
refreshTableData()
|
||||
} catch (error) {
|
||||
tableData.value = []
|
||||
allRecords.value = []
|
||||
total.value = 0
|
||||
message.error(error?.message || '合同签署列表查询失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuery() {
|
||||
queryParam.value.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryParam.value = {
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
contractName: undefined,
|
||||
contractCode: undefined,
|
||||
contractType: undefined,
|
||||
status: undefined,
|
||||
dateRange: [],
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
function onShowSizeChange(current, pageSize) {
|
||||
queryParam.value.pageNo = current
|
||||
queryParam.value.pageSize = pageSize
|
||||
refreshTableData()
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
getLaunchDetail(row.id).then((res) => {
|
||||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||||
message.error(getResponseMessage(res, '获取合同详情失败'))
|
||||
return
|
||||
}
|
||||
contractModalRef.value?.showModal(getDataResult(res), 'view')
|
||||
})
|
||||
}
|
||||
|
||||
function handleViewApprove(row) {
|
||||
if (!row.lawContractId) {
|
||||
message.warning('当前暂无法务合同ID,审批记录地址尚未生成')
|
||||
return
|
||||
}
|
||||
queryApproveViewUrl({
|
||||
contractId: String(row.lawContractId),
|
||||
ivUser: row.initiator || 'admin',
|
||||
}).then((res) => {
|
||||
if (!isSuccessResponse(res)) {
|
||||
message.error(getResponseMessage(res, '获取审批记录查看地址失败'))
|
||||
return
|
||||
}
|
||||
const approveViewUrl = res?.result?.approveViewUrl
|
||||
if (!approveViewUrl) {
|
||||
message.warning('审批记录地址为空')
|
||||
return
|
||||
}
|
||||
window.open(approveViewUrl, '_blank')
|
||||
})
|
||||
}
|
||||
|
||||
function buildLawCallbackModalContent(sample) {
|
||||
const approvedJson = JSON.stringify({ data: sample?.approvedData || {} }, null, 2)
|
||||
const rejectedJson = JSON.stringify({ data: sample?.rejectedData || {} }, null, 2)
|
||||
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '12px' } }, [
|
||||
h('div', [
|
||||
h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '回调地址'),
|
||||
h('div', { style: { color: '#666', wordBreak: 'break-all' } }, `${sample?.callbackMethod || 'POST'} ${sample?.callbackUrl || '/contract/launch/callback/law/approve'}`),
|
||||
]),
|
||||
h('div', [
|
||||
h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '审批通过 data'),
|
||||
h('pre', {
|
||||
style: {
|
||||
margin: 0,
|
||||
maxHeight: '260px',
|
||||
overflow: 'auto',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
background: '#f6f8fa',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
}, approvedJson),
|
||||
]),
|
||||
h('div', [
|
||||
h('div', { style: { marginBottom: '6px', fontWeight: 600 } }, '审批驳回 data'),
|
||||
h('pre', {
|
||||
style: {
|
||||
margin: 0,
|
||||
maxHeight: '220px',
|
||||
overflow: 'auto',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
background: '#f6f8fa',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.6',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
}, rejectedJson),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
function handleViewLawCallback(row) {
|
||||
getLawApproveCallbackSample(row.id).then((res) => {
|
||||
if (!isSuccessResponse(res) || !getDataResult(res)) {
|
||||
message.error(getResponseMessage(res, '获取法务回调样例失败'))
|
||||
return
|
||||
}
|
||||
const sample = getDataResult(res)
|
||||
Modal.info({
|
||||
title: `法务回调 data 结构:${row.contractName}`,
|
||||
width: 900,
|
||||
okText: '知道了',
|
||||
content: buildLawCallbackModalContent(sample),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleMockFinishSign(row) {
|
||||
Modal.confirm({
|
||||
title: '测试操作:模拟签署完成',
|
||||
content: `模拟合同【${row.contractName}】签署完成,状态将变为「待采集」。`,
|
||||
okText: '确认模拟',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
const signDate = sliceDateValue(row.signDate) || todayString()
|
||||
const sealDate = sliceDateValue(row.sealDate) || signDate
|
||||
const activeDate = sliceDateValue(row.activeDate) || sliceDateValue(row.effectiveStart) || signDate
|
||||
return finishLaunchSeal(row.id, {
|
||||
sealContractId: row.sealContractId || `TEST-SEAL-${row.id}`,
|
||||
sealContractStatus: 2000,
|
||||
signedFileName: `${row.contractName || '合同'}-测试已签署.pdf`,
|
||||
signedFilePath: row.contractTextFilePath || `mock-signed/${row.id}.pdf`,
|
||||
signedFileType: 'pdf',
|
||||
signedFileSize: row.contractTextFileSize || null,
|
||||
signedOfflineReason: '测试按钮模拟签署完成,真实签章接入后可删除此按钮',
|
||||
signDate: formatDateTimeValue(signDate),
|
||||
sealDate: formatDateTimeValue(sealDate),
|
||||
activeDate: formatDateTimeValue(activeDate),
|
||||
}).then((res) => {
|
||||
if (!isSuccessResponse(res)) {
|
||||
message.error(getResponseMessage(res, '测试签署完成失败'))
|
||||
return
|
||||
}
|
||||
message.success('测试签署完成,合同已流转到待采集')
|
||||
getList()
|
||||
})
|
||||
},
|
||||
icon: null,
|
||||
})
|
||||
}
|
||||
|
||||
function handleDownloadSigned(row) {
|
||||
const filePath = row.signedFilePath
|
||||
const fileName = row.signedFileName || `${row.contractName || '合同'}-已签署版.pdf`
|
||||
if (!filePath) {
|
||||
message.warning('暂无签署文件可下载')
|
||||
return
|
||||
}
|
||||
downloadLaunchFile(filePath)
|
||||
.then((res) => {
|
||||
const arrayBuffer = res?.data ?? res
|
||||
const blob = new Blob([arrayBuffer])
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('签署文件下载失败')
|
||||
})
|
||||
}
|
||||
|
||||
function handleUploadSigned(row) {
|
||||
uploadSignedModalRef.value?.showModal(row)
|
||||
}
|
||||
|
||||
async function ensureDetailReady(row, actionLabel) {
|
||||
const detailRes = await getLaunchDetail(row.id)
|
||||
if (!isSuccessResponse(detailRes) || !getDataResult(detailRes)) {
|
||||
message.error(getResponseMessage(detailRes, '获取合同详情失败'))
|
||||
return null
|
||||
}
|
||||
let detail = getDataResult(detailRes)
|
||||
if (detail.contractTextFilePath) {
|
||||
return detail
|
||||
}
|
||||
const uploaded = await handleWatermarkUpload(row, actionLabel)
|
||||
if (!uploaded) {
|
||||
return null
|
||||
}
|
||||
const reloadRes = await getLaunchDetail(row.id)
|
||||
if (!isSuccessResponse(reloadRes) || !getDataResult(reloadRes)) {
|
||||
message.error(getResponseMessage(reloadRes, '获取合同详情失败'))
|
||||
return null
|
||||
}
|
||||
detail = getDataResult(reloadRes)
|
||||
return detail
|
||||
}
|
||||
|
||||
async function handleConfigSealPosition(row) {
|
||||
const detail = await ensureDetailReady(row, '配置签章位')
|
||||
if (!detail) return
|
||||
sealPositionConfigModalRef.value?.showModal(detail)
|
||||
}
|
||||
|
||||
async function handleOnlineSign(row) {
|
||||
const detail = await ensureDetailReady(row, '在线签订')
|
||||
if (!detail) return
|
||||
try {
|
||||
const pageRes = await getLaunchSealPositionPage(row.id)
|
||||
if (!isSuccessResponse(pageRes) || !getDataResult(pageRes)) {
|
||||
message.error(getResponseMessage(pageRes, '获取签章位失败'))
|
||||
return
|
||||
}
|
||||
const page = getDataResult(pageRes)
|
||||
if (!Array.isArray(page.positions) || !page.positions.length) {
|
||||
message.warning('当前合同还未配置签章位,请先点击“配置签章位”')
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(page.availablePartyIds) || !page.availablePartyIds.length) {
|
||||
message.warning('当前用户未匹配到可签署方,暂时不能在线签订')
|
||||
return
|
||||
}
|
||||
mixedSignModalRef.value?.showModal({
|
||||
...detail,
|
||||
contractId: detail.sealContractId,
|
||||
})
|
||||
} catch {
|
||||
message.error('获取签章位失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWatermarkUpload(row, actionLabel = '在线签订') {
|
||||
let file
|
||||
const ok = await new Promise((resolve) => {
|
||||
Modal.confirm({
|
||||
title: '法务水印文件缺失',
|
||||
content: h('div', [
|
||||
h('p', { style: 'margin-bottom: 12px' }, `法务审批回调未带回带水印的合同文件,请先上传后再${actionLabel}。`),
|
||||
h('input', {
|
||||
type: 'file',
|
||||
accept: '.pdf,.doc,.docx',
|
||||
style: 'margin-top: 8px; width: 100%',
|
||||
onChange(e) {
|
||||
file = e.target.files?.[0]
|
||||
},
|
||||
}),
|
||||
]),
|
||||
okText: '上传并继续',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
if (!file) {
|
||||
message.warning('请先选择文件')
|
||||
return false
|
||||
}
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
try {
|
||||
const res = await uploadWatermarkFile(row.id, formData)
|
||||
if (!isSuccessResponse(res)) {
|
||||
message.error(getResponseMessage(res, '上传水印文件失败'))
|
||||
return false
|
||||
}
|
||||
message.success('水印文件上传成功')
|
||||
resolve(true)
|
||||
} catch {
|
||||
message.error('上传水印文件失败')
|
||||
return false
|
||||
}
|
||||
},
|
||||
onCancel() {
|
||||
resolve(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
return ok
|
||||
}
|
||||
|
||||
function handleSealPositionSaved() {
|
||||
getList()
|
||||
}
|
||||
|
||||
function handleSignBusinessUpdated() {
|
||||
getList()
|
||||
}
|
||||
|
||||
async function loadCurrentMockUser() {
|
||||
try {
|
||||
currentMockUser.value = await getCurrentMockUser()
|
||||
} catch {
|
||||
currentMockUser.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function syncTableHeight() {
|
||||
nextTick(() => {
|
||||
const viewportHeight = window.innerHeight
|
||||
const queryHeight = queryContainer.value?.offsetHeight || 0
|
||||
const pageHeight = pageWrapperRef.value?.$el?.offsetHeight || 56
|
||||
tableHeight.value = Math.max(320, viewportHeight - queryHeight - pageHeight - 320)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCurrentMockUser()
|
||||
handleQuery()
|
||||
nextTick(() => {
|
||||
connectToolbar()
|
||||
syncTableHeight()
|
||||
})
|
||||
window.addEventListener('resize', syncTableHeight)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', syncTableHeight)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section-container {
|
||||
height: calc(100vh - 116px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.query-card {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.section-query-container :deep(.ant-form-item) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.section-query-container :deep(.ant-form-item .ant-form-item-control) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 12px 4px 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.section-container {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -35,6 +35,10 @@ export default defineConfig({
|
||||
target: 'http://127.0.0.1:8899',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/mock': {
|
||||
target: 'http://127.0.0.1:8899',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/trans': {
|
||||
target: 'http://127.0.0.1:8899',
|
||||
changeOrigin: true,
|
||||
|
||||
Reference in New Issue
Block a user