1. 变量占位符:{中文变量}

2. 变量占位符预设类型,可以是:输入框、下拉、富文本(允许插入表格作为合同正本的附件内容)
This commit is contained in:
2026-07-09 14:08:43 +08:00
parent 2de0a12d44
commit e03e019f58
8 changed files with 1899 additions and 286 deletions
+3
View File
@@ -10,6 +10,9 @@
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"@xmldom/xmldom": "^0.8.10",
"ant-design-vue": "^4.2.6",
"axios": "^1.16.0",
"dayjs": "^1.11.20",
+488
View File
@@ -11,6 +11,15 @@ importers:
'@ant-design/icons-vue':
specifier: ^7.0.1
version: 7.0.1(vue@3.5.34)
'@wangeditor/editor':
specifier: ^5.1.23
version: 5.1.23
'@wangeditor/editor-for-vue':
specifier: ^5.1.12
version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)
'@xmldom/xmldom':
specifier: ^0.8.10
version: 0.8.13
ant-design-vue:
specifier: ^4.2.6
version: 4.2.6(vue@3.5.34)
@@ -402,12 +411,35 @@ packages:
'@simonwep/pickr@1.8.2':
resolution: {integrity: sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==}
'@transloadit/prettier-bytes@0.0.7':
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/event-emitter@0.3.5':
resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
'@types/node@25.9.1':
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
'@uppy/companion-client@2.2.2':
resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
'@uppy/core@2.3.4':
resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==}
'@uppy/store-default@2.1.1':
resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==}
'@uppy/utils@4.1.3':
resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==}
'@uppy/xhr-upload@2.1.3':
resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==}
peerDependencies:
'@uppy/core': ^2.3.3
'@vitejs/plugin-vue@5.2.4':
resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -452,6 +484,97 @@ packages:
peerDependencies:
vue: ^3.2.0
'@wangeditor/basic-modules@1.1.7':
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.throttle: ^4.1.1
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/code-highlight@1.0.3':
resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/core@1.1.19':
resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==}
peerDependencies:
'@uppy/core': ^2.1.1
'@uppy/xhr-upload': ^2.0.3
dom7: ^3.0.0
is-hotkey: ^0.2.0
lodash.camelcase: ^4.3.0
lodash.clonedeep: ^4.5.0
lodash.debounce: ^4.0.8
lodash.foreach: ^4.5.0
lodash.isequal: ^4.5.0
lodash.throttle: ^4.1.1
lodash.toarray: ^4.4.0
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/editor-for-vue@5.1.12':
resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==}
peerDependencies:
'@wangeditor/editor': '>=5.1.0'
vue: ^3.0.5
'@wangeditor/editor@5.1.23':
resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==}
'@wangeditor/list-module@1.0.5':
resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/table-module@1.1.4':
resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==}
peerDependencies:
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.isequal: ^4.5.0
lodash.throttle: ^4.1.1
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/upload-image-module@1.0.2':
resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==}
peerDependencies:
'@uppy/core': ^2.0.3
'@uppy/xhr-upload': ^2.0.3
'@wangeditor/basic-modules': 1.x
'@wangeditor/core': 1.x
dom7: ^3.0.0
lodash.foreach: ^4.5.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@wangeditor/video-module@1.1.4':
resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==}
peerDependencies:
'@uppy/core': ^2.1.4
'@uppy/xhr-upload': ^2.0.7
'@wangeditor/core': 1.x
dom7: ^3.0.0
nanoid: ^3.2.0
slate: ^0.72.0
snabbdom: ^3.1.0
'@xmldom/xmldom@0.8.13':
resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
engines: {node: '>=10.0.0'}
'@xmldom/xmldom@0.9.10':
resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==}
engines: {node: '>=14.6'}
@@ -498,6 +621,10 @@ packages:
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
d@1.0.2:
resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
engines: {node: '>=0.12'}
dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
@@ -534,6 +661,9 @@ packages:
dom-zindex@1.0.6:
resolution: {integrity: sha512-FKWIhiU96bi3xpP9ewRMgANsoVmMUBnMnmpCT6dPMZOunVYJQmJhSRruoI0XSPoHeIif3kyEuiHbFrOJwEJaEA==}
dom7@3.0.0:
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -558,14 +688,35 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
es5-ext@0.10.64:
resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
engines: {node: '>=0.10'}
es6-iterator@2.0.3:
resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
es6-symbol@3.1.4:
resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
engines: {node: '>=0.12'}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
hasBin: true
esniff@2.0.1:
resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
engines: {node: '>=0.10'}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
event-emitter@0.3.5:
resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
ext@1.7.0:
resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -626,20 +777,39 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
html-void-elements@2.0.1:
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
i18next@20.6.1:
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
is-hotkey@0.2.0:
resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==}
is-plain-object@3.0.1:
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
engines: {node: '>=0.10.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
@@ -655,6 +825,28 @@ packages:
lodash-es@4.18.1:
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.clonedeep@4.5.0:
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.foreach@4.5.0:
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
lodash.toarray@4.4.0:
resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@@ -673,6 +865,9 @@ packages:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-match@1.0.2:
resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
@@ -683,6 +878,9 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
namespace-emitter@2.0.1:
resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -696,6 +894,9 @@ packages:
nanopop@2.4.2:
resolution: {integrity: sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==}
next-tick@1.1.0:
resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@@ -725,6 +926,18 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
preact@10.29.7:
resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==}
peerDependencies:
preact-render-to-string: '>=5'
peerDependenciesMeta:
preact-render-to-string:
optional: true
prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@@ -759,10 +972,25 @@ packages:
shallow-equal@1.2.1:
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
slate-history@0.66.0:
resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==}
peerDependencies:
slate: '>=0.65.3'
slate@0.72.8:
resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==}
snabbdom@3.6.4:
resolution: {integrity: sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==}
engines: {node: '>=12.17.0'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
ssr-window@3.0.0:
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@@ -773,10 +1001,16 @@ packages:
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
engines: {node: '>=12.22'}
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
type@2.7.3:
resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
@@ -862,6 +1096,9 @@ packages:
warning@4.0.3:
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
wildcard@1.1.2:
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
xe-utils@4.0.8:
resolution: {integrity: sha512-l+p++9iPtSxpf/5ZCzkPXsyV0avE34KzieAC9oAhi+3e98PLJaBQHNiY2V8Lnbiux4JFAbU10p8AT6oujXkP9A==}
@@ -1067,12 +1304,47 @@ snapshots:
core-js: 3.49.0
nanopop: 2.4.2
'@transloadit/prettier-bytes@0.0.7': {}
'@types/estree@1.0.8': {}
'@types/event-emitter@0.3.5': {}
'@types/node@25.9.1':
dependencies:
undici-types: 7.24.6
'@uppy/companion-client@2.2.2':
dependencies:
'@uppy/utils': 4.1.3
namespace-emitter: 2.0.1
'@uppy/core@2.3.4':
dependencies:
'@transloadit/prettier-bytes': 0.0.7
'@uppy/store-default': 2.1.1
'@uppy/utils': 4.1.3
lodash.throttle: 4.1.1
mime-match: 1.0.2
namespace-emitter: 2.0.1
nanoid: 3.3.12
preact: 10.29.7
transitivePeerDependencies:
- preact-render-to-string
'@uppy/store-default@2.1.1': {}
'@uppy/utils@4.1.3':
dependencies:
lodash.throttle: 4.1.1
'@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)':
dependencies:
'@uppy/companion-client': 2.2.2
'@uppy/core': 2.3.4
'@uppy/utils': 4.1.3
nanoid: 3.3.12
'@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@25.9.1))(vue@3.5.34)':
dependencies:
vite: 6.4.2(@types/node@25.9.1)
@@ -1140,6 +1412,118 @@ snapshots:
vue: 3.5.34
xe-utils: 4.0.8
'@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
is-url: 1.2.4
lodash.throttle: 4.1.1
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
prismjs: 1.30.0
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@types/event-emitter': 0.3.5
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
dom7: 3.0.0
event-emitter: 0.3.5
html-void-elements: 2.0.1
i18next: 20.6.1
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8
lodash.foreach: 4.5.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.12
scroll-into-view-if-needed: 2.2.31
slate: 0.72.8
slate-history: 0.66.0(slate@0.72.8)
snabbdom: 3.6.4
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.34)':
dependencies:
'@wangeditor/editor': 5.1.23
vue: 3.5.34
'@wangeditor/editor@5.1.23':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
is-hotkey: 0.2.0
lodash.camelcase: 4.3.0
lodash.clonedeep: 4.5.0
lodash.debounce: 4.0.8
lodash.foreach: 4.5.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
lodash.toarray: 4.4.0
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.4
transitivePeerDependencies:
- preact-render-to-string
'@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
lodash.isequal: 4.5.0
lodash.throttle: 4.1.1
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
lodash.foreach: 4.5.0
slate: 0.72.8
snabbdom: 3.6.4
'@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)':
dependencies:
'@uppy/core': 2.3.4
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.12)(slate@0.72.8)(snabbdom@3.6.4)
dom7: 3.0.0
nanoid: 3.3.12
slate: 0.72.8
snabbdom: 3.6.4
'@xmldom/xmldom@0.8.13': {}
'@xmldom/xmldom@0.9.10': {}
agent-base@6.0.2:
@@ -1207,6 +1591,11 @@ snapshots:
csstype@3.2.3: {}
d@1.0.2:
dependencies:
es5-ext: 0.10.64
type: 2.7.3
dayjs@1.11.20: {}
debug@4.4.3:
@@ -1238,6 +1627,10 @@ snapshots:
dom-zindex@1.0.6: {}
dom7@3.0.0:
dependencies:
ssr-window: 3.0.0
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -1261,6 +1654,24 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.3
es5-ext@0.10.64:
dependencies:
es6-iterator: 2.0.3
es6-symbol: 3.1.4
esniff: 2.0.1
next-tick: 1.1.0
es6-iterator@2.0.3:
dependencies:
d: 1.0.2
es5-ext: 0.10.64
es6-symbol: 3.1.4
es6-symbol@3.1.4:
dependencies:
d: 1.0.2
ext: 1.7.0
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
@@ -1290,8 +1701,24 @@ snapshots:
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
esniff@2.0.1:
dependencies:
d: 1.0.2
es5-ext: 0.10.64
event-emitter: 0.3.5
type: 2.7.3
estree-walker@2.0.2: {}
event-emitter@0.3.5:
dependencies:
d: 1.0.2
es5-ext: 0.10.64
ext@1.7.0:
dependencies:
type: 2.7.3
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
@@ -1348,6 +1775,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
html-void-elements@2.0.1: {}
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
@@ -1355,12 +1784,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
i18next@20.6.1:
dependencies:
'@babel/runtime': 7.29.2
immediate@3.0.6: {}
immer@9.0.21: {}
inherits@2.0.4: {}
is-hotkey@0.2.0: {}
is-plain-object@3.0.1: {}
is-plain-object@5.0.0: {}
is-url@1.2.4: {}
isarray@1.0.0: {}
js-tokens@4.0.0: {}
@@ -1378,6 +1819,20 @@ snapshots:
lodash-es@4.18.1: {}
lodash.camelcase@4.3.0: {}
lodash.clonedeep@4.5.0: {}
lodash.debounce@4.0.8: {}
lodash.foreach@4.5.0: {}
lodash.isequal@4.5.0: {}
lodash.throttle@4.1.1: {}
lodash.toarray@4.4.0: {}
lodash@4.18.1: {}
loose-envify@1.4.0:
@@ -1392,6 +1847,10 @@ snapshots:
mime-db@1.52.0: {}
mime-match@1.0.2:
dependencies:
wildcard: 1.1.2
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
@@ -1400,12 +1859,16 @@ snapshots:
ms@2.1.3: {}
namespace-emitter@2.0.1: {}
nanoid@3.3.12: {}
nanoid@5.1.11: {}
nanopop@2.4.2: {}
next-tick@1.1.0: {}
pako@1.0.11: {}
pako@2.1.0: {}
@@ -1432,6 +1895,10 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
preact@10.29.7: {}
prismjs@1.30.0: {}
process-nextick-args@2.0.1: {}
proxy-from-env@2.1.0: {}
@@ -1491,8 +1958,23 @@ snapshots:
shallow-equal@1.2.1: {}
slate-history@0.66.0(slate@0.72.8):
dependencies:
is-plain-object: 5.0.0
slate: 0.72.8
slate@0.72.8:
dependencies:
immer: 9.0.21
is-plain-object: 5.0.0
tiny-warning: 1.0.3
snabbdom@3.6.4: {}
source-map-js@1.2.1: {}
ssr-window@3.0.0: {}
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
@@ -1501,11 +1983,15 @@ snapshots:
throttle-debounce@5.0.2: {}
tiny-warning@1.0.3: {}
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
type@2.7.3: {}
undici-types@7.24.6: {}
util-deprecate@1.0.2: {}
@@ -1560,6 +2046,8 @@ snapshots:
dependencies:
loose-envify: 1.4.0
wildcard@1.1.2: {}
xe-utils@4.0.8: {}
xml-js@1.6.11:
@@ -71,12 +71,34 @@
v-for="field in launchFieldList"
:key="field.key"
:label="field.label"
:required="!field.readonly"
:required="field.required"
:extra="field.remark || undefined"
:class="{ 'grid-span-2': field.fieldType === 'rich_text' }"
>
<a-input
<a-select
v-if="field.fieldType === 'select'"
:value="getLaunchFieldValue(field.key)"
:placeholder="`请输入${field.label}`"
:readonly="field.readonly"
:options="getFieldDictOptions(field)"
:placeholder="field.placeholder"
:loading="isDictLoading(field.dictType)"
:disabled="isViewMode"
allow-clear
show-search
option-filter-prop="label"
@update:value="setLaunchFieldValue(field.key, $event)"
/>
<TemplateRichTextEditor
v-else-if="field.fieldType === 'rich_text'"
:model-value="getLaunchFieldValue(field.key)"
:placeholder="field.placeholder"
:disabled="isViewMode"
@update:model-value="setLaunchFieldValue(field.key, $event)"
/>
<a-input
v-else
:value="getLaunchFieldValue(field.key)"
:placeholder="field.placeholder"
:readonly="isViewMode"
@update:value="setLaunchFieldValue(field.key, $event)"
/>
</a-form-item>
@@ -177,7 +199,6 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import Docxtemplater from "docxtemplater";
import dayjs from "dayjs";
import {
FileWordOutlined,
@@ -185,9 +206,9 @@ import {
SettingOutlined,
} from "@ant-design/icons-vue";
import { message } from "ant-design-vue";
import PizZip from "pizzip";
import { renderAsync } from "docx-preview";
import { saveAs } from "file-saver";
import { getDictDataByType } from "@/api/dict.js";
import {
downloadTemplateFile,
queryPublishedManifestList,
@@ -197,6 +218,9 @@ import {
buildTemplateFieldLabelMap,
loadTemplateFieldList,
} from "../../shared/templateFieldDict.js";
import { renderTemplateDocxBlob } from "../../shared/templateDocxRenderer.js";
import { buildTemplateLaunchFieldList } from "../../shared/templateVariableConfig.js";
import TemplateRichTextEditor from "./TemplateRichTextEditor.vue";
import {
extraAmountFieldOptions,
} from "../contractLaunchOptions.js";
@@ -599,16 +623,9 @@ function copyContractFormData(data = {}) {
return form;
}
function buildTemplateRenderData(templateFieldValues = {}) {
const data = {};
(templateFieldList.value || []).forEach((item) => {
data[item.value] = templateFieldValues?.[item.value] ?? "";
});
return data;
}
const emit = defineEmits(["save"]);
const formRef = ref(null);
const visible = ref(false);
const previewRef = ref(null);
const modalMode = ref("create");
@@ -622,6 +639,8 @@ const previewBlob = ref(null);
const manualBodyFileList = ref([]);
const publishedTemplates = ref([]);
const templateFieldList = ref([]);
const dictOptionMap = ref({});
const dictLoadingMap = ref({});
const templateBufferCache = new Map();
let previewTimer = null;
@@ -648,19 +667,10 @@ const templateFieldLabelMap = computed(() =>
const launchFieldList = computed(() => {
if (creationMode.value === "manual") return [];
const mappings = activeTemplate.value?.variableMappings || [];
const keys = [];
mappings.forEach((item) => {
const fieldKey = item.bindField || item.variableKey;
if (fieldKey && !keys.includes(fieldKey)) {
keys.push(fieldKey);
}
});
return keys.map((key) => ({
key,
label: templateFieldLabelMap.value[key] || key,
readonly: false,
}));
return buildTemplateLaunchFieldList(
activeTemplate.value?.variableMappings || [],
templateFieldLabelMap.value,
);
});
function getLaunchFieldValue(fieldKey) {
@@ -671,15 +681,111 @@ function setLaunchFieldValue(fieldKey, value) {
if (!contractDetailState.value.templateFieldValues) {
contractDetailState.value.templateFieldValues = {};
}
contractDetailState.value.templateFieldValues[fieldKey] = value;
contractDetailState.value.templateFieldValues[fieldKey] = value ?? "";
if (visible.value) {
schedulePreview();
}
}
const templateVariables = computed(() =>
buildTemplateRenderData(contractDetailState.value.templateFieldValues || {}),
);
function normalizeDictOptions(list = []) {
return (list || [])
.map((item) => ({
label: item?.k || item?.label || "",
value: item?.v || item?.value || "",
remark: item?.remark || "",
}))
.filter((item) => item.label && item.value);
}
async function loadDictOptionsByType(dictType) {
const key = String(dictType || "").trim();
if (!key) {
return [];
}
if (dictOptionMap.value[key]?.length) {
return dictOptionMap.value[key];
}
if (dictLoadingMap.value[key]) {
return dictOptionMap.value[key] || [];
}
dictLoadingMap.value = { ...dictLoadingMap.value, [key]: true };
try {
const result = await getDictDataByType(key);
const options = normalizeDictOptions(result);
dictOptionMap.value = { ...dictOptionMap.value, [key]: options };
return options;
} finally {
dictLoadingMap.value = { ...dictLoadingMap.value, [key]: false };
}
}
async function loadCurrentTemplateDictOptions() {
if (creationMode.value !== "template") {
return;
}
const dictTypes = [...new Set(
launchFieldList.value
.filter((item) => item.fieldType === "select" && item.dictType)
.map((item) => item.dictType),
)];
await Promise.all(dictTypes.map((item) => loadDictOptionsByType(item)));
}
function getFieldDictOptions(field = {}) {
return dictOptionMap.value[String(field.dictType || "").trim()] || [];
}
function isDictLoading(dictType) {
return !!dictLoadingMap.value[String(dictType || "").trim()];
}
function getLaunchFieldByKey(fieldKey) {
return launchFieldList.value.find((item) => item.key === fieldKey) || null;
}
function getLaunchFieldDisplayValue(field = {}) {
const rawValue = getLaunchFieldValue(field.key);
if (field.fieldType !== "select") {
return rawValue;
}
const option = getFieldDictOptions(field).find(
(item) => String(item.value) === String(rawValue ?? ""),
);
return option?.label ?? rawValue ?? "";
}
function stripRichTextContent(htmlValue) {
return String(htmlValue || "")
.replace(/<table[\s\S]*?<\/table>/gi, " [表格] ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/&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(() => {
return creationMode.value === "manual"
@@ -710,9 +816,11 @@ watch(
previewError.value = "";
if (visible.value && creationMode.value === "template") {
prefillFromMainFields();
loadCurrentTemplateDictOptions().finally(() => {
nextTick(() => {
schedulePreview();
});
});
}
},
);
@@ -721,15 +829,17 @@ watch(
() => visible.value,
(open) => {
if (open) {
loadCurrentTemplateDictOptions().finally(() => {
nextTick(() => {
refreshPreview();
});
});
}
},
);
watch(
() => JSON.stringify(templateVariables.value),
() => JSON.stringify(contractDetailState.value.templateFieldValues || {}),
() => {
if (visible.value) {
schedulePreview();
@@ -764,7 +874,9 @@ watch(
previewBlob.value = null;
previewError.value = "";
if (visible.value) {
loadCurrentTemplateDictOptions().finally(() => {
schedulePreview();
});
}
},
);
@@ -793,6 +905,22 @@ function loadTemplateFields() {
});
}
watch(
() => JSON.stringify(
launchFieldList.value.map((item) => ({
key: item.key,
fieldType: item.fieldType,
dictType: item.dictType,
})),
),
() => {
if (visible.value && creationMode.value === "template") {
loadCurrentTemplateDictOptions();
}
},
{ deep: false },
);
function getManifestDetailLocal(templateId) {
return (
publishedTemplates.value.find((item) => item.id === templateId) || null
@@ -869,6 +997,8 @@ async function buildFilledDocumentBlob() {
throw new Error("请选择合同模板");
}
await loadCurrentTemplateDictOptions();
const buffer = activeTemplate.value.templateArrayBuffer
? activeTemplate.value.templateArrayBuffer.slice(0)
: await fetchTemplateBuffer(
@@ -876,36 +1006,10 @@ async function buildFilledDocumentBlob() {
activeTemplate.value.templateFileUrl ||
activeTemplate.value.fileUrl,
);
const zip = new PizZip(buffer);
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
nullGetter(part) {
return part?.raw ? `[${part.raw}]` : "";
},
});
//这里是生成的测试数据
const sourceValues = buildTemplateRenderData(
contractDetailState.value.templateFieldValues || {},
);
const mappings = activeTemplate.value.variableMappings || [];
const renderData = {};
if (mappings.length) {
mappings.forEach((item) => {
if (!item.variableKey) return;
renderData[item.variableKey] = sourceValues[item.bindField] ?? "";
});
} else {
Object.assign(renderData, sourceValues);
}
doc.render(renderData);
return doc.getZip().generate({
type: "blob",
mimeType:
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
return await renderTemplateDocxBlob({
templateArrayBuffer: buffer,
variableMappings: activeTemplate.value.variableMappings || [],
templateFieldValues: buildTemplateRenderFieldValues(),
});
}
@@ -1048,12 +1152,11 @@ function validateMainForm() {
return "请选择合同模板";
for (const field of launchFieldList.value) {
if (field.readonly) {
continue;
}
const value = getLaunchFieldValue(field.key);
if (value === null || value === undefined || value === "") {
return `请输入${field.label}`;
if (field.required && isLaunchFieldEmpty(field, value)) {
return field.fieldType === "select"
? `请选择${field.label}`
: `请输入${field.label}`;
}
}
@@ -1129,6 +1232,7 @@ async function showModal(initialValues = {}, mode = "create") {
// 预填充:从主合同同名字段自动取值填入模板变量
prefillFromMainFields(initialValues.detailState || initialValues);
await loadCurrentTemplateDictOptions();
syncManualBodyFileList();
previewBlob.value = null;
@@ -1274,6 +1378,15 @@ onBeforeUnmount(() => {
gap: 14px;
}
.grid-span-2 {
grid-column: 1 / -1;
}
.config-form :deep(.ant-select),
.config-form :deep(.ant-input) {
width: 100%;
}
.template-tip {
padding: 12px 14px;
border-radius: 12px;
@@ -0,0 +1,161 @@
<template>
<div class="template-rich-editor" :class="{ 'is-disabled': disabled }">
<Toolbar
class="editor-toolbar"
:editor="editorRef"
:default-config="toolbarConfig"
:mode="mode"
/>
<Editor
class="editor-content"
:model-value="modelValue"
:default-config="editorConfig"
:mode="mode"
@onCreated="handleCreated"
@update:modelValue="handleChange"
/>
<div class="editor-tip">
当前支持普通文字列表和基础表格为保证导出 Word 效果建议不要使用合并单元格和复杂样式
</div>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, shallowRef } from 'vue'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
const props = defineProps({
modelValue: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '请输入内容,支持插入表格',
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const mode = 'default'
const editorRef = shallowRef(null)
const toolbarConfig = {
toolbarKeys: [
'headerSelect',
'|',
'bold',
'italic',
'underline',
'through',
'|',
'color',
'bgColor',
'|',
'bulletedList',
'numberedList',
'|',
'justifyLeft',
'justifyCenter',
'justifyRight',
'|',
'insertTable',
'deleteTable',
'insertTableRow',
'deleteTableRow',
'insertTableCol',
'deleteTableCol',
'tableHeader',
'|',
'undo',
'redo',
],
excludeKeys: ['fullScreen'],
}
const editorConfig = computed(() => ({
placeholder: props.placeholder,
readOnly: props.disabled,
autoFocus: false,
scroll: true,
hoverbarKeys: {
table: {
menuKeys: [
'insertTableRow',
'deleteTableRow',
'insertTableCol',
'deleteTableCol',
'tableHeader',
'deleteTable',
],
},
},
MENU_CONF: {
insertTable: {
rows: 3,
cols: 4,
},
insertImage: { base64LimitSize: 0 },
uploadImage: { server: '' },
uploadVideo: { server: '' },
},
}))
function handleCreated(editor) {
editorRef.value = editor
}
function handleChange(value) {
emit('update:modelValue', value || '')
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor) {
editor.destroy()
}
})
</script>
<style scoped>
.template-rich-editor {
border: 1px solid #d9e1ec;
border-radius: 12px;
overflow: hidden;
background: #fff;
}
.template-rich-editor.is-disabled {
opacity: 0.9;
}
.editor-toolbar {
border-bottom: 1px solid #edf2f7;
}
.editor-content {
min-height: 260px;
}
.editor-content :deep(.w-e-text-container) {
min-height: 240px !important;
}
.editor-content :deep(.w-e-text-placeholder) {
top: 14px;
}
.editor-tip {
padding: 8px 12px 10px;
font-size: 12px;
color: #64748b;
background: #f8fafc;
border-top: 1px solid #edf2f7;
line-height: 1.6;
}
</style>
@@ -17,7 +17,7 @@
<div class="panel-header">
<div>
<h3>合同模板配置中心</h3>
<p>在线维护模板配置与变量映射推荐在本地 Word 编辑正文后回传系统</p>
<p>上传模板后自动识别 `{中文变量}` 占位符并为每个变量配置录入类型展示名称和数据源</p>
</div>
</div>
@@ -33,18 +33,11 @@
<a-card size="small" title="基本信息" class="panel-card">
<div class="field-grid field-grid-2">
<a-form-item label="范本名称" name="templateName">
<a-input
v-model:value="formState.templateName"
placeholder="请输入范本名称"
/>
<a-input v-model:value="formState.templateName" placeholder="请输入范本名称" />
</a-form-item>
<a-form-item label="范本编号" name="templateCode">
<a-input
v-model:value="formState.templateCode"
placeholder="保存后自动生成"
readonly
/>
<a-input v-model:value="formState.templateCode" placeholder="保存后自动生成" readonly />
</a-form-item>
<a-form-item label="范本类型" name="source">
@@ -62,11 +55,9 @@
placeholder="请选择合同类型"
/>
</a-form-item>
<a-form-item label="版本号" name="version">
<a-input
v-model:value="formState.version"
placeholder="例如 V1.0"
/>
<a-input v-model:value="formState.version" placeholder="例如 V1.0" />
</a-form-item>
<a-form-item label="发布范围" name="publishOrgIds" class="field-span-2">
@@ -102,20 +93,19 @@
</div>
</a-card>
<a-card size="small" title="模板文件与配置预览" class="panel-card">
<a-card size="small" title="模板文件与识别结果" class="panel-card">
<a-alert
type="info"
show-icon
class="workflow-alert"
message="推荐维护方式"
description="下载当前模板到本地 Word 编辑正文与占位符,保存后重新上传回传;平台负责变量识别、映射、预览与发布。"
message="维护建议"
description="富文本变量建议单独占一行或单独放在表格单元格内,这样后续渲染表格和多段文字会更稳。"
/>
<div class="upload-meta">
<div>
<div class="meta-label">当前模板文件</div>
<div class="meta-value">
{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}
</div>
<div class="meta-value">{{ formState.templateFileName || '尚未上传 .docx 模板文件' }}</div>
</div>
<a-space v-if="!isViewMode">
@@ -123,7 +113,7 @@
<DownloadOutlined />
下载当前模板
</a-button>
<a-button @click="handleReDetectVariables">
<a-button @click="handleReDetectVariables" :disabled="!templateArrayBuffer">
<ReloadOutlined />
重新识别变量
</a-button>
@@ -148,22 +138,20 @@
<FileWordOutlined />
</p>
<p class="ant-upload-text">上传 Word 模板文件支持 `.docx`</p>
<p class="ant-upload-hint">
上传后会自动识别花括号变量 `{contractName}``{partyAName}`并生成变量匹配清单
</p>
<p class="ant-upload-hint">
如需修改正文内容建议先下载模板到本地 Word 编辑后再重新上传回传
</p>
<p class="ant-upload-hint">上传后自动识别 `{合同名称}``{附件一内容}` 这类花括号变量</p>
<p class="ant-upload-hint">推荐先在本地 Word 调整正文再上传到平台做变量配置</p>
</a-upload-dragger>
</a-card>
<a-card size="small" title="占位符工具栏与变量映射" class="panel-card">
<a-card size="small" title="变量映射与录入类型" class="panel-card">
<div class="variable-header">
<div>
<div class="meta-label">已识别变量</div>
<div class="meta-value">{{ detectedVariableKeys.length ? detectedVariableKeys.join('、') : '暂未识别到变量' }}</div>
<div class="meta-value">
{{ detectedVariableKeys.length ? detectedVariableKeys.join('、') : '暂未识别到变量' }}
</div>
<div class="catalog-help">
点击下方字段标签可直接复制占位符点击加入映射可快速补充变量绑定
每个变量都要配置展示名称和录入类型下拉变量需要填写字典类型富文本变量支持文字和表格
</div>
</div>
<a-space>
@@ -178,33 +166,13 @@
</a-space>
</div>
<!-- <div class="catalog-wrap">
<div class="catalog-title">占位符工具栏</div>
<div class="catalog-list">
<div
v-for="item in templateFieldList"
:key="item.value"
class="catalog-item"
>
<a-tag
color="blue"
class="catalog-tag"
@click="handleCopyPlaceholder(item.value)"
>
{{ item.label }}
</a-tag>
<a-button
v-if="!isViewMode"
type="link"
size="small"
class="catalog-action"
@click.stop="handleQuickAddCatalogField(item.value)"
>
加入映射
</a-button>
</div>
</div>
</div> -->
<a-alert
type="warning"
show-icon
class="workflow-alert"
message="富文本变量说明"
description="富文本变量最终会替换为 Word 正文内容,支持多段文本和表格。请尽量把富文本占位符单独放在一行。"
/>
<div v-if="variableMappings.length" class="mapping-list">
<div
@@ -230,31 +198,81 @@
<a-input
v-model:value="item.variableKey"
:disabled="isViewMode"
placeholder="例如 contractName"
placeholder="例如 合同名称、附件一内容"
/>
</a-form-item>
<a-form-item label="匹配业务字段">
<a-select
<a-auto-complete
v-model:value="item.bindField"
:options="templateFieldOptions"
:options="templateFieldAutoOptions"
:disabled="isViewMode"
placeholder="请选择业务字段"
placeholder="请选择或直接输入业务字段编码"
allow-clear
/>
</a-form-item>
<a-form-item label="展示名称">
<a-input
v-model:value="item.fieldLabel"
:disabled="isViewMode"
placeholder="合同发起页表单展示名称"
/>
</a-form-item>
<a-form-item label="录入类型">
<a-select
v-model:value="item.fieldType"
:options="fieldTypeOptions"
:disabled="isViewMode"
placeholder="请选择录入类型"
/>
</a-form-item>
<a-form-item label="是否必填">
<a-switch
v-model:checked="item.required"
:disabled="isViewMode"
checked-children="必填"
un-checked-children="选填"
/>
</a-form-item>
<a-form-item label="输入提示">
<a-input
v-model:value="item.placeholder"
:disabled="isViewMode"
placeholder="可选,例如 请输入附件一内容"
/>
</a-form-item>
<a-form-item v-if="item.fieldType === 'select'" label="字典类型" class="field-span-2">
<a-auto-complete
v-model:value="item.dictType"
:options="dictTypePresetOptions"
:disabled="isViewMode"
placeholder="例如 orgBusType、contractPayMethod"
allow-clear
/>
</a-form-item>
<a-form-item label="备注" class="field-span-2">
<a-input
v-model:value="item.remark"
:disabled="isViewMode"
placeholder="可选,说明该变量的填写要求"
/>
</a-form-item>
</div>
</div>
</div>
<a-empty
v-else
description="暂无变量匹配项"
/>
<a-empty v-else description="暂无变量匹配项" />
</a-card>
<a-card size="small" title="维护信息" class="panel-card">
<div class="field-grid field-grid-2">
<a-form-item label="创建人">
<a-form-item label="维护人">
<a-input v-model:value="formState.updatedBy" disabled />
</a-form-item>
<a-form-item label="创建时间">
@@ -269,12 +287,8 @@
<a-space>
<a-button @click="visible = false">关闭</a-button>
<template v-if="!isViewMode">
<a-button @click="handleSubmit('draft')">
保存
</a-button>
<a-button type="primary" @click="handleSubmit('published')">
发布
</a-button>
<a-button @click="handleSubmit('draft')">保存</a-button>
<a-button type="primary" @click="handleSubmit('published')">发布</a-button>
</template>
</a-space>
</div>
@@ -284,7 +298,7 @@
<div class="panel-header">
<div>
<h3>模板预览</h3>
<p>右侧基于当前变量映射渲染模板效果用于检查占位符位置与展示结果本页不直接在线修改 Word 正文</p>
<p>右侧用变量展示名模拟填充效果帮助检查占位符是否识别正确</p>
</div>
<a-space size="small">
<a-tag color="geekblue">{{ formState.source || '未分类' }}</a-tag>
@@ -295,7 +309,7 @@
<div class="preview-shell">
<div v-if="!templateArrayBuffer" class="preview-state">
<FileWordOutlined style="font-size: 42px; color: #94a3b8" />
<p>上传 `.docx` 模板文件后这里会显示填充后的预览正文修改建议在本地 Word 完成后回传</p>
<p>上传 `.docx` 模板文件后这里会显示填充预览</p>
</div>
<div v-else ref="previewRef" class="preview-stage"></div>
<div v-if="rendering" class="preview-state preview-mask">
@@ -334,16 +348,23 @@ import {
import { contractTypeTreeOptions } from '../contractTypeOptions.js'
import {
buildTemplateFieldLabelMap,
buildTemplateFieldOptions,
loadTemplateFieldList,
} from '../../shared/templateFieldDict.js'
import {
buildTemplatePreviewData,
} from '../../shared/templateDocxRenderer.js'
import {
createEmptyTemplateVariableMapping,
normalizeTemplateVariableMapping,
TEMPLATE_FIELD_TYPE_OPTIONS,
} from '../../shared/templateVariableConfig.js'
import {
loadDictTypeOptions,
buildDictTypeSelectOptions,
} from '../../shared/dictTypeOptions.js'
// ========== 模板管理字典(后续可由后端接口替换) ==========
// 合同类型选项
const contractTypeOptions = contractTypeTreeOptions
// 范本类型选项
const templateSourceOptions = [
{ label: '标准模板', value: '标准模板' },
{ label: '推荐文本', value: '推荐文本' },
@@ -351,13 +372,8 @@ const templateSourceOptions = [
{ label: '对方文本', value: '对方文本' },
]
// 批量创建变量映射
function createSimpleVariableList(list = []) {
return list.map((item, index) => ({
id: item.id || `var_${Date.now()}_${index}`,
variableKey: item.variableKey || '',
bindField: item.bindField || '',
}))
return list.map((item, index) => normalizeTemplateVariableMapping(item, index))
}
const emit = defineEmits(['submit'])
@@ -379,14 +395,20 @@ const templateFieldList = ref([])
let previewTimer = null
let renderTaskId = 0
const templateFieldOptions = computed(() =>
buildTemplateFieldOptions(templateFieldList.value),
)
const templateFieldLabelMap = computed(() =>
buildTemplateFieldLabelMap(templateFieldList.value),
)
const templateFieldAutoOptions = computed(() =>
(templateFieldList.value || []).map((item) => ({
value: item.value,
label: `${item.label}${item.value}`,
})),
)
const fieldTypeOptions = TEMPLATE_FIELD_TYPE_OPTIONS
const dictTypePresetOptions = ref([])
const formRules = {
templateName: [{ required: true, message: '请输入范本名称', trigger: 'blur' }],
source: [{ required: true, message: '请选择范本类型', trigger: 'change' }],
@@ -446,6 +468,11 @@ watch(
variableMappings.value.map((item) => ({
variableKey: item.variableKey,
bindField: item.bindField,
fieldLabel: item.fieldLabel,
fieldType: item.fieldType,
required: item.required,
dictType: item.dictType,
placeholder: item.placeholder,
})),
),
() => {
@@ -520,7 +547,6 @@ function resolveUploadedFilePath(res) {
return ''
}
function extractVariablesFromText(text) {
const result = new Set()
const regex = /\{+\s*([^{}\s]+)\s*\}+/g
@@ -543,11 +569,17 @@ function mergeDetectedVariables(variableKeys) {
const detectedRows = nextKeys.map((variableKey) => {
const existing = existingMap.get(variableKey)
return {
return normalizeTemplateVariableMapping({
id: existing?.id || `var_${Date.now()}_${variableKey}`,
variableKey,
bindField: existing?.bindField || variableKey,
}
fieldLabel: existing?.fieldLabel || variableKey,
fieldType: existing?.fieldType || 'input',
required: existing?.required !== false,
placeholder: existing?.placeholder || '',
dictType: existing?.dictType || '',
remark: existing?.remark || '',
})
})
const manualRows = variableMappings.value
@@ -575,7 +607,6 @@ async function detectVariablesFromBuffer(buffer) {
})
mergeDetectedVariables([...allVariables])
} catch (error) {
message.warning('模板文件已上传,但变量自动识别失败,请手动维护变量')
}
@@ -649,22 +680,8 @@ async function handleReDetectVariables() {
}
function handleAddManualVariable() {
variableMappings.value.push({
id: `var_${Date.now()}_${variableMappings.value.length + 1}`,
variableKey: '',
bindField: '',
})
}
function handleQuickAddCatalogField(fieldKey) {
if (isViewMode.value) return
variableMappings.value.push(
{
id: `var_${Date.now()}_${fieldKey}`,
variableKey: fieldKey,
bindField: fieldKey,
},
createEmptyTemplateVariableMapping(variableMappings.value.length + 1),
)
}
@@ -672,47 +689,12 @@ function buildPlaceholderText(fieldKey) {
return `{${fieldKey}}`
}
async function copyTextToClipboard(text) {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
return
}
const textarea = document.createElement('textarea')
textarea.value = text
textarea.setAttribute('readonly', 'readonly')
textarea.style.position = 'fixed'
textarea.style.left = '-9999px'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
async function handleCopyPlaceholder(fieldKey) {
const placeholder = buildPlaceholderText(fieldKey)
try {
await copyTextToClipboard(placeholder)
message.success(`已复制占位符:${placeholder}`)
} catch (error) {
message.warning(`复制失败,请手动使用占位符:${placeholder}`)
}
}
function removeVariable(id) {
variableMappings.value = variableMappings.value.filter((item) => item.id !== id)
}
function buildPreviewData() {
const data = {}
variableMappings.value.forEach((item) => {
if (!item.variableKey) return
data[item.variableKey] =
templateFieldLabelMap.value[item.bindField] ||
item.bindField ||
`[${item.variableKey}]`
})
return data
return buildTemplatePreviewData(variableMappings.value, templateFieldLabelMap.value)
}
async function renderPreview() {
@@ -796,9 +778,9 @@ function buildRecord(status) {
variables: variableMappings.value
.filter((item) => item.variableKey)
.map((item) => item.variableKey),
variableMappings: variableMappings.value.map((item) => ({
...item,
})),
variableMappings: variableMappings.value.map((item, index) =>
normalizeTemplateVariableMapping(item, index),
),
}
}
@@ -824,6 +806,18 @@ function validateVariableMappings() {
if (!item.bindField) {
return `请为变量【${item.variableKey}】选择匹配业务字段`
}
if (!item.fieldLabel) {
return `请为变量【${item.variableKey}】填写展示名称`
}
if (!item.fieldType) {
return `请为变量【${item.variableKey}】选择录入类型`
}
if (item.fieldType === 'select' && !item.dictType) {
return `请为下拉变量【${item.variableKey}】配置字典类型`
}
}
return ''
@@ -888,13 +882,18 @@ function buildVariableGuideText() {
'',
'推荐维护方式:',
'1. 下载当前模板到本地 Word 编辑正文内容。',
'2. 在正文中插入占位符,例如 {contractName}、{partyAName}。',
'2. 在正文中插入占位符,例如 {合同名称}、{附件一内容}。',
'3. 保存后重新上传 .docx 文件回传系统。',
'4. 在平台内完成变量识别、字段映射、预览和发布。',
'4. 在平台内为每个变量配置展示名称、录入类型和数据源。',
'',
'可用占位符清单',
'录入类型说明',
'- 输入框:普通文本输入。',
'- 下拉框:需配置 dictType,例如 orgBusType。',
'- 富文本:支持多段文本与表格,建议占位符单独成段。',
'',
'可用业务字段参考:',
...templateFieldList.value.map(
(item) => `- ${item.label}: ${buildPlaceholderText(item.value)}`,
(item) => `- ${item.label}: ${item.value}`,
),
]
@@ -942,9 +941,6 @@ async function handleDownloadCurrentTemplate() {
}
}
/**
* 将后端组织树节点转为 a-tree-select 需要的格式
*/
function convertOrgTreeToTreeSelect(node) {
const treeNode = {
value: String(node.id),
@@ -956,9 +952,6 @@ function convertOrgTreeToTreeSelect(node) {
return treeNode
}
/**
* 从树数据构建 orgId → orgName 的映射(递归遍历)
*/
function buildOrgNameMap(treeData) {
const map = new Map()
function walk(list) {
@@ -976,6 +969,10 @@ async function showModal(record = {}, mode = 'create') {
resetState()
modalMode.value = mode || 'create'
await ensureTemplateFieldListLoaded()
if (!dictTypePresetOptions.value.length) {
const list = await loadDictTypeOptions()
dictTypePresetOptions.value = buildDictTypeSelectOptions(list)
}
if (!orgTreeData.value.length) {
const tree = await getOrgTree()
orgTreeData.value = (tree || []).map(convertOrgTreeToTreeSelect)
@@ -1017,7 +1014,6 @@ defineExpose({
</script>
<style scoped>
/* 详情模式下禁用表单文字颜色加深 */
.view-form:deep(.ant-select-disabled .ant-select-selection-item) {
color: rgba(0, 0, 0, 0.85) !important;
}
@@ -1028,20 +1024,6 @@ defineExpose({
color: rgba(0, 0, 0, 0.85) !important;
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
}
.view-form:deep(.ant-input-number-disabled .ant-input-number-input) {
color: rgba(0, 0, 0, 0.85) !important;
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
}
.view-form:deep(.ant-picker.ant-picker-disabled .ant-picker-input input) {
color: rgba(0, 0, 0, 0.85) !important;
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
}
.view-form:deep(.ant-radio-wrapper.ant-radio-wrapper-disabled .ant-radio + span) {
color: rgba(0, 0, 0, 0.85) !important;
}
.view-form:deep(.ant-checkbox-wrapper.ant-checkbox-wrapper-disabled .ant-checkbox + span) {
color: rgba(0, 0, 0, 0.85) !important;
}
.view-form:deep(.ant-input-disabled) {
color: rgba(0, 0, 0, 0.85) !important;
-webkit-text-fill-color: rgba(0, 0, 0, 0.85) !important;
@@ -1049,7 +1031,7 @@ defineExpose({
.template-manage-modal {
display: grid;
grid-template-columns: 560px minmax(0, 1fr);
grid-template-columns: 580px minmax(0, 1fr);
height: 100%;
min-height: 0;
}
@@ -1157,21 +1139,6 @@ defineExpose({
line-height: 1.8;
}
.catalog-wrap {
margin-bottom: 16px;
padding: 12px 14px;
border-radius: 12px;
background: #f8fbff;
border: 1px solid #e0eaf8;
}
.catalog-title {
font-size: 13px;
font-weight: 600;
color: #334155;
margin-bottom: 10px;
}
.catalog-help {
margin-top: 6px;
font-size: 12px;
@@ -1179,30 +1146,6 @@ defineExpose({
line-height: 1.7;
}
.catalog-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.catalog-item {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 2px 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.72);
}
.catalog-tag {
cursor: pointer;
user-select: none;
}
.catalog-action {
padding-inline: 4px;
}
.mapping-list {
display: flex;
flex-direction: column;
@@ -1290,7 +1233,7 @@ defineExpose({
@media (max-width: 1280px) {
.template-manage-modal {
grid-template-columns: 520px minmax(0, 1fr);
grid-template-columns: 540px minmax(0, 1fr);
}
}
@@ -0,0 +1,32 @@
import { getDictDataByType } from "@/api/dict.js";
export const TEMPLATE_DICT_TYPES_KEY = "TEMPLATE_DICT_TYPES";
let cachedDictTypeOptions = null;
function normalizeDictTypeList(list = []) {
return list
.map((item) => ({
label: item?.k || item?.label || "",
value: item?.v || item?.value || "",
remark: item?.remark || "",
}))
.filter((item) => item.label && item.value);
}
export async function loadDictTypeOptions() {
if (cachedDictTypeOptions?.length) {
return [...cachedDictTypeOptions];
}
const result = await getDictDataByType(TEMPLATE_DICT_TYPES_KEY);
cachedDictTypeOptions = normalizeDictTypeList(result);
return [...cachedDictTypeOptions];
}
export function buildDictTypeSelectOptions(list = []) {
return list.map((item) => ({
label: item.label,
value: item.value,
}));
}
@@ -0,0 +1,789 @@
import Docxtemplater from 'docxtemplater'
import { DOMParser, XMLSerializer } from '@xmldom/xmldom'
import PizZip from 'pizzip'
const DOCX_MIME_TYPE =
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
const WORD_XML_FILE_REGEX = /^word\/(document|header\d+|footer\d+)\.xml$/i
const WORD_NAMESPACE = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
const ELEMENT_NODE = 1
const TEXT_NODE = 3
const DEFAULT_WORD_TABLE_WIDTH_TWIPS = 9000
const DEFAULT_WORD_TABLE_WIDTH_PERCENT = 5000
const DEFAULT_TABLE_CELL_PADDING_TWIPS = 120
const DEFAULT_PARAGRAPH_SPACING_BEFORE = 40
const DEFAULT_PARAGRAPH_SPACING_AFTER = 40
const DEFAULT_PARAGRAPH_LINE = 300
export function buildTemplatePreviewData(variableMappings = [], templateFieldLabelMap = {}) {
const data = {}
;(variableMappings || []).forEach((item) => {
const variableKey = String(item?.variableKey || '').trim()
if (!variableKey) return
const fieldType = normalizeFieldType(item?.fieldType)
const fieldLabel = String(item?.fieldLabel || '').trim()
const bindField = String(item?.bindField || '').trim()
data[variableKey] =
fieldType === 'rich_text'
? `[富文本:${fieldLabel || variableKey}]`
: fieldType === 'select'
? `[下拉:${fieldLabel || variableKey}]`
: fieldLabel || templateFieldLabelMap[bindField] || bindField || `[${variableKey}]`
})
return data
}
export async function renderTemplateDocxBlob({
templateArrayBuffer,
variableMappings = [],
templateFieldValues = {},
}) {
if (!(templateArrayBuffer instanceof ArrayBuffer)) {
throw new Error('模板文件尚未加载完成')
}
const zip = new PizZip(templateArrayBuffer.slice(0))
const richMappings = (variableMappings || []).filter(
(item) => normalizeFieldType(item?.fieldType) === 'rich_text',
)
if (richMappings.length) {
transformRichTextPlaceholders(zip, richMappings)
}
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
nullGetter() {
return ''
},
})
doc.render(buildTemplateRenderData(variableMappings, templateFieldValues))
return doc.getZip().generate({
type: 'blob',
mimeType: DOCX_MIME_TYPE,
})
}
export function buildTemplateRenderData(variableMappings = [], templateFieldValues = {}) {
const renderData = {}
;(variableMappings || []).forEach((item) => {
const variableKey = String(item?.variableKey || '').trim()
if (!variableKey) return
const bindField = String(item?.bindField || variableKey).trim()
const fieldType = normalizeFieldType(item?.fieldType)
const rawValue = templateFieldValues?.[bindField]
renderData[variableKey] =
fieldType === 'rich_text'
? buildRichTextWordXml(rawValue)
: stringifyPlainTemplateValue(rawValue)
})
return renderData
}
function transformRichTextPlaceholders(zip, richMappings = []) {
const richPlaceholderSet = new Set(
richMappings
.map((item) => String(item?.variableKey || '').trim())
.filter(Boolean),
)
if (!richPlaceholderSet.size) {
return
}
Object.keys(zip.files)
.filter((name) => WORD_XML_FILE_REGEX.test(name))
.forEach((name) => {
const xmlText = zip.files[name].asText()
const parser = new DOMParser()
const xmlDoc = parser.parseFromString(xmlText, 'application/xml')
const paragraphs = xmlDoc.getElementsByTagNameNS(WORD_NAMESPACE, 'p')
let changed = false
Array.from(paragraphs).forEach((paragraph) => {
const text = getParagraphText(paragraph).trim()
if (!text.startsWith('{') || !text.endsWith('}')) {
return
}
const variableKey = text.slice(1, -1).trim()
if (!richPlaceholderSet.has(variableKey)) {
return
}
rewriteParagraphAsRawTag(xmlDoc, paragraph, variableKey)
changed = true
})
if (changed) {
zip.file(name, new XMLSerializer().serializeToString(xmlDoc))
}
})
}
function getParagraphText(paragraphNode) {
const textNodes = paragraphNode.getElementsByTagNameNS(WORD_NAMESPACE, 't')
return Array.from(textNodes)
.map((node) => node.textContent || '')
.join('')
}
function rewriteParagraphAsRawTag(xmlDoc, paragraphNode, variableKey) {
const paragraphProps = paragraphNode.getElementsByTagNameNS(WORD_NAMESPACE, 'pPr')[0]
while (paragraphNode.firstChild) {
paragraphNode.removeChild(paragraphNode.firstChild)
}
if (paragraphProps) {
paragraphNode.appendChild(paragraphProps.cloneNode(true))
}
const runNode = xmlDoc.createElementNS(WORD_NAMESPACE, 'w:r')
const textNode = xmlDoc.createElementNS(WORD_NAMESPACE, 'w:t')
textNode.appendChild(xmlDoc.createTextNode(`{@${variableKey}}`))
runNode.appendChild(textNode)
paragraphNode.appendChild(runNode)
}
function buildRichTextWordXml(htmlValue) {
const html = String(htmlValue || '').trim()
if (!html) {
return '<w:p><w:r><w:t></w:t></w:r></w:p>'
}
const parser = new DOMParser()
const htmlDoc = parser.parseFromString(`<html><body><div>${html}</div></body></html>`, 'text/html')
const bodyNode = htmlDoc.getElementsByTagName('body')[0]
const root = firstElementChild(bodyNode) || bodyNode
const blocks = []
collectBlocksFromChildren(root?.childNodes || [], blocks)
if (!blocks.length) {
const paragraph = buildParagraphXml(flattenNodeText(root).trim())
return paragraph || '<w:p><w:r><w:t></w:t></w:r></w:p>'
}
return blocks.join('')
}
function collectBlocksFromChildren(nodeList, blocks, baseStyle = {}) {
Array.from(nodeList || []).forEach((node) => {
if (node.nodeType === TEXT_NODE) {
const text = normalizeText(node.data || node.nodeValue || '')
if (text) {
blocks.push(buildParagraphXml(text, baseStyle))
}
return
}
if (node.nodeType !== ELEMENT_NODE) {
return
}
const tag = String(node.tagName || '').toLowerCase()
if (tag === 'table') {
blocks.push(buildTableXml(node))
return
}
if (['p', 'div', 'section', 'article', 'blockquote'].includes(tag)) {
const paragraphXml = buildParagraphXmlFromNode(node, baseStyle)
if (paragraphXml) {
blocks.push(paragraphXml)
}
return
}
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
const paragraphXml = buildParagraphXmlFromNode(node, { ...baseStyle, bold: true })
if (paragraphXml) {
blocks.push(paragraphXml)
}
return
}
if (['ul', 'ol'].includes(tag)) {
childElements(node).forEach((child) => {
const line = flattenNodeText(child).trim()
if (line) {
blocks.push(buildParagraphXml(`${line}`, baseStyle))
}
})
return
}
if (tag === 'br') {
blocks.push(buildParagraphXml('', baseStyle))
return
}
const paragraphXml = buildParagraphXmlFromNode(node, baseStyle)
if (paragraphXml) {
blocks.push(paragraphXml)
}
})
}
function buildParagraphXmlFromNode(node, extraStyle = {}) {
const mergedStyle = mergeTextStyle(extraStyle, parseNodeStyleInfo(node))
const segments = []
collectInlineSegments(node, mergedStyle, segments)
return buildParagraphXmlBySegments(segments, mergedStyle)
}
function collectInlineSegments(node, style = {}, segments = []) {
Array.from(node?.childNodes || []).forEach((child) => {
if (child.nodeType === TEXT_NODE) {
const text = normalizeText(child.data || child.nodeValue || '')
if (text) {
segments.push({
text,
bold: !!style.bold,
italic: !!style.italic,
underline: !!style.underline,
color: style.color || '',
bgColor: style.bgColor || '',
break: false,
})
}
return
}
if (child.nodeType !== ELEMENT_NODE) {
return
}
const tag = String(child.tagName || '').toLowerCase()
if (tag === 'br') {
segments.push({ break: true })
return
}
const childStyle = parseNodeStyleInfo(child)
const nextStyle = mergeTextStyle(style, {
bold: !!style.bold || ['b', 'strong'].includes(tag),
italic: !!style.italic || ['i', 'em'].includes(tag),
underline: !!style.underline || tag === 'u',
color: childStyle.color,
bgColor: childStyle.bgColor,
align: childStyle.align,
})
if (tag === 'table') {
const tableText = flattenNodeText(child).trim()
if (tableText) {
segments.push({
text: tableText,
bold: nextStyle.bold,
italic: nextStyle.italic,
underline: nextStyle.underline,
color: nextStyle.color || '',
bgColor: nextStyle.bgColor || '',
break: false,
})
}
return
}
collectInlineSegments(child, nextStyle, segments)
})
}
function buildParagraphXml(text = '', style = {}) {
const normalized = typeof text === 'string' ? text : ''
return buildParagraphXmlBySegments(
[
{
text: normalized,
bold: !!style.bold,
italic: !!style.italic,
underline: !!style.underline,
color: style.color || '',
bgColor: style.bgColor || '',
break: false,
},
],
style,
)
}
function buildParagraphXmlBySegments(segments = [], paragraphStyle = {}) {
const validSegments = segments.filter((item) => item.break || item.text !== undefined)
if (!validSegments.length) {
return '<w:p><w:r><w:t></w:t></w:r></w:p>'
}
const runXml = validSegments
.map((item) => {
if (item.break) {
return '<w:r><w:br/></w:r>'
}
const text = escapeXml(String(item.text || ''))
const runProps = []
if (item.bold) runProps.push('<w:b/>')
if (item.italic) runProps.push('<w:i/>')
if (item.underline) runProps.push('<w:u w:val="single"/>')
if (item.color) runProps.push(`<w:color w:val="${item.color}"/>`)
if (item.bgColor) {
runProps.push(`<w:shd w:val="clear" w:color="auto" w:fill="${item.bgColor}"/>`)
}
const propsXml = runProps.length ? `<w:rPr>${runProps.join('')}</w:rPr>` : ''
return `<w:r>${propsXml}<w:t xml:space="preserve">${text}</w:t></w:r>`
})
.join('')
const paragraphProps = [
`<w:spacing w:before="${DEFAULT_PARAGRAPH_SPACING_BEFORE}" w:after="${DEFAULT_PARAGRAPH_SPACING_AFTER}" w:line="${DEFAULT_PARAGRAPH_LINE}" w:lineRule="auto"/>`,
]
const alignmentValue = normalizeWordAlignment(paragraphStyle.align)
if (alignmentValue) {
paragraphProps.push(`<w:jc w:val="${alignmentValue}"/>`)
}
return `<w:p><w:pPr>${paragraphProps.join('')}</w:pPr>${runXml}</w:p>`
}
function buildTableXml(tableElement) {
const directRows = getDirectChildElements(tableElement, 'tr')
const rows = directRows.length ? directRows : Array.from(tableElement.getElementsByTagName('tr'))
const columnWidths = extractTableColumnWidths(tableElement)
const tableWidth = resolveTableWordWidth(readElementWidth(tableElement))
const normalizedColumnWidths = normalizeTableColumnWidths(rows, columnWidths, tableWidth)
const rowXml = rows
.map((row) => {
const cells = getDirectCellElements(row)
let logicalColumnIndex = 0
const cellXml = cells
.map((cell) => {
const isHeader = isHeaderCell(cell)
const cellStyle = parseNodeStyleInfo(cell)
const defaultCellAlign = cellStyle.align || 'center'
const blocks = []
collectBlocksFromChildren(cell.childNodes, blocks, {
bold: isHeader,
color: cellStyle.color || '',
bgColor: '',
align: defaultCellAlign,
})
const contentXml =
blocks.filter(Boolean).join('') || buildParagraphXml('', { align: defaultCellAlign })
const gridSpan = parseSpanValue(cell.getAttribute?.('colspan'))
const tcWidth =
buildCellWidthFromGrid(normalizedColumnWidths, logicalColumnIndex, gridSpan) || {
value: 0,
type: 'auto',
}
logicalColumnIndex += gridSpan
const tcProps = [
`<w:tcW w:w="${tcWidth.value}" w:type="${tcWidth.type}"/>`,
'<w:vAlign w:val="center"/>',
cellStyle.bgColor
? `<w:shd w:val="clear" w:color="auto" w:fill="${cellStyle.bgColor}"/>`
: '',
gridSpan > 1 ? `<w:gridSpan w:val="${gridSpan}"/>` : '',
]
.filter(Boolean)
.join('')
return `<w:tc><w:tcPr>${tcProps}</w:tcPr>${contentXml}</w:tc>`
})
.join('')
return `<w:tr>${cellXml}</w:tr>`
})
.join('')
const normalizedTableWidth = buildNormalizedTableWidth(tableWidth, normalizedColumnWidths)
const tableGridXml = buildTableGridXml(normalizedColumnWidths)
return `<w:tbl><w:tblPr><w:tblW w:w="${normalizedTableWidth.value}" w:type="${normalizedTableWidth.type}"/><w:tblLayout w:type="fixed"/><w:tblCellMar><w:top w:w="${DEFAULT_TABLE_CELL_PADDING_TWIPS}" w:type="dxa"/><w:left w:w="${DEFAULT_TABLE_CELL_PADDING_TWIPS}" w:type="dxa"/><w:bottom w:w="${DEFAULT_TABLE_CELL_PADDING_TWIPS}" w:type="dxa"/><w:right w:w="${DEFAULT_TABLE_CELL_PADDING_TWIPS}" w:type="dxa"/></w:tblCellMar><w:tblBorders><w:top w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:left w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:bottom w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:right w:val="single" w:sz="8" w:space="0" w:color="000000"/><w:insideH w:val="single" w:sz="6" w:space="0" w:color="000000"/><w:insideV w:val="single" w:sz="6" w:space="0" w:color="000000"/></w:tblBorders></w:tblPr>${tableGridXml}${rowXml}</w:tbl>`
}
function getDirectChildElements(parent, tagName) {
return childElements(parent).filter(
(child) => String(child.tagName || '').toLowerCase() === tagName,
)
}
function getDirectCellElements(row) {
const directCells = childElements(row).filter((child) => {
const tag = String(child.tagName || '').toLowerCase()
return tag === 'td' || tag === 'th'
})
return directCells.length
? directCells
: Array.from(row.getElementsByTagName('td')).concat(Array.from(row.getElementsByTagName('th')))
}
function isHeaderCell(cell) {
return String(cell?.tagName || '').toLowerCase() === 'th'
}
function extractTableColumnWidths(tableElement) {
const colGroups = getDirectChildElements(tableElement, 'colgroup')
const cols = colGroups.flatMap((group) => getDirectChildElements(group, 'col'))
return cols.map((col) => resolveCellWordWidth(readElementWidth(col))).filter(Boolean)
}
function normalizeTableColumnWidths(rows, initialColumnWidths, tableWidth) {
const columnCount = getTableColumnCount(rows)
if (!columnCount) {
return []
}
const targetWidth = resolveTableTargetWidthTwips(tableWidth, columnCount)
const widths = Array.from({ length: columnCount }, (_, index) => {
const existingWidth = initialColumnWidths[index]?.value
return Number.isFinite(existingWidth) && existingWidth > 0 ? existingWidth : 0
})
rows.forEach((row) => {
let logicalColumnIndex = 0
getDirectCellElements(row).forEach((cell) => {
const gridSpan = parseSpanValue(cell.getAttribute?.('colspan'))
const cellWidth = resolveCellWordWidth(readElementWidth(cell))
if (cellWidth?.type === 'dxa' && cellWidth.value > 0) {
const distributedWidth = Math.max(300, Math.round(cellWidth.value / gridSpan))
for (let offset = 0; offset < gridSpan; offset += 1) {
const columnIndex = logicalColumnIndex + offset
if (columnIndex < columnCount) {
widths[columnIndex] = Math.max(widths[columnIndex] || 0, distributedWidth)
}
}
}
logicalColumnIndex += gridSpan
})
})
const defaultWidth = Math.max(300, Math.round(targetWidth / columnCount))
const filledWidths = widths.map((width) => (width > 0 ? width : defaultWidth))
const currentTotal = filledWidths.reduce((sum, width) => sum + width, 0)
if (currentTotal <= 0) {
return filledWidths.map((width) => Math.max(300, Math.round(width)))
}
const scaledWidths = filledWidths.map((width) =>
Math.max(300, Math.round((width / currentTotal) * targetWidth)),
)
let scaledTotal = scaledWidths.reduce((sum, width) => sum + width, 0)
let adjustIndex = 0
while (scaledTotal !== targetWidth && scaledWidths.length) {
const diff = targetWidth - scaledTotal
const step = diff > 0 ? 1 : -1
const nextWidth = scaledWidths[adjustIndex] + step
if (nextWidth >= 300) {
scaledWidths[adjustIndex] = nextWidth
scaledTotal += step
}
adjustIndex = (adjustIndex + 1) % scaledWidths.length
}
return scaledWidths.map((width) => Math.max(300, Math.round(width)))
}
function getTableColumnCount(rows) {
return rows.reduce((maxCount, row) => {
const count = getDirectCellElements(row).reduce(
(sum, cell) => sum + parseSpanValue(cell.getAttribute?.('colspan')),
0,
)
return Math.max(maxCount, count)
}, 0)
}
function resolveTableTargetWidthTwips(tableWidth, columnCount) {
if (tableWidth?.type === 'dxa' && tableWidth.value > 0) {
return Math.max(300 * columnCount, Math.round(tableWidth.value))
}
return Math.max(300 * columnCount, DEFAULT_WORD_TABLE_WIDTH_TWIPS)
}
function buildCellWidthFromGrid(columnWidths, startIndex, gridSpan) {
const totalWidth = (columnWidths || [])
.slice(startIndex, startIndex + gridSpan)
.reduce((sum, width) => sum + (Number.isFinite(width) ? width : 0), 0)
if (totalWidth > 0) {
return { value: totalWidth, type: 'dxa' }
}
return null
}
function buildNormalizedTableWidth(tableWidth, columnWidths) {
const totalWidth = (columnWidths || []).reduce(
(sum, width) => sum + (Number.isFinite(width) ? width : 0),
0,
)
if (totalWidth > 0) {
return { value: totalWidth, type: 'dxa' }
}
return tableWidth
}
function buildTableGridXml(columnWidths = []) {
if (!columnWidths.length) {
return ''
}
const gridColumns = columnWidths
.map((width) => `<w:gridCol w:w="${Math.max(300, Math.round(width))}"/>`)
.join('')
return `<w:tblGrid>${gridColumns}</w:tblGrid>`
}
function readElementWidth(element) {
if (!element) {
return ''
}
const directWidth = String(element.getAttribute?.('width') || '').trim()
if (directWidth) {
return directWidth
}
const styleText = String(element.getAttribute?.('style') || '').trim()
if (!styleText) {
return ''
}
const widthMatch = styleText.match(/(?:^|;)\s*width\s*:\s*([^;]+)/i)
if (widthMatch?.[1]) {
return widthMatch[1].trim()
}
const minWidthMatch = styleText.match(/(?:^|;)\s*min-width\s*:\s*([^;]+)/i)
return minWidthMatch?.[1] ? minWidthMatch[1].trim() : ''
}
function resolveTableWordWidth(rawWidth) {
const normalized = String(rawWidth || '').trim().toLowerCase()
if (!normalized) {
return { value: DEFAULT_WORD_TABLE_WIDTH_PERCENT, type: 'pct' }
}
const numericValue = parseFloat(normalized)
if (!Number.isFinite(numericValue) || numericValue <= 0) {
return { value: DEFAULT_WORD_TABLE_WIDTH_PERCENT, type: 'pct' }
}
if (normalized.endsWith('%')) {
return {
value: Math.max(500, Math.min(Math.round((numericValue / 100) * 5000), 5000)),
type: 'pct',
}
}
return resolveCellWordWidth(normalized) || { value: DEFAULT_WORD_TABLE_WIDTH_PERCENT, type: 'pct' }
}
function resolveCellWordWidth(rawWidth, fallbackWidth = null) {
const normalized = String(rawWidth || '').trim().toLowerCase()
if (!normalized) {
return fallbackWidth
}
const numericValue = parseFloat(normalized)
if (!Number.isFinite(numericValue) || numericValue <= 0) {
return fallbackWidth
}
if (normalized.endsWith('%')) {
return {
value: clampWordWidth(Math.round((numericValue / 100) * DEFAULT_WORD_TABLE_WIDTH_TWIPS)),
type: 'dxa',
}
}
if (normalized.endsWith('px')) {
return {
value: clampWordWidth(Math.round(numericValue * 15)),
type: 'dxa',
}
}
if (normalized.endsWith('pt')) {
return {
value: clampWordWidth(Math.round(numericValue * 20)),
type: 'dxa',
}
}
if (normalized.endsWith('cm')) {
return {
value: clampWordWidth(Math.round(numericValue * 567)),
type: 'dxa',
}
}
if (normalized.endsWith('mm')) {
return {
value: clampWordWidth(Math.round(numericValue * 57)),
type: 'dxa',
}
}
if (normalized.endsWith('in')) {
return {
value: clampWordWidth(Math.round(numericValue * 1440)),
type: 'dxa',
}
}
return {
value: clampWordWidth(Math.round(numericValue * 15)),
type: 'dxa',
}
}
function clampWordWidth(value) {
return Math.max(300, Math.min(Math.round(value || 0), DEFAULT_WORD_TABLE_WIDTH_TWIPS))
}
function parseSpanValue(rawValue) {
const value = parseInt(String(rawValue || '').trim(), 10)
return Number.isFinite(value) && value > 1 ? value : 1
}
function parseNodeStyleInfo(node) {
const tag = String(node?.tagName || '').toLowerCase()
const styleText = String(node?.getAttribute?.('style') || '')
const color = normalizeCssColor(readStyleProperty(styleText, 'color'))
const bgColor = normalizeCssColor(readStyleProperty(styleText, 'background-color'))
const textAlign = normalizeTextAlign(readStyleProperty(styleText, 'text-align'))
return {
bold: tag === 'strong' || tag === 'b',
italic: tag === 'em' || tag === 'i',
underline: tag === 'u',
color,
bgColor,
align: textAlign,
}
}
function mergeTextStyle(baseStyle = {}, overrideStyle = {}) {
return {
bold: overrideStyle.bold === true || baseStyle.bold === true,
italic: overrideStyle.italic === true || baseStyle.italic === true,
underline: overrideStyle.underline === true || baseStyle.underline === true,
color: overrideStyle.color || baseStyle.color || '',
bgColor: overrideStyle.bgColor || baseStyle.bgColor || '',
align: overrideStyle.align || baseStyle.align || '',
}
}
function readStyleProperty(styleText, propertyName) {
const match = String(styleText || '').match(
new RegExp(`(?:^|;)\\s*${escapeRegExp(propertyName)}\\s*:\\s*([^;]+)`, 'i'),
)
return match?.[1] ? match[1].trim() : ''
}
function normalizeCssColor(rawColor) {
const value = String(rawColor || '').trim().toLowerCase()
if (!value || value === 'transparent' || value === 'inherit') {
return ''
}
if (value.startsWith('#')) {
const hex = value.slice(1)
if (hex.length === 3) {
return hex
.split('')
.map((char) => char + char)
.join('')
.toUpperCase()
}
if (hex.length >= 6) {
return hex.slice(0, 6).toUpperCase()
}
}
const rgbMatch = value.match(/rgba?\(([^)]+)\)/i)
if (rgbMatch?.[1]) {
const [r, g, b] = rgbMatch[1]
.split(',')
.slice(0, 3)
.map((item) => Math.max(0, Math.min(255, parseInt(item.trim(), 10) || 0)))
return [r, g, b]
.map((num) => num.toString(16).padStart(2, '0'))
.join('')
.toUpperCase()
}
const colorMap = {
black: '000000',
white: 'FFFFFF',
red: 'FF0000',
blue: '0000FF',
green: '008000',
yellow: 'FFFF00',
gray: '808080',
grey: '808080',
orange: 'FFA500',
purple: '800080',
}
return colorMap[value] || ''
}
function normalizeTextAlign(rawValue) {
const value = String(rawValue || '').trim().toLowerCase()
if (['left', 'center', 'right', 'justify'].includes(value)) {
return value
}
return ''
}
function normalizeWordAlignment(value) {
if (value === 'left') return 'left'
if (value === 'center') return 'center'
if (value === 'right') return 'right'
if (value === 'justify') return 'both'
return ''
}
function escapeRegExp(text) {
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function flattenNodeText(node) {
if (!node) {
return ''
}
const text = Array.from(node.childNodes || [])
.map((child) => {
if (child.nodeType === TEXT_NODE) {
return child.data || child.nodeValue || ''
}
if (child.nodeType === ELEMENT_NODE) {
if (String(child.tagName || '').toLowerCase() === 'br') {
return '\n'
}
return flattenNodeText(child)
}
return ''
})
.join('')
return normalizeText(text)
}
function childElements(node) {
return Array.from(node?.childNodes || []).filter((item) => item.nodeType === ELEMENT_NODE)
}
function firstElementChild(node) {
return childElements(node)[0] || null
}
function normalizeText(text) {
return String(text || '')
.replace(/\u00a0/g, ' ')
.replace(/[\ufeff\u200b]/g, '')
.replace(/\r/g, '')
.replace(/\t/g, ' ')
}
function stringifyPlainTemplateValue(value) {
if (value === null || value === undefined) {
return ''
}
return String(value)
}
function normalizeFieldType(fieldType) {
const value = String(fieldType || 'input').trim()
if (value === 'select') {
return 'select'
}
if (value === 'rich_text') {
return 'rich_text'
}
return 'input'
}
function escapeXml(text) {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
export { DOCX_MIME_TYPE }
@@ -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();
}