Compare commits

..
10 Commits
Author SHA1 Message Date
ifly b909dd693e 富文本优化 2026-07-14 13:09:35 +08:00
ifly 3ded1868a3 富文本问题 2026-07-14 12:58:25 +08:00
ifly c36003db2f 带出甲乙方数据,下载权限控制 2026-07-13 13:47:19 +08:00
ifly a6e4e74ad1 签章位置固定功能 2026-07-10 17:03:55 +08:00
ifly fc969390ae 补足用户切换功能 2026-07-09 16:07:56 +08:00
ifly 6f20fec8de 审批完成的合同,在合同签署界面进行在线签署。合同发起界面不能在线签署。 2026-07-09 15:12:34 +08:00
ifly e03e019f58 1. 变量占位符:{中文变量}
2. 变量占位符预设类型,可以是:输入框、下拉、富文本(允许插入表格作为合同正本的附件内容)
2026-07-09 14:08:43 +08:00
ifly 2de0a12d44 细节修改 2026-07-08 08:37:05 +08:00
ifly fa7be38e7f 相对方确认签署 2026-07-01 13:00:33 +08:00
ifly c5b744737d 日志翻译 2026-07-01 09:26:23 +08:00
25 changed files with 7637 additions and 399 deletions
+3
View File
@@ -10,6 +10,9 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^7.0.1", "@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", "ant-design-vue": "^4.2.6",
"axios": "^1.16.0", "axios": "^1.16.0",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
+488
View File
@@ -11,6 +11,15 @@ importers:
'@ant-design/icons-vue': '@ant-design/icons-vue':
specifier: ^7.0.1 specifier: ^7.0.1
version: 7.0.1(vue@3.5.34) 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: ant-design-vue:
specifier: ^4.2.6 specifier: ^4.2.6
version: 4.2.6(vue@3.5.34) version: 4.2.6(vue@3.5.34)
@@ -402,12 +411,35 @@ packages:
'@simonwep/pickr@1.8.2': '@simonwep/pickr@1.8.2':
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==} resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
'@transloadit/prettier-bytes@0.0.7':
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/event-emitter@0.3.5':
resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
'@types/node@25.9.1': '@types/node@25.9.1':
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} 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': '@vitejs/plugin-vue@5.2.4':
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
engines: {node: ^18.0.0 || >=20.0.0} engines: {node: ^18.0.0 || >=20.0.0}
@@ -452,6 +484,97 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.2.0 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': '@xmldom/xmldom@0.9.10':
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
engines: {node: '>=14.6'} engines: {node: '>=14.6'}
@@ -498,6 +621,10 @@ packages:
csstype@3.2.3: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 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: dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
@@ -534,6 +661,9 @@ packages:
dom-zindex@1.0.6: dom-zindex@1.0.6:
resolution: {integrity: sha512-FKWIhiU96bi3xpP9ewRMgANsoVmMUBnMnmpCT6dPMZOunVYJQmJhSRruoI0XSPoHeIif3kyEuiHbFrOJwEJaEA==} resolution: {integrity: sha512-FKWIhiU96bi3xpP9ewRMgANsoVmMUBnMnmpCT6dPMZOunVYJQmJhSRruoI0XSPoHeIif3kyEuiHbFrOJwEJaEA==}
dom7@3.0.0:
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -558,14 +688,35 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} 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: esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
esniff@2.0.1:
resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
engines: {node: '>=0.10'}
estree-walker@2.0.2: estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 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: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -626,20 +777,39 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
html-void-elements@2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
i18next@20.6.1:
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
immediate@3.0.6: immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
inherits@2.0.4: inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 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: is-plain-object@3.0.1:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==} resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'} 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: isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
@@ -655,6 +825,28 @@ packages:
lodash-es@4.18.1: lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} 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: lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@@ -673,6 +865,9 @@ packages:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-match@1.0.2:
resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
mime-types@2.1.35: mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -683,6 +878,9 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 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: nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -696,6 +894,9 @@ packages:
nanopop@2.4.2: nanopop@2.4.2:
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==} resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
next-tick@1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
pako@1.0.11: pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@@ -725,6 +926,18 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14} 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: process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -759,10 +972,25 @@ packages:
shallow-equal@1.2.1: shallow-equal@1.2.1:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} 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: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
ssr-window@3.0.0:
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
string_decoder@1.1.1: string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@@ -773,10 +1001,16 @@ packages:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'} engines: {node: '>=12.22'}
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinyglobby@0.2.16: tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'} 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: undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
@@ -862,6 +1096,9 @@ packages:
warning@4.0.3: warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
wildcard@1.1.2:
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
xe-utils@4.0.8: xe-utils@4.0.8:
resolution: {integrity: sha512-l+p++9iPtSxpf/5ZCzkPXsyV0avE34KzieAC9oAhi+3e98PLJaBQHNiY2V8Lnbiux4JFAbU10p8AT6oujXkP9A==} resolution: {integrity: sha512-l+p++9iPtSxpf/5ZCzkPXsyV0avE34KzieAC9oAhi+3e98PLJaBQHNiY2V8Lnbiux4JFAbU10p8AT6oujXkP9A==}
@@ -1067,12 +1304,47 @@ snapshots:
core-js: 3.49.0 core-js: 3.49.0
nanopop: 2.4.2 nanopop: 2.4.2
'@transloadit/prettier-bytes@0.0.7': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/event-emitter@0.3.5': {}
'@types/node@25.9.1': '@types/node@25.9.1':
dependencies: dependencies:
undici-types: 7.24.6 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)': '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@25.9.1))(vue@3.5.34)':
dependencies: dependencies:
vite: 6.4.2(@types/node@25.9.1) vite: 6.4.2(@types/node@25.9.1)
@@ -1140,6 +1412,118 @@ snapshots:
vue: 3.5.34 vue: 3.5.34
xe-utils: 4.0.8 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': {} '@xmldom/xmldom@0.9.10': {}
agent-base@6.0.2: agent-base@6.0.2:
@@ -1207,6 +1591,11 @@ snapshots:
csstype@3.2.3: {} csstype@3.2.3: {}
d@1.0.2:
dependencies:
es5-ext: 0.10.64
type: 2.7.3
dayjs@1.11.20: {} dayjs@1.11.20: {}
debug@4.4.3: debug@4.4.3:
@@ -1238,6 +1627,10 @@ snapshots:
dom-zindex@1.0.6: {} dom-zindex@1.0.6: {}
dom7@3.0.0:
dependencies:
ssr-window: 3.0.0
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -1261,6 +1654,24 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.3 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: esbuild@0.25.12:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12 '@esbuild/aix-ppc64': 0.25.12
@@ -1290,8 +1701,24 @@ snapshots:
'@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 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: {} 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): fdir@6.5.0(picomatch@4.0.4):
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
@@ -1348,6 +1775,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
html-void-elements@2.0.1: {}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
@@ -1355,12 +1784,24 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
i18next@20.6.1:
dependencies:
'@babel/runtime': 7.29.2
immediate@3.0.6: {} immediate@3.0.6: {}
immer@9.0.21: {}
inherits@2.0.4: {} inherits@2.0.4: {}
is-hotkey@0.2.0: {}
is-plain-object@3.0.1: {} is-plain-object@3.0.1: {}
is-plain-object@5.0.0: {}
is-url@1.2.4: {}
isarray@1.0.0: {} isarray@1.0.0: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -1378,6 +1819,20 @@ snapshots:
lodash-es@4.18.1: {} 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: {} lodash@4.18.1: {}
loose-envify@1.4.0: loose-envify@1.4.0:
@@ -1392,6 +1847,10 @@ snapshots:
mime-db@1.52.0: {} mime-db@1.52.0: {}
mime-match@1.0.2:
dependencies:
wildcard: 1.1.2
mime-types@2.1.35: mime-types@2.1.35:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
@@ -1400,12 +1859,16 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
namespace-emitter@2.0.1: {}
nanoid@3.3.12: {} nanoid@3.3.12: {}
nanoid@5.1.11: {} nanoid@5.1.11: {}
nanopop@2.4.2: {} nanopop@2.4.2: {}
next-tick@1.1.0: {}
pako@1.0.11: {} pako@1.0.11: {}
pako@2.1.0: {} pako@2.1.0: {}
@@ -1432,6 +1895,10 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
preact@10.29.7: {}
prismjs@1.30.0: {}
process-nextick-args@2.0.1: {} process-nextick-args@2.0.1: {}
proxy-from-env@2.1.0: {} proxy-from-env@2.1.0: {}
@@ -1491,8 +1958,23 @@ snapshots:
shallow-equal@1.2.1: {} 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: {} source-map-js@1.2.1: {}
ssr-window@3.0.0: {}
string_decoder@1.1.1: string_decoder@1.1.1:
dependencies: dependencies:
safe-buffer: 5.1.2 safe-buffer: 5.1.2
@@ -1501,11 +1983,15 @@ snapshots:
throttle-debounce@5.0.2: {} throttle-debounce@5.0.2: {}
tiny-warning@1.0.3: {}
tinyglobby@0.2.16: tinyglobby@0.2.16:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4 picomatch: 4.0.4
type@2.7.3: {}
undici-types@7.24.6: {} undici-types@7.24.6: {}
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
@@ -1560,6 +2046,8 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
wildcard@1.1.2: {}
xe-utils@4.0.8: {} xe-utils@4.0.8: {}
xml-js@1.6.11: xml-js@1.6.11:
+47
View File
@@ -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) { export function queryApproveViewUrl(data) {
return request({ return request({
url: "/contract/push/settlement/approve/view-url", 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) { export function queryContractLogList(data) {
return request({ return request({
url: "/contract/log/list", url: "/contract/log/list",
+30
View File
@@ -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
View File
@@ -175,7 +175,7 @@ export function downloadContract(contractId) {
} }
// ======================================== // ========================================
// 回调轮询 // 回调轮询 / 测试回调
// ======================================== // ========================================
export function pollCallback(requestId) { export function pollCallback(requestId) {
return request({ return request({
@@ -183,3 +183,11 @@ export function pollCallback(requestId) {
method: 'get', method: 'get',
}) })
} }
export function mockPreSignSuccessCallback(data) {
return request({
url: '/seal/callback/presign/mock-success',
method: 'post',
data,
})
}
+108 -24
View File
@@ -3,7 +3,6 @@
<a-layout-sider width="244" class="contract-sider"> <a-layout-sider width="244" class="contract-sider">
<div class="sider-brand"> <div class="sider-brand">
<div class="brand-badge">合同管理</div> <div class="brand-badge">合同管理</div>
</div> </div>
<a-menu <a-menu
@@ -16,6 +15,10 @@
<form-outlined /> <form-outlined />
<span>发起合同</span> <span>发起合同</span>
</a-menu-item> </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"> <a-menu-item key="/contract-console/manifest">
<file-text-outlined /> <file-text-outlined />
<span>模板管理</span> <span>模板管理</span>
@@ -32,6 +35,21 @@
<div class="sider-footer"> <div class="sider-footer">
<a-button block @click="router.push('/')">返回首页</a-button> <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> </div>
</a-layout-sider> </a-layout-sider>
@@ -39,9 +57,7 @@
<a-layout-header class="contract-header"> <a-layout-header class="contract-header">
<div> <div>
<div class="header-title">{{ currentTitle }}</div> <div class="header-title">{{ currentTitle }}</div>
</div> </div>
</a-layout-header> </a-layout-header>
<a-layout-content class="contract-content"> <a-layout-content class="contract-content">
@@ -52,14 +68,34 @@
</template> </template>
<script setup> <script setup>
import { computed } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' 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 route = useRoute()
const router = useRouter() const router = useRouter()
const mockUserLoading = ref(false)
const mockUserList = ref([])
const currentMockUser = ref(null)
const selectedMockUserKey = ref(getStoredMockUser())
const selectedKey = computed(() => { const selectedKey = computed(() => {
if (route.path.startsWith('/contract-console/sign')) {
return '/contract-console/sign'
}
if (route.path.startsWith('/contract-console/manifest')) { if (route.path.startsWith('/contract-console/manifest')) {
return '/contract-console/manifest' return '/contract-console/manifest'
} }
@@ -77,9 +113,49 @@ const selectedKey = computed(() => {
const currentTitle = computed(() => route.meta?.title || '合同管理') const currentTitle = computed(() => route.meta?.title || '合同管理')
const mockUserOptions = computed(() =>
mockUserList.value.map((item) => ({
label: `${item.displayName} (${item.orgId})`,
value: item.key,
})),
)
function handleMenuClick({ key }) { function handleMenuClick({ key }) {
router.push(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> </script>
<style scoped> <style scoped>
@@ -114,19 +190,6 @@ function handleMenuClick({ key }) {
margin-bottom: 12px; 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 { .contract-menu {
border-inline-end: none; border-inline-end: none;
background: transparent; background: transparent;
@@ -151,6 +214,33 @@ function handleMenuClick({ key }) {
padding: 16px 20px 20px; 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 { .contract-header {
height: auto; height: auto;
background: rgba(255, 255, 255, 0.96); background: rgba(255, 255, 255, 0.96);
@@ -169,12 +259,6 @@ function handleMenuClick({ key }) {
margin-bottom: 4px; margin-bottom: 4px;
} }
.header-desc {
font-size: 13px;
color: #64748b;
line-height: 1.8;
}
.contract-content { .contract-content {
padding: 20px; padding: 20px;
min-height: 0; min-height: 0;
+8
View File
@@ -19,6 +19,14 @@ const routes = [
title: '发起合同', title: '发起合同',
}, },
}, },
{
path: 'sign',
name: 'ContractSignManage',
component: () => import('@/views/contract/sign/index.vue'),
meta: {
title: '合同签署',
},
},
{ {
path: 'manifest', path: 'manifest',
name: 'ContractTemplateManage', name: 'ContractTemplateManage',
+15
View File
@@ -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)
}
+3
View File
@@ -1,4 +1,5 @@
import axios from 'axios' import axios from 'axios'
import { DEFAULT_MOCK_USER, getStoredMockUser, MOCK_USER_HEADER } from '@/utils/mockUserStorage'
import { message, Modal, notification } from 'ant-design-vue' import { message, Modal, notification } from 'ant-design-vue'
const service = axios.create({ const service = axios.create({
@@ -12,6 +13,8 @@ service.interceptors.request.use(
if (token) { if (token) {
config.headers['Authorization'] = 'Bearer ' + token config.headers['Authorization'] = 'Bearer ' + token
} }
const mockUser = getStoredMockUser() || DEFAULT_MOCK_USER
config.headers[MOCK_USER_HEADER] = mockUser
return config return config
}, },
(error) => Promise.reject(error), (error) => Promise.reject(error),
@@ -8,6 +8,7 @@
ok-text="确认采集" ok-text="确认采集"
cancel-text="取消" cancel-text="取消"
@ok="handleSubmit" @ok="handleSubmit"
centered
> >
<div class="collect-body"> <div class="collect-body">
<a-descriptions :column="2" size="small" bordered class="contract-brief"> <a-descriptions :column="2" size="small" bordered class="contract-brief">
@@ -707,15 +707,15 @@
<a-radio-group <a-radio-group
v-model:value="detailState.isInvolveHongKongMacaoTaiwan" v-model:value="detailState.isInvolveHongKongMacaoTaiwan"
> >
<a-radio :value="true">是</a-radio> <a-radio value="1">是</a-radio>
<a-radio :value="false">否</a-radio> <a-radio value="0">否</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div class="boolean-row"> <div class="boolean-row">
<span>是否公开招标</span> <span>是否公开招标</span>
<a-radio-group v-model:value="detailState.isPublicTender"> <a-radio-group v-model:value="detailState.isPublicTender">
<a-radio :value="true">是</a-radio> <a-radio value="1">是</a-radio>
<a-radio :value="false">否</a-radio> <a-radio value="0">否</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div class="boolean-row"> <div class="boolean-row">
@@ -723,8 +723,8 @@
<a-radio-group <a-radio-group
v-model:value="detailState.isForeignRelatedSubject" v-model:value="detailState.isForeignRelatedSubject"
> >
<a-radio :value="true">是</a-radio> <a-radio value="1">是</a-radio>
<a-radio :value="false">否</a-radio> <a-radio value="0">否</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div <div
@@ -735,22 +735,22 @@
<a-radio-group <a-radio-group
v-model:value="detailState.isIncludedInCurrentYearBudget" v-model:value="detailState.isIncludedInCurrentYearBudget"
> >
<a-radio :value="true">是</a-radio> <a-radio value="1">是</a-radio>
<a-radio :value="false">否</a-radio> <a-radio value="0">否</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div class="boolean-row"> <div class="boolean-row">
<span>是否共享</span> <span>是否共享</span>
<a-radio-group v-model:value="detailState.isShared"> <a-radio-group v-model:value="detailState.isShared">
<a-radio :value="true">是</a-radio> <a-radio value="1">是</a-radio>
<a-radio :value="false">否</a-radio> <a-radio value="0">否</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div class="boolean-row"> <div class="boolean-row">
<span>是否重大合同</span> <span>是否重大合同</span>
<a-radio-group v-model:value="detailState.isMajorContract"> <a-radio-group v-model:value="detailState.isMajorContract">
<a-radio :value="true">是</a-radio> <a-radio value="1">是</a-radio>
<a-radio :value="false">否</a-radio> <a-radio value="0">否</a-radio>
</a-radio-group> </a-radio-group>
</div> </div>
<div class="boolean-row"> <div class="boolean-row">
@@ -1043,12 +1043,9 @@ function normalizeFileItem(item = {}) {
} }
function normalizeBooleanValue(value) { function normalizeBooleanValue(value) {
if (value === true || value === false) return value; if (value === 1 || value === "1" || value === true || value === "Y" || value === "true")
if (value === 1 || value === "1" || value === "Y" || value === "true") return "1";
return true; return "0";
if (value === 0 || value === "0" || value === "N" || value === "false")
return false;
return false;
} }
function pickBooleanSource(source = {}, key) { function pickBooleanSource(source = {}, key) {
@@ -1093,21 +1090,24 @@ function splitFilesByCategory(source = {}) {
return { return {
contractBodyFileName: contractBodyFileName:
getFileNameFromDetail(source, "body") || bodyFile.fileName || "", source.contractBodyFileName ||
bodyFile.fileName ||
getFileNameFromDetail(source, "body") ||
"",
contractBodyFilePath: contractBodyFilePath:
source.contractBodyFilePath || source.contractBodyFilePath ||
source.contractTextFilePath ||
bodyFile.filePath || bodyFile.filePath ||
source.contractTextFilePath ||
"", "",
contractBodyFileType: contractBodyFileType:
source.contractBodyFileType || source.contractBodyFileType ||
source.contractTextFileType ||
bodyFile.fileType || bodyFile.fileType ||
source.contractTextFileType ||
"", "",
contractBodyFileSize: contractBodyFileSize:
source.contractBodyFileSize ?? source.contractBodyFileSize ??
source.contractTextFileSize ??
bodyFile.fileSize ?? bodyFile.fileSize ??
source.contractTextFileSize ??
null, null,
otherAttachmentList: source.otherAttachmentList?.length otherAttachmentList: source.otherAttachmentList?.length
? source.otherAttachmentList.map(normalizeFileItem) ? source.otherAttachmentList.map(normalizeFileItem)
@@ -1184,6 +1184,9 @@ function createEmptyParty(roleCode, index = 0) {
oppositeId: "", oppositeId: "",
companyName: "", companyName: "",
companyType: "", companyType: "",
companyNature: "",
orgNature: "",
orgNatureLevel: "",
oppositeCharacter: "", oppositeCharacter: "",
oppCharacter: "", oppCharacter: "",
contactName: "", contactName: "",
@@ -1193,6 +1196,8 @@ function createEmptyParty(roleCode, index = 0) {
registeredAddress: "", registeredAddress: "",
legalRepresentative: "", legalRepresentative: "",
creditCode: "", creditCode: "",
registeredCapitalText: "",
countryName: "",
naturalPersonIdType: "", naturalPersonIdType: "",
naturalPersonIdNumber: "", naturalPersonIdNumber: "",
isGroupInternal: false, isGroupInternal: false,
@@ -1405,12 +1410,12 @@ function createContractFormData() {
counterpartyList: [], counterpartyList: [],
}, },
isSingleAgreement: "0", isSingleAgreement: "0",
isInvolveHongKongMacaoTaiwan: false, isInvolveHongKongMacaoTaiwan: "0",
isPublicTender: false, isPublicTender: "0",
isForeignRelatedSubject: false, isForeignRelatedSubject: "0",
isIncludedInCurrentYearBudget: false, isIncludedInCurrentYearBudget: "0",
isShared: false, isShared: "0",
isMajorContract: false, isMajorContract: "0",
needAmendments: "0", needAmendments: "0",
extraAmounts: { extraAmounts: {
depositEnabled: false, depositEnabled: false,
@@ -1734,6 +1739,9 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
oppositeId: raw.oppositeId || "", oppositeId: raw.oppositeId || "",
companyName: raw.customerNameC || companyName || "", companyName: raw.customerNameC || companyName || "",
companyType: getRelativeTypeLabel(raw.relativeType), companyType: getRelativeTypeLabel(raw.relativeType),
companyNature: raw.oppositeCharacter || "",
orgNature: raw.natureOfEnterpriseOrg || "",
orgNatureLevel: raw.natureOfEnterpriseOrgLev || "",
oppositeCharacter: raw.oppositeCharacter || "", oppositeCharacter: raw.oppositeCharacter || "",
oppCharacter: raw.oppositeCharacter || "", oppCharacter: raw.oppositeCharacter || "",
creditCode: raw.customerTaxno || "", creditCode: raw.customerTaxno || "",
@@ -1743,6 +1751,8 @@ function mapPartnerToParty(raw = {}, roleKey, companyName) {
contactPhone: firstContact.contactPhone || raw.officephone || "", contactPhone: firstContact.contactPhone || raw.officephone || "",
contactEmail: firstContact.email || raw.emailAddress || "", contactEmail: firstContact.email || raw.emailAddress || "",
contactAddress: firstContact.postalAddress || raw.customerAddress || "", contactAddress: firstContact.postalAddress || raw.customerAddress || "",
registeredCapitalText: raw.regCapital || raw.registeredCapital || "",
countryName: raw.countryName || raw.country || "",
naturalPersonIdType: raw.certificateType || "", naturalPersonIdType: raw.certificateType || "",
naturalPersonIdNumber: raw.certificateNum || "", naturalPersonIdNumber: raw.certificateNum || "",
isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1", isGroupInternal: raw.groupFlag === "Y" || raw.isJtn === "1",
@@ -1757,12 +1767,19 @@ function mapOrgNodeToParty(raw = {}) {
...createEmptyParty(PARTY_ROLE_OUR), ...createEmptyParty(PARTY_ROLE_OUR),
subjectCode: raw.orgId == null ? "" : String(raw.orgId), subjectCode: raw.orgId == null ? "" : String(raw.orgId),
companyName: raw.orgName || "", companyName: raw.orgName || "",
companyNature: raw.companyNature || "1",
orgNature: raw.orgNature || "QY",
orgNatureLevel: raw.orgNatureLevel || "",
contactPhone: raw.contactPhone || "", contactPhone: raw.contactPhone || "",
contactEmail: raw.contactEmail || "", contactEmail: raw.contactEmail || "",
contactAddress: raw.contactAddress || "", contactAddress: raw.contactAddress || "",
registeredAddress: raw.registeredAddress || "", registeredAddress: raw.registeredAddress || "",
legalRepresentative: raw.legalRepresentative || "", legalRepresentative: raw.legalRepresentative || "",
creditCode: raw.creditCode || "", creditCode: raw.creditCode || "",
registeredCapitalText: raw.regCapital || raw.registeredCapital || "",
countryName: raw.countryName || raw.country || "",
isGroupInternal: true,
isHongKongMacaoTaiwan: false,
roleCode: PARTY_ROLE_OUR, roleCode: PARTY_ROLE_OUR,
roleName: "我方", roleName: "我方",
}; };
@@ -2172,9 +2189,9 @@ function validateSubmit() {
// 以下为 pushContract 必填项补充校验(接口文档要求) // 以下为 pushContract 必填项补充校验(接口文档要求)
if (detailState.value.isMajorContract === undefined || detailState.value.isMajorContract === null || detailState.value.isMajorContract === "") if (detailState.value.isMajorContract === undefined || detailState.value.isMajorContract === null || detailState.value.isMajorContract === "")
return "请选择是否重大合同"; return "请选择是否重大合同";
if (detailState.value.isPublicTender === undefined || detailState.value.isPublicTender === null) if (detailState.value.isPublicTender === undefined || detailState.value.isPublicTender === null || detailState.value.isPublicTender === "")
return "请选择是否公开招标"; return "请选择是否公开招标";
if (detailState.value.isInvolveHongKongMacaoTaiwan === undefined || detailState.value.isInvolveHongKongMacaoTaiwan === null) if (detailState.value.isInvolveHongKongMacaoTaiwan === undefined || detailState.value.isInvolveHongKongMacaoTaiwan === null || detailState.value.isInvolveHongKongMacaoTaiwan === "")
return "请选择是否涉及港澳台"; return "请选择是否涉及港澳台";
if (!detailState.value.paymentDirection) return "请选择收支方向"; if (!detailState.value.paymentDirection) return "请选择收支方向";
if (!detailState.value.contractTypeCode) return "请选择合同类型编码"; if (!detailState.value.contractTypeCode) return "请选择合同类型编码";
@@ -2376,12 +2393,12 @@ function buildLaunchRecord() {
isElectron: detailState.value.isElectron || "1", isElectron: detailState.value.isElectron || "1",
isSingleAgreement: detailState.value.isSingleAgreement || "0", isSingleAgreement: detailState.value.isSingleAgreement || "0",
paymentDirection: detailState.value.paymentDirection || "0", paymentDirection: detailState.value.paymentDirection || "0",
isMajorContract: detailState.value.isMajorContract ? 1 : 0, isMajorContract: detailState.value.isMajorContract,
isOpenBidding: detailState.value.isPublicTender ? 1 : 0, isOpenBidding: detailState.value.isPublicTender,
isYearBudget: detailState.value.isIncludedInCurrentYearBudget ? 1 : 0, isYearBudget: detailState.value.isIncludedInCurrentYearBudget,
isShare: detailState.value.isShared ? "1" : "0", isShare: detailState.value.isShared,
contractNature: detailState.value.isForeignRelatedSubject ? "1" : "0", contractNature: detailState.value.isForeignRelatedSubject,
isSgat: detailState.value.isInvolveHongKongMacaoTaiwan ? "1" : "0", isSgat: detailState.value.isInvolveHongKongMacaoTaiwan,
biddingNo: detailState.value.isPublicTender biddingNo: detailState.value.isPublicTender
? detailState.value.biddingNo || "" ? detailState.value.biddingNo || ""
: "", : "",
@@ -71,12 +71,34 @@
v-for="field in launchFieldList" v-for="field in launchFieldList"
:key="field.key" :key="field.key"
:label="field.label" :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)" :value="getLaunchFieldValue(field.key)"
:placeholder="`请输入${field.label}`" :options="getFieldDictOptions(field)"
:readonly="field.readonly" :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)" @update:value="setLaunchFieldValue(field.key, $event)"
/> />
</a-form-item> </a-form-item>
@@ -177,7 +199,6 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue"; import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import Docxtemplater from "docxtemplater";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { import {
FileWordOutlined, FileWordOutlined,
@@ -185,9 +206,9 @@ import {
SettingOutlined, SettingOutlined,
} from "@ant-design/icons-vue"; } from "@ant-design/icons-vue";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
import PizZip from "pizzip";
import { renderAsync } from "docx-preview"; import { renderAsync } from "docx-preview";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { getDictDataByType } from "@/api/dict.js";
import { import {
downloadTemplateFile, downloadTemplateFile,
queryPublishedManifestList, queryPublishedManifestList,
@@ -197,6 +218,10 @@ import {
buildTemplateFieldLabelMap, buildTemplateFieldLabelMap,
loadTemplateFieldList, loadTemplateFieldList,
} from "../../shared/templateFieldDict.js"; } 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 { import {
extraAmountFieldOptions, extraAmountFieldOptions,
} from "../contractLaunchOptions.js"; } from "../contractLaunchOptions.js";
@@ -293,6 +318,21 @@ function createParty(roleCode = PARTY_ROLE_OPPOSITE, index = 0) {
: `相对方${index + 1}`, : `相对方${index + 1}`,
subjectCode: "", subjectCode: "",
companyName: "", 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 || "", oppositeId: item.oppositeId || "",
companyName: item.companyName || "", companyName: item.companyName || "",
companyType: item.companyType || "", companyType: item.companyType || "",
companyNature: item.companyNature || item.oppositeCharacter || "",
orgNature: item.orgNature || item.natureOfEnterpriseOrg || "",
orgNatureLevel: item.orgNatureLevel || item.natureOfEnterpriseOrgLev || "",
oppCharacter: item.oppositeCharacter || "", oppCharacter: item.oppositeCharacter || "",
contactName: item.contactName || "", contactName: item.contactName || "",
contactPhone: item.contactPhone || "", contactPhone: item.contactPhone || "",
@@ -319,6 +362,9 @@ function mapLaunchPartyToFormParty(item = {}) {
registeredAddress: item.registeredAddress || "", registeredAddress: item.registeredAddress || "",
legalRepresentative: item.legalRepresentative || "", legalRepresentative: item.legalRepresentative || "",
creditCode: item.creditCode || "", creditCode: item.creditCode || "",
registeredCapitalText:
item.registeredCapitalText || item.regCapital || item.registeredCapital || "",
countryName: item.countryName || item.country || "",
bankName: item.bankName || "", bankName: item.bankName || "",
bankAccount: item.bankAccount || "", bankAccount: item.bankAccount || "",
naturalPersonIdType: item.naturalPersonIdType || "", naturalPersonIdType: item.naturalPersonIdType || "",
@@ -455,12 +501,12 @@ function createContractFormData() {
isSingleAgreement: "0", isSingleAgreement: "0",
// 合同属性 // 合同属性
isInvolveHongKongMacaoTaiwan: false, // 是否涉及港澳台 isInvolveHongKongMacaoTaiwan: "0", // 是否涉及港澳台
isPublicTender: false, // 是否公开招标 isPublicTender: "0", // 是否公开招标
isForeignRelatedSubject: false, // 签约主体是否涉外 isForeignRelatedSubject: "0", // 签约主体是否涉外
isIncludedInCurrentYearBudget: false, // 是否列入当年预算 isIncludedInCurrentYearBudget: "0", // 是否列入当年预算
isShared: false, // 是否共享 isShared: "0", // 是否共享
isMajorContract: false, // 是否重大合同 isMajorContract: "0", // 是否重大合同
// 额外金额 // 额外金额
extraAmounts: { extraAmounts: {
@@ -599,16 +645,9 @@ function copyContractFormData(data = {}) {
return form; return form;
} }
function buildTemplateRenderData(templateFieldValues = {}) {
const data = {};
(templateFieldList.value || []).forEach((item) => {
data[item.value] = templateFieldValues?.[item.value] ?? "";
});
return data;
}
const emit = defineEmits(["save"]); const emit = defineEmits(["save"]);
const formRef = ref(null);
const visible = ref(false); const visible = ref(false);
const previewRef = ref(null); const previewRef = ref(null);
const modalMode = ref("create"); const modalMode = ref("create");
@@ -622,6 +661,8 @@ const previewBlob = ref(null);
const manualBodyFileList = ref([]); const manualBodyFileList = ref([]);
const publishedTemplates = ref([]); const publishedTemplates = ref([]);
const templateFieldList = ref([]); const templateFieldList = ref([]);
const dictOptionMap = ref({});
const dictLoadingMap = ref({});
const templateBufferCache = new Map(); const templateBufferCache = new Map();
let previewTimer = null; let previewTimer = null;
@@ -648,19 +689,10 @@ const templateFieldLabelMap = computed(() =>
const launchFieldList = computed(() => { const launchFieldList = computed(() => {
if (creationMode.value === "manual") return []; if (creationMode.value === "manual") return [];
const mappings = activeTemplate.value?.variableMappings || []; return buildTemplateLaunchFieldList(
const keys = []; activeTemplate.value?.variableMappings || [],
mappings.forEach((item) => { templateFieldLabelMap.value,
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,
}));
}); });
function getLaunchFieldValue(fieldKey) { function getLaunchFieldValue(fieldKey) {
@@ -671,15 +703,111 @@ function setLaunchFieldValue(fieldKey, value) {
if (!contractDetailState.value.templateFieldValues) { if (!contractDetailState.value.templateFieldValues) {
contractDetailState.value.templateFieldValues = {}; contractDetailState.value.templateFieldValues = {};
} }
contractDetailState.value.templateFieldValues[fieldKey] = value; contractDetailState.value.templateFieldValues[fieldKey] = value ?? "";
if (visible.value) { if (visible.value) {
schedulePreview(); schedulePreview();
} }
} }
const templateVariables = computed(() => function normalizeDictOptions(list = []) {
buildTemplateRenderData(contractDetailState.value.templateFieldValues || {}), 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(/&nbsp;/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(() => { const hasPreviewSource = computed(() => {
return creationMode.value === "manual" return creationMode.value === "manual"
@@ -710,8 +838,10 @@ watch(
previewError.value = ""; previewError.value = "";
if (visible.value && creationMode.value === "template") { if (visible.value && creationMode.value === "template") {
prefillFromMainFields(); prefillFromMainFields();
nextTick(() => { loadCurrentTemplateDictOptions().finally(() => {
schedulePreview(); nextTick(() => {
schedulePreview();
});
}); });
} }
}, },
@@ -721,15 +851,17 @@ watch(
() => visible.value, () => visible.value,
(open) => { (open) => {
if (open) { if (open) {
nextTick(() => { loadCurrentTemplateDictOptions().finally(() => {
refreshPreview(); nextTick(() => {
refreshPreview();
});
}); });
} }
}, },
); );
watch( watch(
() => JSON.stringify(templateVariables.value), () => JSON.stringify(contractDetailState.value.templateFieldValues || {}),
() => { () => {
if (visible.value) { if (visible.value) {
schedulePreview(); schedulePreview();
@@ -764,7 +896,9 @@ watch(
previewBlob.value = null; previewBlob.value = null;
previewError.value = ""; previewError.value = "";
if (visible.value) { if (visible.value) {
schedulePreview(); 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) { function getManifestDetailLocal(templateId) {
return ( return (
publishedTemplates.value.find((item) => item.id === templateId) || null publishedTemplates.value.find((item) => item.id === templateId) || null
@@ -869,6 +1019,8 @@ async function buildFilledDocumentBlob() {
throw new Error("请选择合同模板"); throw new Error("请选择合同模板");
} }
await loadCurrentTemplateDictOptions();
const buffer = activeTemplate.value.templateArrayBuffer const buffer = activeTemplate.value.templateArrayBuffer
? activeTemplate.value.templateArrayBuffer.slice(0) ? activeTemplate.value.templateArrayBuffer.slice(0)
: await fetchTemplateBuffer( : await fetchTemplateBuffer(
@@ -876,36 +1028,10 @@ async function buildFilledDocumentBlob() {
activeTemplate.value.templateFileUrl || activeTemplate.value.templateFileUrl ||
activeTemplate.value.fileUrl, activeTemplate.value.fileUrl,
); );
const zip = new PizZip(buffer); return await renderTemplateDocxBlob({
const doc = new Docxtemplater(zip, { templateArrayBuffer: buffer,
paragraphLoop: true, variableMappings: activeTemplate.value.variableMappings || [],
linebreaks: true, templateFieldValues: buildTemplateRenderFieldValues(),
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",
}); });
} }
@@ -1048,12 +1174,11 @@ function validateMainForm() {
return "请选择合同模板"; return "请选择合同模板";
for (const field of launchFieldList.value) { for (const field of launchFieldList.value) {
if (field.readonly) {
continue;
}
const value = getLaunchFieldValue(field.key); const value = getLaunchFieldValue(field.key);
if (value === null || value === undefined || value === "") { if (field.required && isLaunchFieldEmpty(field, value)) {
return `请输入${field.label}`; return field.fieldType === "select"
? `请选择${field.label}`
: `请输入${field.label}`;
} }
} }
@@ -1129,6 +1254,7 @@ async function showModal(initialValues = {}, mode = "create") {
// 预填充:从主合同同名字段自动取值填入模板变量 // 预填充:从主合同同名字段自动取值填入模板变量
prefillFromMainFields(initialValues.detailState || initialValues); prefillFromMainFields(initialValues.detailState || initialValues);
await loadCurrentTemplateDictOptions();
syncManualBodyFileList(); syncManualBodyFileList();
previewBlob.value = null; previewBlob.value = null;
@@ -1153,6 +1279,16 @@ function prefillFromMainFields(rawSource) {
}); });
let changed = false; 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 => { keysToFill.forEach(key => {
if (values[key] !== undefined && values[key] !== null && values[key] !== "") return; if (values[key] !== undefined && values[key] !== null && values[key] !== "") return;
// 1. 先尝试从当前合同详情直接取(同名字段) // 1. 先尝试从当前合同详情直接取(同名字段)
@@ -1218,7 +1354,7 @@ onBeforeUnmount(() => {
.contract-template-layout { .contract-template-layout {
display: grid; display: grid;
grid-template-columns: 460px minmax(0, 1fr); grid-template-columns: 560px minmax(0, 1fr);
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
@@ -1274,6 +1410,15 @@ onBeforeUnmount(() => {
gap: 14px; gap: 14px;
} }
.grid-span-2 {
grid-column: 1 / -1;
}
.config-form :deep(.ant-select),
.config-form :deep(.ant-input) {
width: 100%;
}
.template-tip { .template-tip {
padding: 12px 14px; padding: 12px 14px;
border-radius: 12px; 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(/&nbsp;/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>&nbsp;</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 || '&nbsp;'}</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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
</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
+66 -4
View File
@@ -228,7 +228,7 @@
<a v-if="canMockReject(row)" @click="handleMockReject(row)">{{ <a v-if="canMockReject(row)" @click="handleMockReject(row)">{{
isSingleAgreementRecord(row) ? "审批驳回" : "测试驳回" isSingleAgreementRecord(row) ? "审批驳回" : "测试驳回"
}}</a> }}</a>
<a v-if="canOnlineSign(row)" @click="handleOnlineSign(row)">{{ <a v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canOnlineSign(row)" @click="handleOnlineSign(row)">{{
isSingleAgreementRecord(row) ? "发起盖章" : "在线签订" isSingleAgreementRecord(row) ? "发起盖章" : "在线签订"
}}</a> }}</a>
<a <a
@@ -236,11 +236,11 @@
@click="handleDownloadSigned(row)" @click="handleDownloadSigned(row)"
>下载签署文件</a >下载签署文件</a
> >
<a v-if="canUploadSigned(row)" @click="handleUploadSigned(row)">{{ <a v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canUploadSigned(row)" @click="handleUploadSigned(row)">{{
isSingleAgreementRecord(row) ? "上传盖章版" : "上传已签署版本" isSingleAgreementRecord(row) ? "上传盖章版" : "上传已签署版本"
}}</a> }}</a>
<a <a
v-if="canMockFinishSign(row)" v-if="SHOW_SIGN_ACTIONS_IN_LAUNCH && canMockFinishSign(row)"
@click="handleMockFinishSign(row)" @click="handleMockFinishSign(row)"
>{{ >{{
isSingleAgreementRecord(row) ? "测试盖章完成" : "测试签署完成" isSingleAgreementRecord(row) ? "测试盖章完成" : "测试签署完成"
@@ -323,6 +323,7 @@ import {
getLaunchDetail, getLaunchDetail,
getLawApproveCallbackSample, getLawApproveCallbackSample,
mockApproveLaunch, mockApproveLaunch,
uploadWatermarkFile,
mockRejectLaunch, mockRejectLaunch,
finishLaunchSeal, finishLaunchSeal,
queryApproveViewUrl, queryApproveViewUrl,
@@ -345,6 +346,9 @@ const contractRecordStatusOptions = [
{ label: "已终止", value: "terminated" }, { label: "已终止", value: "terminated" },
]; ];
// 合同签署已拆到独立菜单,这里只隐藏入口,保留原有逻辑方便后续恢复。
const SHOW_SIGN_ACTIONS_IN_LAUNCH = false;
const formRef = ref(null); const formRef = ref(null);
const queryContainer = ref(null); const queryContainer = ref(null);
const pageWrapperRef = ref(null); const pageWrapperRef = ref(null);
@@ -1039,12 +1043,16 @@ function handleUploadSigned(row) {
} }
function handleOnlineSign(row) { function handleOnlineSign(row) {
getLaunchDetail(row.id).then((res) => { getLaunchDetail(row.id).then(async (res) => {
if (!isSuccessResponse(res) || !getDataResult(res)) { if (!isSuccessResponse(res) || !getDataResult(res)) {
message.error(getResponseMessage(res, "获取合同详情失败")); message.error(getResponseMessage(res, "获取合同详情失败"));
return; return;
} }
const detail = getDataResult(res); const detail = getDataResult(res);
if (!detail.contractTextFilePath) {
await handleWatermarkUpload(row);
return;
}
mixedSignModalRef.value?.showModal({ mixedSignModalRef.value?.showModal({
...detail, ...detail,
contractId: detail.sealContractId, 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() { function handleSignBusinessUpdated() {
getList(); getList();
} }
+9
View File
@@ -180,6 +180,15 @@ const actionTypeOptions = [
{ label: "合同归档", value: "archive" }, { label: "合同归档", value: "archive" },
{ label: "终止合同", value: "terminate" }, { label: "终止合同", value: "terminate" },
{ label: "删除合同", value: "delete" }, { 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 = [ const actionStatusOptions = [
@@ -17,7 +17,7 @@
<div class="panel-header"> <div class="panel-header">
<div> <div>
<h3>合同模板配置中心</h3> <h3>合同模板配置中心</h3>
<p>在线维护模板配置与变量映射推荐在本地 Word 编辑正文后回传系统</p> <p>上传模板后自动识别 `{中文变量}` 占位符并为每个变量配置录入类型展示名称和数据源</p>
</div> </div>
</div> </div>
@@ -33,18 +33,11 @@
<a-card size="small" title="基本信息" class="panel-card"> <a-card size="small" title="基本信息" class="panel-card">
<div class="field-grid field-grid-2"> <div class="field-grid field-grid-2">
<a-form-item label="范本名称" name="templateName"> <a-form-item label="范本名称" name="templateName">
<a-input <a-input v-model:value="formState.templateName" placeholder="请输入范本名称" />
v-model:value="formState.templateName"
placeholder="请输入范本名称"
/>
</a-form-item> </a-form-item>
<a-form-item label="范本编号" name="templateCode"> <a-form-item label="范本编号" name="templateCode">
<a-input <a-input v-model:value="formState.templateCode" placeholder="保存后自动生成" readonly />
v-model:value="formState.templateCode"
placeholder="保存后自动生成"
readonly
/>
</a-form-item> </a-form-item>
<a-form-item label="范本类型" name="source"> <a-form-item label="范本类型" name="source">
@@ -62,11 +55,9 @@
placeholder="请选择合同类型" placeholder="请选择合同类型"
/> />
</a-form-item> </a-form-item>
<a-form-item label="版本号" name="version"> <a-form-item label="版本号" name="version">
<a-input <a-input v-model:value="formState.version" placeholder="例如 V1.0" />
v-model:value="formState.version"
placeholder="例如 V1.0"
/>
</a-form-item> </a-form-item>
<a-form-item label="发布范围" name="publishOrgIds" class="field-span-2"> <a-form-item label="发布范围" name="publishOrgIds" class="field-span-2">
@@ -102,28 +93,27 @@
</div> </div>
</a-card> </a-card>
<a-card size="small" title="模板文件与配置预览" class="panel-card"> <a-card size="small" title="模板文件与识别结果" class="panel-card">
<a-alert <a-alert
type="info" type="info"
show-icon show-icon
class="workflow-alert" class="workflow-alert"
message="推荐维护方式" message="维护建议"
description="下载当前模板到本地 Word 编辑正文与占位符,保存后重新上传回传;平台负责变量识别、映射、预览与发布。" description="富文本变量建议单独占一行或单独放在表格单元格内,这样后续渲染表格和多段文字会更稳。"
/> />
<div class="upload-meta"> <div class="upload-meta">
<div> <div>
<div class="meta-label">当前模板文件</div> <div class="meta-label">当前模板文件</div>
<div class="meta-value"> <div class="meta-value">{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}</div>
{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}
</div>
</div> </div>
<a-space v-if="!isViewMode"> <a-space v-if="!isViewMode">
<a-button @click="handleDownloadCurrentTemplate" :disabled="!hasTemplateFile"> <a-button @click="handleDownloadCurrentTemplate" :disabled="!hasTemplateFile">
<DownloadOutlined /> <DownloadOutlined />
下载当前模板 下载当前模板
</a-button> </a-button>
<a-button @click="handleReDetectVariables"> <a-button @click="handleReDetectVariables" :disabled="!templateArrayBuffer">
<ReloadOutlined /> <ReloadOutlined />
重新识别变量 重新识别变量
</a-button> </a-button>
@@ -148,25 +138,29 @@
<FileWordOutlined /> <FileWordOutlined />
</p> </p>
<p class="ant-upload-text">上传 Word 模板文件支持 `.docx`</p> <p class="ant-upload-text">上传 Word 模板文件支持 `.docx`</p>
<p class="ant-upload-hint"> <p class="ant-upload-hint">上传后自动识别 `{合同名称}``{附件一内容}` 这类花括号变量</p>
上传后会自动识别花括号变量 `{contractName}``{partyAName}`并生成变量匹配清单 <p class="ant-upload-hint">推荐先在本地 Word 调整正文再上传到平台做变量配置</p>
</p>
<p class="ant-upload-hint">
如需修改正文内容建议先下载模板到本地 Word 编辑后再重新上传回传
</p>
</a-upload-dragger> </a-upload-dragger>
</a-card> </a-card>
<a-card size="small" title="占位符工具栏与变量映射" class="panel-card"> <a-card size="small" title="变量映射与录入类型" class="panel-card">
<div class="variable-header"> <div class="variable-header">
<div> <div>
<div class="meta-label">已识别变量</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 class="catalog-help">
点击下方字段标签可直接复制占位符点击加入映射可快速补充变量绑定 每个变量都要配置展示名称和录入类型下拉变量需要填写字典类型富文本变量支持文字和表格
</div> </div>
</div> </div>
<a-space> <a-space>
<a-tooltip title="查看甲方信息自动带出规则">
<a-button size="small" @click="autoFillGuideVisible = true">
<InfoCircleOutlined />
甲方带出说明
</a-button>
</a-tooltip>
<a-button @click="handleDownloadVariableGuide"> <a-button @click="handleDownloadVariableGuide">
<DownloadOutlined /> <DownloadOutlined />
下载变量说明 下载变量说明
@@ -178,33 +172,13 @@
</a-space> </a-space>
</div> </div>
<!-- <div class="catalog-wrap"> <a-alert
<div class="catalog-title">占位符工具栏</div> type="warning"
<div class="catalog-list"> show-icon
<div class="workflow-alert"
v-for="item in templateFieldList" message="富文本变量说明"
:key="item.value" description="富文本变量最终会替换为 Word 正文内容,支持多段文本和表格。请尽量把富文本占位符单独放在一行。"
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> -->
<div v-if="variableMappings.length" class="mapping-list"> <div v-if="variableMappings.length" class="mapping-list">
<div <div
@@ -230,31 +204,81 @@
<a-input <a-input
v-model:value="item.variableKey" v-model:value="item.variableKey"
:disabled="isViewMode" :disabled="isViewMode"
placeholder="例如 contractName" placeholder="例如 合同名称、附件一内容"
/> />
</a-form-item> </a-form-item>
<a-form-item label="匹配业务字段"> <a-form-item label="匹配业务字段">
<a-select <a-auto-complete
v-model:value="item.bindField" v-model:value="item.bindField"
:options="templateFieldOptions" :options="templateFieldAutoOptions"
:disabled="isViewMode" :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> </a-form-item>
</div> </div>
</div> </div>
</div> </div>
<a-empty <a-empty v-else description="暂无变量匹配项" />
v-else
description="暂无变量匹配项"
/>
</a-card> </a-card>
<a-card size="small" title="维护信息" class="panel-card"> <a-card size="small" title="维护信息" class="panel-card">
<div class="field-grid field-grid-2"> <div class="field-grid field-grid-2">
<a-form-item label="创建人"> <a-form-item label="维护人">
<a-input v-model:value="formState.updatedBy" disabled /> <a-input v-model:value="formState.updatedBy" disabled />
</a-form-item> </a-form-item>
<a-form-item label="创建时间"> <a-form-item label="创建时间">
@@ -269,12 +293,8 @@
<a-space> <a-space>
<a-button @click="visible = false">关闭</a-button> <a-button @click="visible = false">关闭</a-button>
<template v-if="!isViewMode"> <template v-if="!isViewMode">
<a-button @click="handleSubmit('draft')"> <a-button @click="handleSubmit('draft')">保存</a-button>
保存 <a-button type="primary" @click="handleSubmit('published')">发布</a-button>
</a-button>
<a-button type="primary" @click="handleSubmit('published')">
发布
</a-button>
</template> </template>
</a-space> </a-space>
</div> </div>
@@ -284,7 +304,7 @@
<div class="panel-header"> <div class="panel-header">
<div> <div>
<h3>模板预览</h3> <h3>模板预览</h3>
<p>右侧基于当前变量映射渲染模板效果用于检查占位符位置与展示结果本页不直接在线修改 Word 正文</p> <p>右侧用变量展示名模拟填充效果帮助检查占位符是否识别正确</p>
</div> </div>
<a-space size="small"> <a-space size="small">
<a-tag color="geekblue">{{ formState.source || '未分类' }}</a-tag> <a-tag color="geekblue">{{ formState.source || '未分类' }}</a-tag>
@@ -295,7 +315,7 @@
<div class="preview-shell"> <div class="preview-shell">
<div v-if="!templateArrayBuffer" class="preview-state"> <div v-if="!templateArrayBuffer" class="preview-state">
<FileWordOutlined style="font-size: 42px; color: #94a3b8" /> <FileWordOutlined style="font-size: 42px; color: #94a3b8" />
<p>上传 `.docx` 模板文件后这里会显示填充后的预览正文修改建议在本地 Word 完成后回传</p> <p>上传 `.docx` 模板文件后这里会显示填充预览</p>
</div> </div>
<div v-else ref="previewRef" class="preview-stage"></div> <div v-else ref="previewRef" class="preview-stage"></div>
<div v-if="rendering" class="preview-state preview-mask"> <div v-if="rendering" class="preview-state preview-mask">
@@ -310,6 +330,43 @@
</section> </section>
</div> </div>
</a-modal> </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> </template>
<script setup> <script setup>
@@ -320,6 +377,7 @@ import {
DeleteOutlined, DeleteOutlined,
DownloadOutlined, DownloadOutlined,
FileWordOutlined, FileWordOutlined,
InfoCircleOutlined,
PlusOutlined, PlusOutlined,
ReloadOutlined, ReloadOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
@@ -334,16 +392,30 @@ import {
import { contractTypeTreeOptions } from '../contractTypeOptions.js' import { contractTypeTreeOptions } from '../contractTypeOptions.js'
import { import {
buildTemplateFieldLabelMap, buildTemplateFieldLabelMap,
buildTemplateFieldOptions,
loadTemplateFieldList, loadTemplateFieldList,
} from '../../shared/templateFieldDict.js' } 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 contractTypeOptions = contractTypeTreeOptions
// 范本类型选项
const templateSourceOptions = [ const templateSourceOptions = [
{ label: '标准模板', value: '标准模板' }, { label: '标准模板', value: '标准模板' },
{ label: '推荐文本', value: '推荐文本' }, { label: '推荐文本', value: '推荐文本' },
@@ -351,13 +423,8 @@ const templateSourceOptions = [
{ label: '对方文本', value: '对方文本' }, { label: '对方文本', value: '对方文本' },
] ]
// 批量创建变量映射
function createSimpleVariableList(list = []) { function createSimpleVariableList(list = []) {
return list.map((item, index) => ({ return list.map((item, index) => normalizeTemplateVariableMapping(item, index))
id: item.id || `var_${Date.now()}_${index}`,
variableKey: item.variableKey || '',
bindField: item.bindField || '',
}))
} }
const emit = defineEmits(['submit']) const emit = defineEmits(['submit'])
@@ -372,6 +439,7 @@ const templateArrayBuffer = ref(null)
const templateUploadList = ref([]) const templateUploadList = ref([])
const variableMappings = ref([]) const variableMappings = ref([])
const detectedVariableKeys = ref([]) const detectedVariableKeys = ref([])
const autoFillGuideVisible = ref(false)
const orgTreeData = ref([]) const orgTreeData = ref([])
const SHOW_PARENT = 'SHOW_PARENT' const SHOW_PARENT = 'SHOW_PARENT'
const templateFieldList = ref([]) const templateFieldList = ref([])
@@ -379,14 +447,28 @@ const templateFieldList = ref([])
let previewTimer = null let previewTimer = null
let renderTaskId = 0 let renderTaskId = 0
const templateFieldOptions = computed(() =>
buildTemplateFieldOptions(templateFieldList.value),
)
const templateFieldLabelMap = computed(() => const templateFieldLabelMap = computed(() =>
buildTemplateFieldLabelMap(templateFieldList.value), 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 = { const formRules = {
templateName: [{ required: true, message: '请输入范本名称', trigger: 'blur' }], templateName: [{ required: true, message: '请输入范本名称', trigger: 'blur' }],
source: [{ required: true, message: '请选择范本类型', trigger: 'change' }], source: [{ required: true, message: '请选择范本类型', trigger: 'change' }],
@@ -446,6 +528,11 @@ watch(
variableMappings.value.map((item) => ({ variableMappings.value.map((item) => ({
variableKey: item.variableKey, variableKey: item.variableKey,
bindField: item.bindField, 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 '' return ''
} }
function extractVariablesFromText(text) { function extractVariablesFromText(text) {
const result = new Set() const result = new Set()
const regex = /\{+\s*([^{}\s]+)\s*\}+/g const regex = /\{+\s*([^{}\s]+)\s*\}+/g
@@ -543,11 +629,17 @@ function mergeDetectedVariables(variableKeys) {
const detectedRows = nextKeys.map((variableKey) => { const detectedRows = nextKeys.map((variableKey) => {
const existing = existingMap.get(variableKey) const existing = existingMap.get(variableKey)
return { return normalizeTemplateVariableMapping({
id: existing?.id || `var_${Date.now()}_${variableKey}`, id: existing?.id || `var_${Date.now()}_${variableKey}`,
variableKey, variableKey,
bindField: existing?.bindField || 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 const manualRows = variableMappings.value
@@ -575,7 +667,6 @@ async function detectVariablesFromBuffer(buffer) {
}) })
mergeDetectedVariables([...allVariables]) mergeDetectedVariables([...allVariables])
} catch (error) { } catch (error) {
message.warning('模板文件已上传,但变量自动识别失败,请手动维护变量') message.warning('模板文件已上传,但变量自动识别失败,请手动维护变量')
} }
@@ -649,22 +740,8 @@ async function handleReDetectVariables() {
} }
function handleAddManualVariable() { 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( variableMappings.value.push(
{ createEmptyTemplateVariableMapping(variableMappings.value.length + 1),
id: `var_${Date.now()}_${fieldKey}`,
variableKey: fieldKey,
bindField: fieldKey,
},
) )
} }
@@ -672,47 +749,12 @@ function buildPlaceholderText(fieldKey) {
return `{${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) { function removeVariable(id) {
variableMappings.value = variableMappings.value.filter((item) => item.id !== id) variableMappings.value = variableMappings.value.filter((item) => item.id !== id)
} }
function buildPreviewData() { function buildPreviewData() {
const data = {} return buildTemplatePreviewData(variableMappings.value, templateFieldLabelMap.value)
variableMappings.value.forEach((item) => {
if (!item.variableKey) return
data[item.variableKey] =
templateFieldLabelMap.value[item.bindField] ||
item.bindField ||
`[${item.variableKey}]`
})
return data
} }
async function renderPreview() { async function renderPreview() {
@@ -796,9 +838,9 @@ function buildRecord(status) {
variables: variableMappings.value variables: variableMappings.value
.filter((item) => item.variableKey) .filter((item) => item.variableKey)
.map((item) => item.variableKey), .map((item) => item.variableKey),
variableMappings: variableMappings.value.map((item) => ({ variableMappings: variableMappings.value.map((item, index) =>
...item, normalizeTemplateVariableMapping(item, index),
})), ),
} }
} }
@@ -824,6 +866,18 @@ function validateVariableMappings() {
if (!item.bindField) { if (!item.bindField) {
return `请为变量【${item.variableKey}】选择匹配业务字段` 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 '' return ''
@@ -888,14 +942,23 @@ function buildVariableGuideText() {
'', '',
'推荐维护方式:', '推荐维护方式:',
'1. 下载当前模板到本地 Word 编辑正文内容。', '1. 下载当前模板到本地 Word 编辑正文内容。',
'2. 在正文中插入占位符,例如 {contractName}、{partyAName}。', '2. 在正文中插入占位符,例如 {合同名称}、{附件一内容}。',
'3. 保存后重新上传 .docx 文件回传系统。', '3. 保存后重新上传 .docx 文件回传系统。',
'4. 在平台内完成变量识别、字段映射、预览和发布。', '4. 在平台内为每个变量配置展示名称、录入类型和数据源。',
'', '',
'可用占位符清单', '录入类型说明',
'- 输入框:普通文本输入。',
'- 下拉框:需配置 dictType,例如 orgBusType。',
'- 富文本:支持多段文本与表格,建议占位符单独成段。',
'',
'可用业务字段参考:',
...templateFieldList.value.map( ...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') return lines.join('\r\n')
@@ -942,9 +1005,6 @@ async function handleDownloadCurrentTemplate() {
} }
} }
/**
* 将后端组织树节点转为 a-tree-select 需要的格式
*/
function convertOrgTreeToTreeSelect(node) { function convertOrgTreeToTreeSelect(node) {
const treeNode = { const treeNode = {
value: String(node.id), value: String(node.id),
@@ -956,9 +1016,6 @@ function convertOrgTreeToTreeSelect(node) {
return treeNode return treeNode
} }
/**
* 从树数据构建 orgId → orgName 的映射(递归遍历)
*/
function buildOrgNameMap(treeData) { function buildOrgNameMap(treeData) {
const map = new Map() const map = new Map()
function walk(list) { function walk(list) {
@@ -976,6 +1033,10 @@ async function showModal(record = {}, mode = 'create') {
resetState() resetState()
modalMode.value = mode || 'create' modalMode.value = mode || 'create'
await ensureTemplateFieldListLoaded() await ensureTemplateFieldListLoaded()
if (!dictTypePresetOptions.value.length) {
const list = await loadDictTypeOptions()
dictTypePresetOptions.value = buildDictTypeSelectOptions(list)
}
if (!orgTreeData.value.length) { if (!orgTreeData.value.length) {
const tree = await getOrgTree() const tree = await getOrgTree()
orgTreeData.value = (tree || []).map(convertOrgTreeToTreeSelect) orgTreeData.value = (tree || []).map(convertOrgTreeToTreeSelect)
@@ -1017,7 +1078,6 @@ defineExpose({
</script> </script>
<style scoped> <style scoped>
/* 详情模式下禁用表单文字颜色加深 */
.view-form:deep(.ant-select-disabled .ant-select-selection-item) { .view-form:deep(.ant-select-disabled .ant-select-selection-item) {
color: rgba(0, 0, 0, 0.85) !important; color: rgba(0, 0, 0, 0.85) !important;
} }
@@ -1028,20 +1088,6 @@ defineExpose({
color: rgba(0, 0, 0, 0.85) !important; color: rgba(0, 0, 0, 0.85) !important;
-webkit-text-fill-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) { .view-form:deep(.ant-input-disabled) {
color: rgba(0, 0, 0, 0.85) !important; color: rgba(0, 0, 0, 0.85) !important;
-webkit-text-fill-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 { .template-manage-modal {
display: grid; display: grid;
grid-template-columns: 560px minmax(0, 1fr); grid-template-columns: 580px minmax(0, 1fr);
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
@@ -1086,6 +1132,27 @@ defineExpose({
margin-bottom: 6px; 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 { .panel-header p {
font-size: 13px; font-size: 13px;
color: #64748b; color: #64748b;
@@ -1157,21 +1224,6 @@ defineExpose({
line-height: 1.8; 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 { .catalog-help {
margin-top: 6px; margin-top: 6px;
font-size: 12px; font-size: 12px;
@@ -1179,30 +1231,6 @@ defineExpose({
line-height: 1.7; 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 { .mapping-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1290,7 +1318,7 @@ defineExpose({
@media (max-width: 1280px) { @media (max-width: 1280px) {
.template-manage-modal { .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();
}
+772
View File
@@ -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>
+4
View File
@@ -35,6 +35,10 @@ export default defineConfig({
target: 'http://127.0.0.1:8899', target: 'http://127.0.0.1:8899',
changeOrigin: true, changeOrigin: true,
}, },
'/mock': {
target: 'http://127.0.0.1:8899',
changeOrigin: true,
},
'/trans': { '/trans': {
target: 'http://127.0.0.1:8899', target: 'http://127.0.0.1:8899',
changeOrigin: true, changeOrigin: true,