feat: 引入新版报告在线编辑功能,优化旧版编辑器资源处理并增加背景设置,同时新增报告模板。

This commit is contained in:
Wei_佳 2025-12-04 17:14:57 +08:00
parent 89ccaa8b16
commit afea5b6335
8 changed files with 1450 additions and 83 deletions

View File

@ -5,5 +5,6 @@ VITE_PUBLIC_PATH = '/'
VITE_USE_PROXY = true
# base api
VITE_BASE_API = 'http://139.224.70.152:9990/api/v1'
# VITE_BASE_API = 'http://139.224.70.152:9990/api/v1'
VITE_BASE_API = 'https://manage-value.cdcee.net/api/v1'
# VITE_BASE_API = 'http://127.0.0.1:9999/api/v1'

View File

@ -20,6 +20,15 @@
"axios": "^1.4.0",
"dayjs": "^1.11.9",
"docxtemplater": "^3.67.5",
"@tiptap/core": "^2.6.6",
"@tiptap/starter-kit": "^2.6.6",
"@tiptap/extension-color": "^2.6.6",
"@tiptap/extension-text-style": "^2.6.6",
"@tiptap/extension-text-align": "^2.6.6",
"@tiptap/extension-underline": "^2.6.6",
"@tiptap/extension-image": "^2.6.6",
"@tiptap/extension-link": "^2.6.6",
"@tiptap/vue-3": "^2.6.6",
"dotenv": "^16.3.1",
"eslint": "^8.46.0",
"file-saver": "^2.0.5",

624
web/pnpm-lock.yaml generated
View File

@ -14,6 +14,33 @@ importers:
'@iconify/vue':
specifier: ^4.1.1
version: 4.1.2(vue@3.4.34(typescript@5.5.4))
'@tiptap/core':
specifier: ^2.6.6
version: 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-color':
specifier: ^2.6.6
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)))
'@tiptap/extension-image':
specifier: ^2.6.6
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-link':
specifier: ^2.6.6
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-text-align':
specifier: ^2.6.6
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-text-style':
specifier: ^2.6.6
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-underline':
specifier: ^2.6.6
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/starter-kit':
specifier: ^2.6.6
version: 2.27.1
'@tiptap/vue-3':
specifier: ^2.6.6
version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(vue@3.4.34(typescript@5.5.4))
'@unocss/eslint-config':
specifier: ^0.55.0
version: 0.55.7(eslint@8.57.0)(typescript@5.5.4)
@ -29,9 +56,6 @@ importers:
dayjs:
specifier: ^1.11.9
version: 1.11.12
docx-preview:
specifier: ^0.3.7
version: 0.3.7
docxtemplater:
specifier: ^3.67.5
version: 3.67.5
@ -387,6 +411,12 @@ packages:
'@polka/url@1.0.0-next.25':
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@rollup/pluginutils@4.2.1':
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
@ -400,6 +430,163 @@ packages:
rollup:
optional: true
'@tiptap/core@2.27.1':
resolution: {integrity: sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==}
peerDependencies:
'@tiptap/pm': ^2.7.0
'@tiptap/extension-blockquote@2.27.1':
resolution: {integrity: sha512-QrUX3muElDrNjKM3nqCSAtm3H3pT33c6ON8kwRiQboOAjT/9D57Cs7XEVY7r6rMaJPeKztrRUrNVF9w/w/6B0A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bold@2.27.1':
resolution: {integrity: sha512-g4l4p892x/r7mhea8syp3fNYODxsDrimgouQ+q4DKXIgQmm5+uNhyuEPexP3I8TFNXqQ4DlMNFoM9yCqk97etQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-bubble-menu@2.27.1':
resolution: {integrity: sha512-ki1R27VsSvY2tT9Q2DIlcATwLOoEjf5DsN+5sExarQ8S/ZxT/tvIjRxB8Dx7lb2a818W5f/NER26YchGtmHfpg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-bullet-list@2.27.1':
resolution: {integrity: sha512-5FmnfXkJ76wN4EbJNzBhAlmQxho8yEMIJLchTGmXdsD/n/tsyVVtewnQYaIOj/Z7naaGySTGDmjVtLgTuQ+Sxw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-code-block@2.27.1':
resolution: {integrity: sha512-wCI5VIOfSAdkenCWFvh4m8FFCJ51EOK+CUmOC/PWUjyo2Dgn8QC8HMi015q8XF7886T0KvYVVoqxmxJSUDAYNg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-code@2.27.1':
resolution: {integrity: sha512-i65wUGJevzBTIIUBHBc1ggVa27bgemvGl/tY1/89fEuS/0Xmre+OQjw8rCtSLevoHSiYYLgLRlvjtUSUhE4kgg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-color@2.27.1':
resolution: {integrity: sha512-raYRsdG2tZvVvY1LV/VTZnDG44Y0xRBwo5CZEat0OUqdx34dfvCtYm8HIOTyWBwr7OOW+yR4O1Vc2zFkmfthZw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-style': ^2.7.0
'@tiptap/extension-document@2.27.1':
resolution: {integrity: sha512-NtJzJY7Q/6XWjpOm5OXKrnEaofrcc1XOTYlo/SaTwl8k2bZo918Vl0IDBWhPVDsUN7kx767uHwbtuQZ+9I82hA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-dropcursor@2.27.1':
resolution: {integrity: sha512-3MBQRGHHZ0by3OT0CWbLKS7J3PH9PpobrXjmIR7kr0nde7+bHqxXiVNuuIf501oKU9rnEUSedipSHkLYGkmfsA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-floating-menu@2.27.1':
resolution: {integrity: sha512-nUk/8DbiXO69l6FDwkWso94BTf52IBoWALo+YGWT6o+FO6cI9LbUGghEX2CdmQYXCvSvwvISF2jXeLQWNZvPZQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-gapcursor@2.27.1':
resolution: {integrity: sha512-A9e1jr+jGhDWzNSXtIO6PYVYhf5j/udjbZwMja+wCE/3KvZU9V3IrnGKz1xNW+2Q2BDOe1QO7j5uVL9ElR6nTA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-hard-break@2.27.1':
resolution: {integrity: sha512-W4hHa4Io6QCTwpyTlN6UAvqMIQ7t56kIUByZhyY9EWrg/+JpbfpxE1kXFLPB4ZGgwBknFOw+e4bJ1j3oAbTJFw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-heading@2.27.1':
resolution: {integrity: sha512-6xoC7igZlW1EmnQ5WVH9IL7P1nCQb3bBUaIDLvk7LbweEogcTUECI4Xg1vxMOVmj9tlDe1I4BsgfcKpB5KEsZw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-history@2.27.1':
resolution: {integrity: sha512-K8PHC9gegSAt0wzSlsd4aUpoEyIJYOmVVeyniHr1P1mIblW1KYEDbRGbDlrLALTyUEfMcBhdIm8zrB9X2Nihvg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-horizontal-rule@2.27.1':
resolution: {integrity: sha512-WxXWGEEsqDmGIF2o9av+3r9Qje4CKrqrpeQY6aRO5bxvWX9AabQCfasepayBok6uwtvNzh3Xpsn9zbbSk09dNA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-image@2.27.1':
resolution: {integrity: sha512-wu3vMKDYWJwKS6Hrw5PPCKBO2RxyHNeFLiA/uDErEV7axzNpievK/U9DyaDXmtK3K/h1XzJAJz19X+2d/pY68w==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-italic@2.27.1':
resolution: {integrity: sha512-rcm0GyniWW0UhcNI9+1eIK64GqWQLyIIrWGINslvqSUoBc+WkfocLvv4CMpRkzKlfsAxwVIBuH2eLxHKDtAREA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-link@2.27.1':
resolution: {integrity: sha512-cCwWPZsnVh9MXnGOqSIRXPPuUixRDK8eMN2TvqwbxUBb1TU7b/HtNvfMU4tAOqAuMRJ0aJkFuf3eB0Gi8LVb1g==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@tiptap/extension-list-item@2.27.1':
resolution: {integrity: sha512-dtsxvtzxfwOJP6dKGf0vb2MJAoDF2NxoiWzpq0XTvo7NGGYUHfuHjX07Zp0dYqb4seaDXjwsi5BIQUOp3+WMFQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-ordered-list@2.27.1':
resolution: {integrity: sha512-U1/sWxc2TciozQsZjH35temyidYUjvroHj3PUPzPyh19w2fwKh1NSbFybWuoYs6jS3XnMSwnM2vF52tOwvfEmA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-paragraph@2.27.1':
resolution: {integrity: sha512-R3QdrHcUdFAsdsn2UAIvhY0yWyHjqGyP/Rv8RRdN0OyFiTKtwTPqreKMHKJOflgX4sMJl/OpHTpNG1Kaf7Lo2A==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-strike@2.27.1':
resolution: {integrity: sha512-S9I//K8KPgfFTC5I5lorClzXk0g4lrAv9y5qHzHO5EOWt7AFl0YTg2oN8NKSIBK4bHRnPIrjJJKv+dDFnUp5jQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-align@2.27.1':
resolution: {integrity: sha512-D7dLPk7y5mDn9ZNANQ4K2gCq4vy+Emm5AdeWOGzNeqJsYrBotiQYXd9rb1QYjdup2kzAoKduMTUXV92ujo5cEg==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text-style@2.27.1':
resolution: {integrity: sha512-NagQ9qLk0Ril83gfrk+C65SvTqPjL3WVnLF2arsEVnCrxcx3uDOvdJW67f/K5HEwEHsoqJ4Zq9Irco/koXrOXA==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-text@2.27.1':
resolution: {integrity: sha512-a4GCT+GZ9tUwl82F4CEum9/+WsuW0/De9Be/NqrMmi7eNfAwbUTbLCTFU0gEvv25WMHCoUzaeNk/qGmzeVPJ1Q==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-underline@2.27.1':
resolution: {integrity: sha512-fPTmfJFAQWg1O/os1pYSPVdtvly6eW/w5sDofG7pre+bdQUN+8s1cZYelSuj/ltNVioRaB2Ws7tvNgnHL0aAJQ==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm@2.27.1':
resolution: {integrity: sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==}
'@tiptap/starter-kit@2.27.1':
resolution: {integrity: sha512-uQQlP0Nmn9eq19qm8YoOeloEfmcGbPpB1cujq54Q6nPgxaBozR7rE7tXbFTinxRW2+Hr7XyNWhpjB7DMNkdU2Q==}
'@tiptap/vue-3@2.27.1':
resolution: {integrity: sha512-1D0gTlGBeDmrl+APm/JKoNs/KnW5PecpD1PbQmg2GEHVxVZNeIUpG48D/V5uTIiRwJsDx3PMd8AmsZs1QS6GLw==}
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
vue: ^3.0.0
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@ -413,12 +600,21 @@ packages:
'@types/katex@0.16.7':
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
'@types/lodash@4.17.7':
resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/node@22.0.0':
resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==}
@ -839,6 +1035,9 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -966,9 +1165,6 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
docx-preview@0.3.7:
resolution: {integrity: sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==}
docxtemplater@3.67.5:
resolution: {integrity: sha512-Jnh9rdMf5sDmrfONs3nVDhZwVFxLNdP3RVHkqLQYA6eZLkWA+kx5aYy0cVmEqJcVIaFYf5JJEFdClD7fn6ZUng==}
engines: {node: '>=0.10'}
@ -1645,6 +1841,12 @@ packages:
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
linkifyjs@4.3.2:
resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==}
loader-utils@1.4.2:
resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==}
engines: {node: '>=4.0.0'}
@ -1684,6 +1886,10 @@ packages:
resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
engines: {node: '>=0.10.0'}
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
marked@17.0.1:
resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
engines: {node: '>= 20'}
@ -1695,6 +1901,9 @@ packages:
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
merge-options@1.0.1:
resolution: {integrity: sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==}
engines: {node: '>=4'}
@ -1844,6 +2053,9 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
orderedmap@2.1.1:
resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@ -1983,9 +2195,71 @@ packages:
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
prosemirror-changeset@2.3.1:
resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==}
prosemirror-collab@1.3.1:
resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
prosemirror-commands@1.7.1:
resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
prosemirror-dropcursor@1.8.2:
resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==}
prosemirror-gapcursor@1.4.0:
resolution: {integrity: sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==}
prosemirror-history@1.5.0:
resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==}
prosemirror-inputrules@1.5.1:
resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==}
prosemirror-keymap@1.2.3:
resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==}
prosemirror-markdown@1.13.2:
resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==}
prosemirror-menu@1.2.5:
resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==}
prosemirror-model@1.25.4:
resolution: {integrity: sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==}
prosemirror-schema-basic@1.2.4:
resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==}
prosemirror-schema-list@1.5.1:
resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==}
prosemirror-state@1.4.4:
resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==}
prosemirror-tables@1.8.3:
resolution: {integrity: sha512-wbqCR/RlRPRe41a4LFtmhKElzBEfBTdtAYWNIGHM6X2e24NN/MTNUKyXjjphfAfdQce37Kh/5yf765mLPYDe7Q==}
prosemirror-trailing-node@3.0.0:
resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==}
peerDependencies:
prosemirror-model: ^1.22.1
prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8
prosemirror-transform@1.10.5:
resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==}
prosemirror-view@1.41.4:
resolution: {integrity: sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@ -2075,6 +2349,9 @@ packages:
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
rope-sequence@1.3.4:
resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==}
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@ -2280,6 +2557,9 @@ packages:
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
tippy.js@6.3.7:
resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
@ -2353,6 +2633,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.5.4:
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
@ -2553,6 +2836,9 @@ packages:
peerDependencies:
vue: ^3.0.11
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
webpack-sources@3.2.3:
resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==}
engines: {node: '>=10.13.0'}
@ -2822,6 +3108,10 @@ snapshots:
'@polka/url@1.0.0-next.25': {}
'@popperjs/core@2.11.8': {}
'@remirror/core-constants@3.0.0': {}
'@rollup/pluginutils@4.2.1':
dependencies:
estree-walker: 2.0.2
@ -2835,6 +3125,179 @@ snapshots:
optionalDependencies:
rollup: 3.29.4
'@tiptap/core@2.27.1(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/pm': 2.27.1
'@tiptap/extension-blockquote@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-bold@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-bubble-menu@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
tippy.js: 6.3.7
'@tiptap/extension-bullet-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-code-block@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
'@tiptap/extension-code@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-color@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-text-style': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-document@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-dropcursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
'@tiptap/extension-floating-menu@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
tippy.js: 6.3.7
'@tiptap/extension-gapcursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
'@tiptap/extension-hard-break@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-heading@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-history@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
'@tiptap/extension-horizontal-rule@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
'@tiptap/extension-image@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-italic@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-link@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
linkifyjs: 4.3.2
'@tiptap/extension-list-item@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-ordered-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-paragraph@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-strike@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-text-align@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-text@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-underline@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/pm@2.27.1':
dependencies:
prosemirror-changeset: 2.3.1
prosemirror-collab: 1.3.1
prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2
prosemirror-gapcursor: 1.4.0
prosemirror-history: 1.5.0
prosemirror-inputrules: 1.5.1
prosemirror-keymap: 1.2.3
prosemirror-markdown: 1.13.2
prosemirror-menu: 1.2.5
prosemirror-model: 1.25.4
prosemirror-schema-basic: 1.2.4
prosemirror-schema-list: 1.5.1
prosemirror-state: 1.4.4
prosemirror-tables: 1.8.3
prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4)
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
'@tiptap/starter-kit@2.27.1':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-blockquote': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-bold': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-bullet-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-code': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-document': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-dropcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-gapcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-hard-break': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-heading': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-history': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-horizontal-rule': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-italic': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-list-item': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-ordered-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-paragraph': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-strike': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-text': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/extension-text-style': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))
'@tiptap/pm': 2.27.1
'@tiptap/vue-3@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(vue@3.4.34(typescript@5.5.4))':
dependencies:
'@tiptap/core': 2.27.1(@tiptap/pm@2.27.1)
'@tiptap/extension-bubble-menu': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/extension-floating-menu': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)
'@tiptap/pm': 2.27.1
vue: 3.4.34(typescript@5.5.4)
'@trysound/sax@0.2.0': {}
'@types/estree@1.0.5': {}
@ -2843,12 +3306,21 @@ snapshots:
'@types/katex@0.16.7': {}
'@types/linkify-it@5.0.0': {}
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.7
'@types/lodash@4.17.7': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdurl@2.0.0': {}
'@types/node@22.0.0':
dependencies:
undici-types: 6.11.1
@ -3397,6 +3869,8 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
crelt@1.0.6: {}
cross-spawn@7.0.3:
dependencies:
path-key: 3.1.1
@ -3519,10 +3993,6 @@ snapshots:
dependencies:
esutils: 2.0.3
docx-preview@0.3.7:
dependencies:
jszip: 3.10.1
docxtemplater@3.67.5:
dependencies:
'@xmldom/xmldom': 0.9.8
@ -4299,6 +4769,12 @@ snapshots:
dependencies:
immediate: 3.0.6
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
linkifyjs@4.3.2: {}
loader-utils@1.4.2:
dependencies:
big.js: 5.2.2
@ -4336,12 +4812,23 @@ snapshots:
dependencies:
object-visit: 1.0.1
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
marked@17.0.1: {}
mdn-data@2.0.14: {}
mdn-data@2.0.30: {}
mdurl@2.0.0: {}
merge-options@1.0.1:
dependencies:
is-plain-obj: 1.1.0
@ -4538,6 +5025,8 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
orderedmap@2.1.1: {}
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@ -4663,8 +5152,113 @@ snapshots:
process-nextick-args@2.0.1: {}
prosemirror-changeset@2.3.1:
dependencies:
prosemirror-transform: 1.10.5
prosemirror-collab@1.3.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-commands@1.7.1:
dependencies:
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-dropcursor@1.8.2:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-gapcursor@1.4.0:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-view: 1.41.4
prosemirror-history@1.5.0:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
rope-sequence: 1.3.4
prosemirror-inputrules@1.5.1:
dependencies:
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-keymap@1.2.3:
dependencies:
prosemirror-state: 1.4.4
w3c-keyname: 2.2.8
prosemirror-markdown@1.13.2:
dependencies:
'@types/markdown-it': 14.1.2
markdown-it: 14.1.0
prosemirror-model: 1.25.4
prosemirror-menu@1.2.5:
dependencies:
crelt: 1.0.6
prosemirror-commands: 1.7.1
prosemirror-history: 1.5.0
prosemirror-state: 1.4.4
prosemirror-model@1.25.4:
dependencies:
orderedmap: 2.1.1
prosemirror-schema-basic@1.2.4:
dependencies:
prosemirror-model: 1.25.4
prosemirror-schema-list@1.5.1:
dependencies:
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-state@1.4.4:
dependencies:
prosemirror-model: 1.25.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-tables@1.8.3:
dependencies:
prosemirror-keymap: 1.2.3
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
prosemirror-view: 1.41.4
prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4):
dependencies:
'@remirror/core-constants': 3.0.0
escape-string-regexp: 4.0.0
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-view: 1.41.4
prosemirror-transform@1.10.5:
dependencies:
prosemirror-model: 1.25.4
prosemirror-view@1.41.4:
dependencies:
prosemirror-model: 1.25.4
prosemirror-state: 1.4.4
prosemirror-transform: 1.10.5
proxy-from-env@1.1.0: {}
punycode.js@2.3.1: {}
punycode@2.3.1: {}
query-string@4.3.4:
@ -4747,6 +5341,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
rope-sequence@1.3.4: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@ -4994,6 +5590,10 @@ snapshots:
text-table@0.2.0: {}
tippy.js@6.3.7:
dependencies:
'@popperjs/core': 2.11.8
to-fast-properties@2.0.0: {}
to-object-path@0.3.0:
@ -5081,6 +5681,8 @@ snapshots:
typescript@5.5.4: {}
uc.micro@2.1.0: {}
ufo@1.5.4: {}
unbox-primitive@1.0.2:
@ -5336,6 +5938,8 @@ snapshots:
vooks: 0.2.12(vue@3.4.34(typescript@5.5.4))
vue: 3.4.34(typescript@5.5.4)
w3c-keyname@2.2.8: {}
webpack-sources@3.2.3: {}
webpack-virtual-modules@0.6.2: {}

Binary file not shown.

View File

@ -139,6 +139,29 @@ export const basicRoutes = [
},
],
},
{
name: 'ReportEditorNew',
path: '/valuation/audit/editor-new',
component: Layout,
isHidden: true,
meta: {
title: '报告在线编辑(新)',
icon: 'mdi:file-document-edit-outline',
order: 100,
},
children: [
{
path: '',
component: () => import('@/views/valuation/audit/editor/new.vue'),
name: 'ReportEditorNewPage',
meta: {
title: '报告在线编辑(新)',
icon: 'mdi:file-document-edit-outline',
order: 100,
},
},
],
},
{
name: 'Login',
path: '/login',

View File

@ -197,6 +197,22 @@ const handleOpenEditor = () => {
router.push({ path: '/valuation/audit/editor', query })
}
// 线
const handleOpenEditorNew = () => {
const detail = props.certificateData?.detailData
if (!detail?.id) {
message.error('缺少详情数据,无法打开新版编辑页')
return
}
const query = {
valuationId: detail.id,
templateId: detail.template_id || detail.templateId,
title: detail.asset_name || detail.institution || '评估报告'
}
router.push({ path: '/valuation/audit/editor-new', query })
}
//
const handlePreview = (file) => {
//
@ -255,6 +271,9 @@ const handleSmsNotify = async () => {
<NButton text type="info" @click="handleOpenEditor">
在线编辑
</NButton>
<NButton text type="success" @click="handleOpenEditorNew">
新版在线编辑
</NButton>
</div>
<div class="upload-content">
<NUpload

View File

@ -17,6 +17,8 @@ const exporting = ref(false)
const editorRef = ref(null)
const htmlContent = ref('')
const detailData = ref(null)
const selectedBackground = ref(null)
const customBackgroundUrl = ref('')
const pageMeta = computed(() => ({
valuationId: route.query.valuationId || route.query.id,
@ -41,6 +43,14 @@ const defaultHtml = `
</div>
`
const backgroundOptions = computed(() =>
Object.entries(assetMap)
.filter(([name]) => name.match(/\.(png|jpe?g|svg)$/i))
.map(([name, url]) => ({ label: name, value: url }))
)
const effectiveBackground = computed(() => customBackgroundUrl.value || selectedBackground.value || '')
const syncEditorContent = async () => {
await nextTick()
if (editorRef.value) {
@ -85,12 +95,12 @@ import templateHtmlRaw from '@/assets/report_template/report_template.html?raw'
import styleCssRaw from '@/assets/report_template/report_template/style.css?raw'
// 使 eager as: 'url' URL
const assetModules = import.meta.glob('../../../../assets/report_template/report_template/*.*', {
eager: true,
as: 'url'
const assetModules = import.meta.glob('../../../../assets/report_template/report_template/*.*', {
eager: true,
as: 'url'
})
// URL
// URL base64
const assetMap = {}
Object.entries(assetModules).forEach(([path, url]) => {
const filename = path.split('/').pop()
@ -101,105 +111,126 @@ Object.entries(assetModules).forEach(([path, url]) => {
console.log('Asset Map:', assetMap)
// URL base64 data URL
const convertToDataURL = async (url) => {
try {
const response = await fetch(url)
const blob = await response.blob()
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(blob)
})
} catch (error) {
console.error('Failed to convert to data URL:', url, error)
return url
}
// URL dataURL
const mapAssetUrl = (rawUrl) => {
if (!rawUrl) return rawUrl
const clean = rawUrl.trim().replace(/^['"]|['"]$/g, '')
if (/^data:/.test(clean) || /^(https?:)?\/\//.test(clean)) return clean
const filename = clean.split('/').pop()?.split(/[?#]/)[0]
if (filename && assetMap[filename]) return assetMap[filename]
return clean
}
// base64
const convertAssetsToDataURLs = async () => {
const dataURLMap = {}
console.log('开始转换图片为 base64...')
const promises = Object.entries(assetMap).map(async ([filename, url]) => {
//
if (filename.match(/\.(png|jpg|jpeg|svg|gif)$/i)) {
console.log(`转换图片: ${filename}`)
const dataURL = await convertToDataURL(url)
dataURLMap[filename] = dataURL
console.log(`${filename} 转换完成,大小: ${(dataURL.length / 1024).toFixed(2)} KB`)
} else {
dataURLMap[filename] = url
}
const replaceAssetUrlsInCss = (css) =>
css.replace(/url\(([^)]+)\)/g, (match, url) => `url(${mapAssetUrl(url)})`)
const replaceAssetUrlsInHtml = (html) => {
let processed = html
processed = processed.replace(/(href|src|data)=["']([^"']+)["']/g, (match, attr, url) => {
const mapped = mapAssetUrl(url)
return `${attr}="${mapped}"`
})
await Promise.all(promises)
console.log('所有图片转换完成:', Object.keys(dataURLMap).filter(k => k.match(/\.(png|jpg|jpeg|svg|gif)$/i)))
return dataURLMap
processed = processed.replace(/url\(([^)]+)\)/g, (match, url) => `url(${mapAssetUrl(url)})`)
// svg xlink:href/href
processed = processed.replace(/(xlink:href|href)=["']([^"']+)["']/g, (match, attr, url) => {
const mapped = mapAssetUrl(url)
return `${attr}="${mapped}"`
})
return processed
}
const processTemplate = async (html, css) => {
// base64
const dataURLMap = await convertAssetsToDataURLs()
console.log('dataURLMap keys:', Object.keys(dataURLMap))
// 1. CSS
let processedCss = css.replace(/url\(['"]?([^'"()]+)['"]?\)/g, (match, url) => {
const filename = url.split('/').pop()
if (filename && dataURLMap[filename]) {
console.log(`CSS: 替换 ${filename}`)
return `url(${dataURLMap[filename]})`
}
return match
})
// 1) CSS URL
const processedCss = replaceAssetUrlsInCss(css)
// 2. HTML ()
let processedHtml = html.replace(/(href|src|data)=["'](?:\.\/)?report_template\/([^"')]+)["']/g, (match, attr, filename) => {
const cleanFilename = filename.split(/[?#]/)[0]
if (dataURLMap[cleanFilename]) {
console.log(`HTML: 替换 ${attr}="${cleanFilename}" 为 base64`)
return `${attr}="${dataURLMap[cleanFilename]}"`
}
console.warn(`HTML: 未找到 ${cleanFilename}`)
return match
})
// 2) HTML styleSVG href URL
let processedHtml = replaceAssetUrlsInHtml(html)
// 3. object/embed img
// 3) object/embed img便
let objectCount = 0
processedHtml = processedHtml.replace(
/<object([^>]*?)>[\s\S]*?<\/object>/g,
(match, attrs) => {
const dataMatch = attrs.match(/data=["']([^"']+)["']/)
if (!dataMatch) return match
const src = dataMatch[1]
const src = mapAssetUrl(dataMatch[1])
const classMatch = attrs.match(/class=["']([^"']+)["']/)
const className = classMatch ? classMatch[1] : ''
const styleMatch = attrs.match(/style=["']([^"']+)["']/)
const style = styleMatch ? styleMatch[1] : ''
objectCount++
console.log(`转换 <object> #${objectCount}: ${src}`)
return `<img src="${src}" class="${className}" style="${style}" />`
}
)
console.log(`总共转换了 ${objectCount} 个 <object> 标签`)
// 4. CSS
// 4) CSS
processedHtml = processedHtml.replace(/<link[^>]+href=["'].*?style\.css["'][^>]*>/, '')
processedHtml = processedHtml.replace('</head>', `<style>\n${processedCss}\n</style></head>`)
// HTML 500
console.log('处理后的 HTML 预览:', processedHtml.substring(0, 500))
return processedHtml
}
const applyCustomBackgroundToDom = () => {
if (!editorRef.value) return
const target = editorRef.value
if (effectiveBackground.value) {
target.style.backgroundImage = `url(${effectiveBackground.value})`
target.style.backgroundSize = 'cover'
target.style.backgroundRepeat = 'no-repeat'
target.style.backgroundPosition = 'center top'
} else {
target.style.backgroundImage = ''
target.style.backgroundSize = ''
target.style.backgroundRepeat = ''
target.style.backgroundPosition = ''
}
}
const injectBackgroundIntoHtml = (html) => {
if (!effectiveBackground.value || !html) return html
const styleTag = `<style id="custom-bg-style">body{background-image:url('${effectiveBackground.value}');background-size:cover;background-repeat:no-repeat;background-position:center top;}</style>`
if (html.includes('id="custom-bg-style"')) {
return html.replace(/<style id="custom-bg-style">[\s\S]*?<\/style>/, styleTag)
}
if (html.includes('</head>')) {
return html.replace('</head>', `${styleTag}</head>`)
}
return `${styleTag}${html}`
}
const handleBackgroundSelect = (value) => {
selectedBackground.value = value || ''
if (value) customBackgroundUrl.value = ''
applyCustomBackgroundToDom()
}
const handleBackgroundUpload = (event) => {
const file = event?.target?.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
customBackgroundUrl.value = reader.result
selectedBackground.value = ''
applyCustomBackgroundToDom()
}
reader.readAsDataURL(file)
}
watch(effectiveBackground, applyCustomBackgroundToDom)
const loadTemplate = async () => {
loading.value = true
try {
@ -212,6 +243,7 @@ const loadTemplate = async () => {
editorRef.value.innerHTML = html
editorRef.value.setAttribute('contenteditable', 'true')
htmlContent.value = html
applyCustomBackgroundToDom()
} else {
await syncEditorContent()
}
@ -315,12 +347,13 @@ const handleSave = async () => {
message.warning('暂无内容可保存')
return
}
const htmlToPersist = injectBackgroundIntoHtml(htmlContent.value)
saving.value = true
try {
await api.saveReportDraft({
valuation_id: pageMeta.value.valuationId,
template_id: pageMeta.value.templateId,
html: htmlContent.value,
html: htmlToPersist,
})
message.success('草稿已保存')
} catch (error) {
@ -336,11 +369,13 @@ const handleExport = async () => {
message.error('缺少参数,无法导出')
return
}
const htmlToPersist = injectBackgroundIntoHtml(htmlContent.value)
exporting.value = true
try {
const res = await api.exportReportTemplate({
valuation_id: pageMeta.value.valuationId,
template_id: pageMeta.value.templateId,
html: htmlToPersist,
})
const blob = res instanceof Blob ? res : new Blob([res])
const fileName = `${pageMeta.value.title || '报告'}.pdf`
@ -425,6 +460,26 @@ onMounted(loadTemplate)
<input type="color" @input="applyBackColor" />
</label>
</NSpace>
<div class="bg-controls">
<span class="bg-label">背景</span>
<NSelect
size="small"
style="width: 180px"
placeholder="选择模板背景"
:options="backgroundOptions"
@update:value="handleBackgroundSelect"
clearable
/>
<label class="bg-upload">
自定义
<input type="file" accept="image/*" @change="handleBackgroundUpload" />
</label>
<span
v-if="effectiveBackground"
class="bg-preview"
:style="{ backgroundImage: `url(${effectiveBackground})` }"
></span>
</div>
</div>
<NSpin :show="loading">
@ -498,6 +553,32 @@ onMounted(loadTemplate)
background: transparent;
}
.bg-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.bg-label {
font-size: 12px;
color: #555;
}
.bg-upload input {
font-size: 12px;
}
.bg-preview {
width: 44px;
height: 30px;
border: 1px solid #e0e4eb;
border-radius: 6px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.editor-wrapper {
width: 100%;
display: flex;

View File

@ -0,0 +1,630 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { NButton, NSelect, NSpace, NSpin, NTag, useMessage } from 'naive-ui'
import { saveAs } from 'file-saver'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import { Extension, Node, mergeAttributes } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import TextStyle from '@tiptap/extension-text-style'
import Color from '@tiptap/extension-color'
import TextAlign from '@tiptap/extension-text-align'
import Image from '@tiptap/extension-image'
import Link from '@tiptap/extension-link'
import CommonPage from '@/components/page/CommonPage.vue'
import api from '@/api'
// 使 TipTap //
const route = useRoute()
const router = useRouter()
const message = useMessage()
const loading = ref(false)
const saving = ref(false)
const exporting = ref(false)
const htmlContent = ref('')
const detailData = ref(null)
const selectedBackground = ref(null)
const customBackgroundUrl = ref('')
const editorMountRef = ref(null)
const pageMeta = computed(() => ({
valuationId: route.query.valuationId || route.query.id,
templateId: route.query.templateId,
title: route.query.title || '评估报告',
}))
const defaultHtml = `
<div class="doc-header">
<h1>新版在线编辑</h1>
<p>这里是新版富文本编辑入口可自由编辑内容并切换背景</p>
</div>
<div class="doc-block">
<h2>正文示例</h2>
<p>可粘贴 Word/网页内容或直接编辑</p>
</div>
`
//
import templateHtmlRaw from '@/assets/report_template/report_template.html?raw'
import styleCssRaw from '@/assets/report_template/report_template/style.css?raw'
// 使 eager as: 'url' URL
const assetModules = import.meta.glob('../../../../assets/report_template/report_template/*.*', {
eager: true,
as: 'url'
})
// URL
const assetMap = {}
Object.entries(assetModules).forEach(([path, url]) => {
const filename = path.split('/').pop()
if (filename) assetMap[filename] = url
})
const backgroundOptions = computed(() =>
Object.entries(assetMap)
.filter(([name]) => name.match(/\.(png|jpe?g|svg)$/i))
.map(([name, url]) => ({ label: name, value: url }))
)
// class/style/id
const GlobalAttrs = Extension.create({
name: 'globalAttrs',
addGlobalAttributes() {
return [
{
types: ['paragraph', 'heading', 'textStyle', 'image', 'bulletList', 'orderedList', 'listItem', 'doc', 'div', 'text'],
attributes: {
class: { default: null },
style: { default: null },
id: { default: null },
'data-*': { default: null },
},
},
]
},
})
// div
const Div = Node.create({
name: 'div',
group: 'block',
content: 'inline*',
parseHTML() {
return [{ tag: 'div' }]
},
renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(HTMLAttributes), 0]
},
})
const effectiveBackground = computed(() => customBackgroundUrl.value || selectedBackground.value || '')
const ensureDetailData = async () => {
if (detailData.value || !pageMeta.value.valuationId) return
try {
const { data } = await api.getValuationById({ valuation_id: pageMeta.value.valuationId })
detailData.value = data
} catch (error) {
console.error('获取详情数据失败', error)
message.warning('未获取到估值详情,占位符将无法替换')
}
}
const formatDateVars = () => {
const now = new Date()
const yyyy = String(now.getFullYear())
const mm = String(now.getMonth() + 1).padStart(2, '0')
const dd = String(now.getDate()).padStart(2, '0')
return { yyyy, mm, dd, yyyymmdd: `${yyyy}${mm}${dd}` }
}
const applyTemplateVars = (html) => {
if (!html) return html
const vars = {
asset_name: detailData.value?.asset_name || detailData.value?.institution || '',
...formatDateVars(),
...detailData.value,
}
return html.replace(/\$\{([^}]+)\}/g, (_, key) => {
const val = vars[key]
return val !== undefined && val !== null ? String(val) : ''
})
}
//
const mapAssetUrl = (rawUrl) => {
if (!rawUrl) return rawUrl
const clean = rawUrl.trim().replace(/^['"]|['"]$/g, '')
if (/^data:/.test(clean) || /^(https?:)?\/\//.test(clean)) return clean
const filename = clean.split('/').pop()?.split(/[?#]/)[0]
if (filename && assetMap[filename]) return assetMap[filename]
return clean
}
const replaceAssetUrlsInCss = (css) =>
css.replace(/url\(([^)]+)\)/g, (match, url) => `url(${mapAssetUrl(url)})`)
const replaceAssetUrlsInHtml = (html) => {
let processed = html
processed = processed.replace(/(href|src|data)=["']([^"']+)["']/g, (match, attr, url) => {
const mapped = mapAssetUrl(url)
return `${attr}="${mapped}"`
})
processed = processed.replace(/url\(([^)]+)\)/g, (match, url) => `url(${mapAssetUrl(url)})`)
processed = processed.replace(/(xlink:href|href)=["']([^"']+)["']/g, (match, attr, url) => {
const mapped = mapAssetUrl(url)
return `${attr}="${mapped}"`
})
return processed
}
const processTemplate = async (html, css) => {
const processedCss = replaceAssetUrlsInCss(css)
let processedHtml = replaceAssetUrlsInHtml(html)
processedHtml = processedHtml.replace(
/<object([^>]*?)>[\s\S]*?<\/object>/g,
(match, attrs) => {
const dataMatch = attrs.match(/data=["']([^"']+)["']/)
if (!dataMatch) return match
const src = mapAssetUrl(dataMatch[1])
const classMatch = attrs.match(/class=["']([^"']+)["']/)
const className = classMatch ? classMatch[1] : ''
const styleMatch = attrs.match(/style=["']([^"']+)["']/)
const style = styleMatch ? styleMatch[1] : ''
return `<img src="${src}" class="${className}" style="${style}" />`
}
)
// head/link body 使
const bodyMatch = processedHtml.match(/<body[^>]*>([\s\S]*?)<\/body>/i)
const bodyContent = bodyMatch ? bodyMatch[1] : processedHtml
return { bodyContent, processedCss }
}
const applyCustomBackgroundToDom = () => {
if (!editorMountRef.value) return
const target = editorMountRef.value
if (effectiveBackground.value) {
target.style.backgroundImage = `url(${effectiveBackground.value})`
target.style.backgroundSize = 'cover'
target.style.backgroundRepeat = 'no-repeat'
target.style.backgroundPosition = 'center top'
} else {
target.style.backgroundImage = ''
target.style.backgroundSize = ''
target.style.backgroundRepeat = ''
target.style.backgroundPosition = ''
}
}
const applyTemplateStyle = (css) => {
if (!css) return
const styleId = 'report-template-style'
let styleEl = document.getElementById(styleId)
if (!styleEl) {
styleEl = document.createElement('style')
styleEl.id = styleId
document.head.appendChild(styleEl)
}
styleEl.textContent = css
}
const injectBackgroundIntoHtml = (html) => {
if (!effectiveBackground.value || !html) return html
const styleTag = `<style id="custom-bg-style">body{background-image:url('${effectiveBackground.value}');background-size:cover;background-repeat:no-repeat;background-position:center top;}</style>`
if (html.includes('id="custom-bg-style"')) {
return html.replace(/<style id="custom-bg-style">[\s\S]*?<\/style>/, styleTag)
}
if (html.includes('</head>')) {
return html.replace('</head>', `${styleTag}</head>`)
}
return `${styleTag}${html}`
}
const handleBackgroundSelect = (value) => {
selectedBackground.value = value || ''
if (value) customBackgroundUrl.value = ''
applyCustomBackgroundToDom()
}
const handleBackgroundUpload = (event) => {
const file = event?.target?.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = () => {
customBackgroundUrl.value = reader.result
selectedBackground.value = ''
applyCustomBackgroundToDom()
}
reader.readAsDataURL(file)
}
watch(effectiveBackground, applyCustomBackgroundToDom)
const editor = useEditor({
content: defaultHtml,
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3, 4] },
bulletList: { keepMarks: true },
orderedList: { keepMarks: true },
}),
Div,
GlobalAttrs,
Underline,
TextStyle,
Color,
TextAlign.configure({ types: ['heading', 'paragraph', 'div'] }),
Image.configure({ inline: false }),
Link.configure({ openOnClick: false }),
],
parseOptions: {
preserveWhitespace: 'full',
},
editorProps: {
attributes: {
class: 'tiptap tiptap-template',
},
},
onUpdate: ({ editor }) => {
htmlContent.value = editor.getHTML()
},
})
const loadTemplate = async () => {
loading.value = true
try {
await ensureDetailData()
const { bodyContent, processedCss } = await processTemplate(templateHtmlRaw, styleCssRaw)
const finalHtml = applyTemplateVars(bodyContent || defaultHtml)
if (editor.value) {
editor.value.commands.setContent(finalHtml)
htmlContent.value = finalHtml
applyTemplateStyle(processedCss)
applyCustomBackgroundToDom()
}
} catch (error) {
console.error('加载模板失败', error)
message.error('加载模板失败')
} finally {
loading.value = false
}
}
const getHtmlWithBg = () => injectBackgroundIntoHtml(htmlContent.value || editor.value?.getHTML() || '')
const handleSave = async () => {
const htmlToPersist = getHtmlWithBg()
if (!htmlToPersist) {
message.warning('暂无内容可保存')
return
}
saving.value = true
try {
await api.saveReportDraft({
valuation_id: pageMeta.value.valuationId,
template_id: pageMeta.value.templateId,
html: htmlToPersist,
})
message.success('草稿已保存')
} catch (error) {
console.error('保存失败', error)
message.error(error?.message || '保存失败,请稍后重试')
} finally {
saving.value = false
}
}
const handleExport = async () => {
if (!pageMeta.value.valuationId) {
message.error('缺少参数,无法导出')
return
}
const htmlToPersist = getHtmlWithBg()
exporting.value = true
try {
const res = await api.exportReportTemplate({
valuation_id: pageMeta.value.valuationId,
template_id: pageMeta.value.templateId,
html: htmlToPersist,
})
const blob = res instanceof Blob ? res : new Blob([res])
const fileName = `${pageMeta.value.title || '报告'}.pdf`
saveAs(blob, fileName)
message.success('导出成功')
} catch (error) {
console.error('导出失败', error)
message.error('导出失败,请检查后端接口')
} finally {
exporting.value = false
}
}
const handleBack = () => router.back()
const applyTextAlign = (value) => editor.value?.chain().focus().setTextAlign(value).run()
const toggleBold = () => editor.value?.chain().focus().toggleBold().run()
const toggleItalic = () => editor.value?.chain().focus().toggleItalic().run()
const toggleUnderline = () => editor.value?.chain().focus().toggleUnderline().run()
const toggleBullet = () => editor.value?.chain().focus().toggleBulletList().run()
const toggleOrdered = () => editor.value?.chain().focus().toggleOrderedList().run()
const undo = () => editor.value?.chain().focus().undo().run()
const redo = () => editor.value?.chain().focus().redo().run()
const setFontSize = (size) => editor.value?.chain().focus().setMark('textStyle', { fontSize: `${size}px` }).run()
const setLineHeight = (height) => editor.value?.chain().focus().setMark('textStyle', { lineHeight: height }).run()
const setTextColor = (color) => editor.value?.chain().focus().setColor(color).run()
const setBgColor = (color) => editor.value?.chain().focus().setMark('textStyle', { backgroundColor: color }).run()
const removeFormat = () => editor.value?.chain().focus().unsetAllMarks().clearNodes().run()
const insertImage = () => {
const url = window.prompt('输入图片地址:')
if (url) editor.value?.chain().focus().setImage({ src: url }).run()
}
const setLink = () => {
const url = window.prompt('输入链接地址:')
if (url === null) return
if (url === '') {
editor.value?.chain().focus().unsetLink().run()
return
}
editor.value?.chain().focus().setLink({ href: url, target: '_blank' }).run()
}
const fontSizeOptions = [12, 14, 16, 18, 20, 24].map((v) => ({ label: `${v}px`, value: v }))
const lineHeightOptions = ['1.2', '1.5', '1.8', '2'].map((v) => ({ label: v, value: v }))
onMounted(loadTemplate)
</script>
<template>
<CommonPage title="报告在线编辑(新)" :show-footer="false">
<template #header>
<div class="editor-page__header">
<div class="title">
<h2>{{ pageMeta.title || '报告在线编辑(新)' }}</h2>
<div class="meta">
<NTag size="small" type="info">估值ID{{ pageMeta.valuationId || '未知' }}</NTag>
<NTag v-if="pageMeta.templateId" size="small" type="success">
模板ID{{ pageMeta.templateId }}
</NTag>
<NTag size="small" type="warning">新版入口</NTag>
</div>
</div>
<NSpace>
<NButton @click="handleBack">返回</NButton>
<NButton secondary @click="handleSave" :loading="saving">保存草稿</NButton>
<NButton type="primary" @click="handleExport" :loading="exporting">
导出 PDF
</NButton>
</NSpace>
</div>
</template>
<div class="editor-page">
<div class="editor-toolbar">
<NSpace wrap>
<NButton size="small" @click="toggleBold">加粗</NButton>
<NButton size="small" @click="toggleItalic">斜体</NButton>
<NButton size="small" @click="toggleUnderline">下划线</NButton>
<NButton size="small" @click="() => applyTextAlign('left')">左对齐</NButton>
<NButton size="small" @click="() => applyTextAlign('center')">居中</NButton>
<NButton size="small" @click="() => applyTextAlign('right')">右对齐</NButton>
<NButton size="small" @click="toggleBullet">无序列表</NButton>
<NButton size="small" @click="toggleOrdered">有序列表</NButton>
<NButton size="small" @click="removeFormat">清除格式</NButton>
<NButton size="small" @click="undo">撤销</NButton>
<NButton size="small" @click="redo">重做</NButton>
<NButton size="small" @click="insertImage">插入图片</NButton>
<NButton size="small" @click="setLink">链接</NButton>
</NSpace>
<NSpace wrap>
<NSelect
size="small"
style="width: 120px"
placeholder="字号"
:options="fontSizeOptions"
@update:value="setFontSize"
/>
<NSelect
size="small"
style="width: 120px"
placeholder="行距"
:options="lineHeightOptions"
@update:value="setLineHeight"
/>
<label class="color-picker">
文字色
<input type="color" @input="(e) => setTextColor(e.target.value)" />
</label>
<label class="color-picker">
背景色
<input type="color" @input="(e) => setBgColor(e.target.value)" />
</label>
</NSpace>
<div class="bg-controls">
<span class="bg-label">背景</span>
<NSelect
size="small"
style="width: 180px"
placeholder="选择模板背景"
:options="backgroundOptions"
@update:value="handleBackgroundSelect"
clearable
/>
<label class="bg-upload">
自定义
<input type="file" accept="image/*" @change="handleBackgroundUpload" />
</label>
<span
v-if="effectiveBackground"
class="bg-preview"
:style="{ backgroundImage: `url(${effectiveBackground})` }"
></span>
</div>
</div>
<NSpin :show="loading">
<div class="editor-wrapper">
<div ref="editorMountRef" class="editor-canvas">
<EditorContent :editor="editor" />
</div>
</div>
</NSpin>
</div>
</CommonPage>
</template>
<style scoped>
.editor-page__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.editor-page__header .title {
display: flex;
flex-direction: column;
gap: 6px;
}
.editor-page__header h2 {
margin: 0;
}
.meta {
display: flex;
align-items: center;
gap: 8px;
}
.editor-page {
display: flex;
flex-direction: column;
gap: 12px;
}
.editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
background: #f7f7f9;
border: 1px solid #e0e4eb;
border-radius: 10px;
padding: 10px 12px;
}
.color-picker {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #555;
}
.color-picker input {
width: 28px;
height: 28px;
padding: 0;
border: none;
background: transparent;
}
.bg-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.bg-label {
font-size: 12px;
color: #555;
}
.bg-upload input {
font-size: 12px;
}
.bg-preview {
width: 44px;
height: 30px;
border: 1px solid #e0e4eb;
border-radius: 6px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.editor-wrapper {
width: 100%;
display: flex;
justify-content: center;
padding: 12px 0 24px;
}
.editor-canvas {
width: 100%;
min-height: 400px;
padding: 12px;
background: transparent;
outline: none;
}
.editor-canvas:focus {
box-shadow: none;
}
.doc-header h1 {
margin: 0 0 10px;
font-size: 22px;
text-align: center;
}
.doc-header p {
margin: 0 0 18px;
color: #555;
text-align: center;
}
.doc-block h2 {
margin: 18px 0 8px;
font-size: 16px;
}
.doc-block p {
margin: 8px 0;
}
/* TipTap 默认内容样式轻度调整,使其接近文档排版 */
.tiptap {
min-height: 380px;
line-height: 1.8;
font-size: 14px;
color: #222;
}
.tiptap p { margin: 0 0 10px; }
.tiptap h1, .tiptap h2, .tiptap h3, .tiptap h4 { margin: 14px 0 10px; }
.tiptap ul, .tiptap ol { padding-left: 22px; margin: 10px 0; }
.tiptap img { max-width: 100%; height: auto; }
/* 让模板类名的样式能够影响编辑区域 */
.tiptap-template :deep(*) {
position: inherit;
}
</style>