添加导出功能

This commit is contained in:
qweasdzxclm 2026-01-27 18:00:43 +08:00
parent 7272342fe6
commit 98b85e8132
7 changed files with 1190 additions and 0 deletions

View File

@ -50,7 +50,10 @@
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.11.1",
"fast-xml-parser": "^4.3.2",
"file-saver": "^2.0.5",
"highlight.js": "^11.9.0",
"html-docx-js": "^0.3.1",
"html-docx-js-typescript": "^0.1.5",
"jsencrypt": "^3.3.2",
"jsoneditor": "^10.1.3",
"lodash-es": "^4.17.21",

248
pnpm-lock.yaml generated
View File

@ -83,9 +83,18 @@ importers:
fast-xml-parser:
specifier: ^4.3.2
version: 4.5.0
file-saver:
specifier: ^2.0.5
version: 2.0.5
highlight.js:
specifier: ^11.9.0
version: 11.10.0
html-docx-js:
specifier: ^0.3.1
version: 0.3.1
html-docx-js-typescript:
specifier: ^0.1.5
version: 0.1.5
jsencrypt:
specifier: ^3.3.2
version: 3.3.2
@ -2566,6 +2575,9 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browser-or-node@1.3.0:
resolution: {integrity: sha512-0F2z/VSnLbmEeBcUrSuDH5l0HxTXdQQzLjkmBR4cYfvg1zJrKSlmIZFqyFR8oX0NrwPhy3c3HQ6i3OxMbew4Tg==}
browserslist-to-esbuild@2.1.1:
resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==}
engines: {node: '>=18'}
@ -2758,6 +2770,9 @@ packages:
core-js@3.39.0:
resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
cosmiconfig-typescript-loader@5.1.0:
resolution: {integrity: sha512-7PtBB+6FdsOvZyJtlF3hEPpACq7RQX6BVGsgC7/lfVXnKMvNCu/XY3ykreqG5w/rBNdu2z8LCIKoF3kpHHdHlA==}
engines: {node: '>=v16'}
@ -3426,6 +3441,9 @@ packages:
resolution: {integrity: sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==}
engines: {node: '>=18'}
file-saver@2.0.5:
resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==}
filelist@1.0.4:
resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
@ -3671,6 +3689,12 @@ packages:
htm@3.1.1:
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
html-docx-js-typescript@0.1.5:
resolution: {integrity: sha512-GNojWFDYbpHSIgKml6/0oAom8mtHrHRTWKMyLRdeJQHO/CyeM6H39DYgzYvPp4OhBp2Ti8dxMKFq0/FkpYD4bg==}
html-docx-js@0.3.1:
resolution: {integrity: sha512-QSrMiRhxesqxYCa3f+2Z3ttIHPzSjDOL1tCOmIDIEET7HdabxXND6tAbsFMXAgRG4RADQ3wbl74ydMmjidaDPA==}
html-tags@3.3.1:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'}
@ -3703,6 +3727,9 @@ packages:
resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
engines: {node: '>= 4'}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
@ -3904,6 +3931,9 @@ packages:
resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
engines: {node: '>= 0.4'}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
@ -3996,6 +4026,12 @@ packages:
resolution: {integrity: sha512-5YRzlAQ7tuzV1nAJu3LvDlrKtBFIALHN2+a+I1MGJCt3ldRDBF/bZuvIPzae8Epot6KBXd0awRZZcuoeAsZ/mw==}
hasBin: true
jszip@2.7.0:
resolution: {integrity: sha512-JIsRKRVC3gTRo2vM4Wy9WBC3TRcfnIZU8k65Phi3izkvPH975FowRYtKGT6PxevA0XnJ/yO8b0QwV0ydVyQwfw==}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
katex@0.16.11:
resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
hasBin: true
@ -4026,6 +4062,9 @@ packages:
lezer-feel@1.4.0:
resolution: {integrity: sha512-kNxG7O38gwpuYy+C3JCRxQNTCE2qu9uTuH5dE3EGVnRhIQMe6rPDz0S8t3urLEOsMud6HI795m6zX2ujfUaqTw==}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
lilconfig@3.1.2:
resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
engines: {node: '>=14'}
@ -4075,6 +4114,33 @@ packages:
lodash: '*'
lodash-es: '*'
lodash._arraycopy@3.0.0:
resolution: {integrity: sha512-RHShTDnPKP7aWxlvXKiDT6IX2jCs6YZLCtNhOru/OX2Q/tzX295vVBK5oX1ECtN+2r86S0Ogy8ykP1sgCZAN0A==}
lodash._arrayeach@3.0.0:
resolution: {integrity: sha512-Mn7HidOVcl3mkQtbPsuKR0Fj0N6Q6DQB77CtYncZcJc0bx5qv2q4Gl6a0LC1AN+GSxpnBDNnK3CKEm9XNA4zqQ==}
lodash._basecopy@3.0.1:
resolution: {integrity: sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==}
lodash._basefor@3.0.3:
resolution: {integrity: sha512-6bc3b8grkpMgDcVJv9JYZAk/mHgcqMljzm7OsbmcE2FGUMmmLQTPHlh/dFqR8LA0GQ7z4K67JSotVKu5058v1A==}
lodash._bindcallback@3.0.1:
resolution: {integrity: sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==}
lodash._createassigner@3.1.1:
resolution: {integrity: sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==}
lodash._getnative@3.9.1:
resolution: {integrity: sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==}
lodash._isiterateecall@3.0.9:
resolution: {integrity: sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==}
lodash._root@3.0.1:
resolution: {integrity: sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
@ -4084,21 +4150,48 @@ packages:
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.escape@3.2.0:
resolution: {integrity: sha512-n1PZMXgaaDWZDSvuNZ/8XOcYO2hOKDqZel5adtR30VKQAtoWs/5AOeFA0vPV8moiPzlqe7F4cP2tzpFewQyelQ==}
lodash.foreach@4.5.0:
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isarray@3.0.4:
resolution: {integrity: sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==}
lodash.isplainobject@3.2.0:
resolution: {integrity: sha512-P4wZnho5curNqeEq/x292Pb57e1v+woR7DJ84DURelKB46lby8aDEGVobSaYtzHdQBWQrJSdxcCwjlGOvvdIyg==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.istypedarray@3.0.6:
resolution: {integrity: sha512-lGWJ6N8AA3KSv+ZZxlTdn4f6A7kMfpJboeyvbFdE7IU9YAgweODqmOgdUHOA+c6lVWeVLysdaxciFXi+foVsWw==}
lodash.kebabcase@4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
lodash.keys@3.1.2:
resolution: {integrity: sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==}
lodash.keysin@3.0.8:
resolution: {integrity: sha512-YDB/5xkL3fBKFMDaC+cfGV00pbiJ6XoJIfRmBhv7aR6wWtbCW6IzkiWnTfkiHTF6ALD7ff83dAtB3OEaSoyQPg==}
lodash.merge@3.3.2:
resolution: {integrity: sha512-ZgGZpRhWLjivGUbjtApZR4HyLv/UAyoYqESVYkK4aLBJVHRrbFpG+GNnE9JPijliME4LkKM0SFI/WyOiBiv1+w==}
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.mergewith@4.6.2:
resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
lodash.restparam@3.6.1:
resolution: {integrity: sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==}
lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
@ -4111,6 +4204,9 @@ packages:
lodash.toarray@4.4.0:
resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
lodash.toplainobject@3.0.0:
resolution: {integrity: sha512-wMI0Ju1bvSmnBS3EcRRH/3zDnZOPpDtMtNDzbbNMKuTrEpALsf+sPyMeogmv63Y11qZQO7H1xFzohIEGRMjPYA==}
lodash.truncate@4.4.2:
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
@ -4459,6 +4555,9 @@ packages:
package-manager-detector@0.2.5:
resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@ -4655,6 +4754,9 @@ packages:
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
engines: {node: '>=6'}
process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
@ -4701,6 +4803,9 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@ -4825,6 +4930,9 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
safe-json-parse@4.0.0:
resolution: {integrity: sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==}
@ -4880,6 +4988,9 @@ packages:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -5010,6 +5121,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
engines: {node: '>=0.10.0'}
@ -5168,6 +5282,9 @@ packages:
tsconfig-paths@3.15.0:
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
@ -8286,6 +8403,8 @@ snapshots:
dependencies:
fill-range: 7.1.1
browser-or-node@1.3.0: {}
browserslist-to-esbuild@2.1.1(browserslist@4.24.2):
dependencies:
browserslist: 4.24.2
@ -8486,6 +8605,8 @@ snapshots:
core-js@3.39.0: {}
core-util-is@1.0.3: {}
cosmiconfig-typescript-loader@5.1.0(@types/node@20.17.9)(cosmiconfig@9.0.0(typescript@5.3.3))(typescript@5.3.3):
dependencies:
'@types/node': 20.17.9
@ -9339,6 +9460,8 @@ snapshots:
dependencies:
flat-cache: 5.0.0
file-saver@2.0.5: {}
filelist@1.0.4:
dependencies:
minimatch: 5.1.6
@ -9598,6 +9721,18 @@ snapshots:
htm@3.1.1: {}
html-docx-js-typescript@0.1.5:
dependencies:
browser-or-node: 1.3.0
jszip: 3.10.1
tslib: 1.14.1
html-docx-js@0.3.1:
dependencies:
jszip: 2.7.0
lodash.escape: 3.2.0
lodash.merge: 3.3.2
html-tags@3.3.1: {}
html-void-elements@3.0.0: {}
@ -9625,6 +9760,8 @@ snapshots:
ignore@6.0.2: {}
immediate@3.0.6: {}
immer@9.0.21: {}
immutable@5.0.3: {}
@ -9807,6 +9944,8 @@ snapshots:
call-bound: 1.0.4
get-intrinsic: 1.3.0
isarray@1.0.0: {}
isarray@2.0.5: {}
isexe@2.0.0: {}
@ -9890,6 +10029,17 @@ snapshots:
jsonrepair@3.13.0: {}
jszip@2.7.0:
dependencies:
pako: 1.0.11
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
katex@0.16.11:
dependencies:
commander: 8.3.0
@ -9926,6 +10076,10 @@ snapshots:
'@lezer/lr': 1.4.2
min-dash: 4.2.2
lie@3.3.0:
dependencies:
immediate: 3.0.6
lilconfig@3.1.2: {}
lines-and-columns@1.2.4: {}
@ -9985,22 +10139,87 @@ snapshots:
lodash: 4.17.21
lodash-es: 4.17.21
lodash._arraycopy@3.0.0: {}
lodash._arrayeach@3.0.0: {}
lodash._basecopy@3.0.1: {}
lodash._basefor@3.0.3: {}
lodash._bindcallback@3.0.1: {}
lodash._createassigner@3.1.1:
dependencies:
lodash._bindcallback: 3.0.1
lodash._isiterateecall: 3.0.9
lodash.restparam: 3.6.1
lodash._getnative@3.9.1: {}
lodash._isiterateecall@3.0.9: {}
lodash._root@3.0.1: {}
lodash.camelcase@4.3.0: {}
lodash.clonedeep@4.5.0: {}
lodash.debounce@4.0.8: {}
lodash.escape@3.2.0:
dependencies:
lodash._root: 3.0.1
lodash.foreach@4.5.0: {}
lodash.isarguments@3.1.0: {}
lodash.isarray@3.0.4: {}
lodash.isplainobject@3.2.0:
dependencies:
lodash._basefor: 3.0.3
lodash.isarguments: 3.1.0
lodash.keysin: 3.0.8
lodash.isplainobject@4.0.6: {}
lodash.istypedarray@3.0.6: {}
lodash.kebabcase@4.1.1: {}
lodash.keys@3.1.2:
dependencies:
lodash._getnative: 3.9.1
lodash.isarguments: 3.1.0
lodash.isarray: 3.0.4
lodash.keysin@3.0.8:
dependencies:
lodash.isarguments: 3.1.0
lodash.isarray: 3.0.4
lodash.merge@3.3.2:
dependencies:
lodash._arraycopy: 3.0.0
lodash._arrayeach: 3.0.0
lodash._createassigner: 3.1.1
lodash._getnative: 3.9.1
lodash.isarguments: 3.1.0
lodash.isarray: 3.0.4
lodash.isplainobject: 3.2.0
lodash.istypedarray: 3.0.6
lodash.keys: 3.1.2
lodash.keysin: 3.0.8
lodash.toplainobject: 3.0.0
lodash.merge@4.6.2: {}
lodash.mergewith@4.6.2: {}
lodash.restparam@3.6.1: {}
lodash.snakecase@4.1.1: {}
lodash.startcase@4.4.0: {}
@ -10009,6 +10228,11 @@ snapshots:
lodash.toarray@4.4.0: {}
lodash.toplainobject@3.0.0:
dependencies:
lodash._basecopy: 3.0.1
lodash.keysin: 3.0.8
lodash.truncate@4.4.2: {}
lodash.uniq@4.5.0: {}
@ -10364,6 +10588,8 @@ snapshots:
package-manager-detector@0.2.5: {}
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@ -10529,6 +10755,8 @@ snapshots:
prismjs@1.29.0: {}
process-nextick-args@2.0.1: {}
process@0.11.10: {}
progress@2.0.3: {}
@ -10563,6 +10791,16 @@ snapshots:
react-is@18.3.1: {}
readable-stream@2.3.8:
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
@ -10714,6 +10952,8 @@ snapshots:
has-symbols: 1.1.0
isarray: 2.0.5
safe-buffer@5.1.2: {}
safe-json-parse@4.0.0:
dependencies:
rust-result: 1.0.0
@ -10777,6 +11017,8 @@ snapshots:
es-errors: 1.3.0
es-object-atoms: 1.1.1
setimmediate@1.0.5: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@ -10928,6 +11170,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
strip-ansi@3.0.1:
dependencies:
ansi-regex: 2.1.1
@ -11112,6 +11358,8 @@ snapshots:
minimist: 1.2.8
strip-bom: 3.0.0
tslib@1.14.1: {}
tslib@2.3.0: {}
tslib@2.8.1: {}

View File

@ -122,6 +122,14 @@
>
编辑
</el-button>
<el-button
type="primary"
link
@click.stop="handleExportReport(row)"
v-hasPermi="['prison:evaluation-report:report:query']"
>
导出
</el-button>
<el-button
type="warning"
link
@ -153,6 +161,9 @@
<!-- 新建报告对话框 -->
<CreateReportDialog ref="createDialogRef" @success="loadReports" />
<!-- 导出报告对话框 -->
<CreateReportOutput ref="exportDialogRef" />
</div>
</template>
@ -163,6 +174,7 @@ import { PrisonerApi, type PrisonerVO } from '@/api/prison/prisoner'
import { ReportApi, type ReportVO } from '@/api/prison/evaluation-report'
import ReportEditDrawer from '../report/ReportEditDrawer.vue'
import CreateReportDialog from '../report/CreateReportDialog.vue'
import CreateReportOutput from '../report/CreateReportOutput.vue'
defineOptions({ name: 'PrisonerReportManage' })
@ -188,6 +200,7 @@ const reportList = ref<ReportVO[]>([])
const selectedPrisoner = ref<PrisonerVO | null>(null)
const editDrawerRef = ref()
const exportDialogRef = ref()
const createDialogRef = ref()
/** 获取服刑人员列表 */
@ -256,6 +269,11 @@ const handleEditReport = (row: ReportVO) => {
editDrawerRef.value?.open(row.id!, selectedPrisoner.value!.id)
}
/** 导出报告 */
const handleExportReport = (row: ReportVO) => {
exportDialogRef.value?.open(row.id!, selectedPrisoner.value!.id)
}
/** 提交报告审核 */
const handleSubmitReport = async (row: ReportVO) => {
try {

View File

@ -0,0 +1,297 @@
<template>
<Dialog :title="'评估报告'" v-model="dialogVisible" width="900px">
<div v-loading="loading" class="report-edit-container" ref="previewRef">
<template v-if="selectedReport">
<!-- 基本信息区 -->
<div class="basic-info-section">
<span class="basic-info-item">服刑人员{{ selectedReport.prisonerName }} ({{ selectedReport.prisonerNo }})</span>
<span class="basic-info-item">监区{{ selectedReport.areaName || '-' }}</span>
<span class="basic-info-item">评估日期{{ formatDateTime(selectedReport.evaluationDate, 'YYYY-MM-DD') }}</span>
<span class="basic-info-item">模板{{ selectedReport.templateName }}</span>
<span class="basic-info-item">风险等级{{ getDictLabel(DICT_TYPE.PRISON_RISK_LEVEL, selectedReport.riskLevel) }}</span>
<span class="basic-info-item">状态{{ getDictLabel(DICT_TYPE.PRISON_REPORT_STATUS, selectedReport.status) }}</span>
</div>
<!-- 维度分析区 - 新版维度分析面板 -->
<div class="dimension-section" v-if="dimensions.length > 0 && selectedReport">
维度分析
</div>
<div v-for="item in dimensionAnalysisPanelRef" :key="item.id" class="dimension-item">
<div class="dimension-item-title">{{ item.name }}</div>
<div style="white-space: pre-line; line-height: 1.5;">{{ item.aiAnalysis?.replace(/##/g, '') }}</div>
</div>
</template>
<template v-else-if="!loading">
<el-empty description="报告不存在或已被删除" />
</template>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { ReportApi, ReportVO, DimensionDataApi, DimensionDataVO, DimensionApi, DimensionVO } from '@/api/prison/evaluation-report'
import { PrisonerApi } from '@/api/prison/prisoner'
import { asBlob } from 'html-docx-js-typescript'
import { saveAs } from 'file-saver'
defineOptions({ name: 'CreateReportOutput' })
const { t } = useI18n()
const message = useMessage()
const router = useRouter()
//
const dialogVisible = ref(false)
const drawerTitle = ref('编辑评估报告')
const loading = ref(false)
const saving = ref(false)
const aiGenerating = ref(false)
const aiGeneratingDimension = ref<number | undefined>(undefined)
//
const reportId = ref<number>()
const selectedReport = ref<ReportVO | null>(null)
const dimensionDataList = ref<DimensionDataVO[]>([])
const dimensions = ref<any[]>([])
const dimensionAnalysisPanelRef = ref<DimensionDataVO[]>([])
const previewRef = ref<HTMLElement | null>(null)
/** 打开抽屉 */
const open = async (id: number, prisonerId?: number) => {
reportId.value = id
dialogVisible.value = true
await loadReportDetail(id)
try {
exportToWord()
} catch {
} finally {
handleClose()
}
}
/** 加载报告详情 */
const loadReportDetail = async (id: number) => {
loading.value = true
try {
selectedReport.value = await ReportApi.getReport(id)
dimensionDataList.value = await DimensionDataApi.getDimensionDataListByReportId(id)
drawerTitle.value = selectedReport.value?.title || `${selectedReport.value?.prisonerName} - 评估报告`
//
if (selectedReport.value?.prisonerId && !selectedReport.value.areaName) {
const prisoner = await PrisonerApi.get(selectedReport.value.prisonerId)
if (prisoner?.prisonAreaName) {
selectedReport.value.areaName = prisoner.prisonAreaName
selectedReport.value.areaId = prisoner.prisonAreaId
}
}
//
if (selectedReport.value?.templateId) {
try {
const dimensionList = await DimensionApi.getDimensionsByTemplateId(selectedReport.value.templateId)
if (dimensionList && dimensionList.length > 0) {
console.log(dimensionList);
dimensions.value = dimensionList
} else {
// 使
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
}
} catch {
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
}
}
if (selectedReport.value?.id && dimensions.value.length > 0) {
const list = await DimensionDataApi.getDimensionDataListByReportId(selectedReport.value.id)
console.log(list, dimensions.value);
dimensionAnalysisPanelRef.value = dimensions.value.map(item => {
return {
...item,
aiAnalysis: list.find(analys => analys.dimensionId === item.id)?.aiAnalysis
}
})
}
} catch (error) {
message.error(error?.msg || '加载报告失败')
selectedReport.value = null
} finally {
loading.value = false
}
}
/** 获取默认维度配置 */
const getDefaultDimensions = (templateId: number): DimensionVO[] => {
return [
{ id: 1, templateId, name: '基本信息', dimensionType: 1, aiEnabled: 0, status: 0, dataSources: ['prisoner'] },
{ id: 2, templateId, name: '犯罪情况分析', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['prisoner', 'risk'] },
{ id: 3, templateId, name: '服刑表现评估', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['score', 'violation', 'reward'] },
{ id: 4, templateId, name: '消费行为分析', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['consumption'] },
{ id: 5, templateId, name: '综合评估结论', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['prisoner', 'psychology'] }
]
}
/** 关闭 */
const handleClose = () => {
dialogVisible.value = false
selectedReport.value = null
dimensionDataList.value = []
}
/** 风险等级变化 */
const handleRiskLevelChange = () => {
// selectedReport
}
/** 导出为Word文档 - 直接使用预览容器的HTML */
const exportToWord = async () => {
try {
loading.value = true
if (!previewRef.value) {
message.error('预览内容未加载完成')
return
}
// HTML
let previewHTML = previewRef.value.innerHTML
// \n <br> 使 Word
// 使 DOM HTML
const tempDiv = document.createElement('div')
tempDiv.innerHTML = previewHTML
const processTextNodes = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
// \n <br>
const text = node.textContent || ''
if (text.includes('\n')) {
const span = document.createElement('span')
span.innerHTML = text.replace(/\n/g, '<br>')
node.parentNode?.replaceChild(span, node)
}
} else if (node.nodeType === Node.ELEMENT_NODE) {
//
Array.from(node.childNodes).forEach(processTextNodes)
}
}
processTextNodes(tempDiv)
previewHTML = tempDiv.innerHTML
// HTML使
const fullHTML = `
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word'>
<head>
<meta charset="utf-8">
<style>
body {
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
margin: 20px;
padding: 20px;
}
.basic-info-section {
padding: 15px 20px;
color: black;
font-size: 14px;
}
.basic-info-item{
margin-right: 30px;
}
.dimension-section {
flex: 1;
padding: 15px 20px 0;
font-size: 16px;
font-weight: 500;
color: black;
}
.dimension-item {
padding: 0 40px;
font-size: 14px;
color: black;
}
.dimension-item-title {
font-size: 15px;
padding: 15px 0;
font-weight: 500;
color: black;
}
</style>
</head>
<body>
${previewHTML}
</body>
</html>
`
// Blob (asBlobPromise)
const converted = await asBlob(fullHTML)
//
const fileName = `${'评估报告'}_${new Date().toLocaleDateString('zh-CN')}.docx`
saveAs(converted as Blob, fileName)
message.success('Word文档导出成功')
} catch (error) {
console.error('导出Word失败:', error)
message.error('导出Word失败请重试')
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>
<style lang="scss" scoped>
.report-edit-container {
height: 100%;
overflow-y: auto;
background-color: #f5f7fa;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.basic-info-section {
padding: 15px 20px;
color: black;
font-size: 14px;
}
.basic-info-item{
margin-right: 30px;
}
.dimension-section {
flex: 1;
padding: 15px 20px 0;
font-size: 16px;
font-weight: 500;
color: black;
}
.dimension-item {
padding: 0 40px;
font-size: 14px;
color: black;
}
.dimension-item-title {
font-size: 15px;
padding: 15px 0;
font-weight: 500;
color: black;
}
</style>

View File

@ -282,6 +282,15 @@
>
查看答案
</el-button>
<el-button
v-if="scope.row.status === 3"
link
type="primary"
size="small"
@click="handleViewOutput(scope.row)"
>
导出
</el-button>
<el-button
v-if="scope.row.status === 1"
link
@ -328,6 +337,9 @@
<!-- 代填弹窗 -->
<AgentFillDialog ref="agentFillDialogRef" @success="loadPrisonerProgress" />
<!-- 问卷导出弹窗 -->
<QuestionnaireOutput ref="outputDialogRef" />
</template>
<script setup lang="ts">
@ -337,6 +349,7 @@ import { formatDateTime } from '@/utils/formatTime'
import AnswerDetailDialog from '@/views/prison/questionnairerecord/AnswerDetailDialog.vue'
import AgentFillDialog from './AgentFillDialog.vue'
import { QuestionnaireTaskApi } from '@/api/prison/questionnaire-task'
import QuestionnaireOutput from '@/views/prison/questionnairerecord/QuestionnaireOutputfile.vue'
defineOptions({ name: 'TaskDetailDialog' })
@ -527,6 +540,19 @@ const handleViewAnswer = (row: any) => {
answerDetailDialogRef.value?.open(row.id)
}
/** 导出答题详情 */
const outputDialogRef = ref()
const handleViewOutput = async (row: any) => {
if (!row.id) {
ElMessage.warning('该人员暂无答题记录')
return
}
try {
await outputDialogRef.value.open(row.id)
} finally {
}
}
/** 通知人员 */
const handleNotifyPrisoner = async (row: any) => {
try {

View File

@ -0,0 +1,574 @@
<template>
<Dialog style="display: none;" title="问卷预览" v-model="dialogVisible" width="900px" :fullscreen="false">
<div ref="previewRef" class="questionnaire-preview" v-loading="loading">
<!-- 问卷头部信息 -->
<h1 class="preview-header">{{ recordInfo?.questionnaireName }}</h1>
<!-- 问卷说明 -->
<div v-if="recordInfo?.description" class="preview-description">
<div class="section-title">问卷说明</div>
<div class="description-content" v-html="recordInfo.description"></div>
</div>
<!-- 填写说明 -->
<div v-if="recordInfo?.instruction" class="preview-instruction">
<div class="section-title">填写说明</div>
<div class="instruction-content">{{ recordInfo.instruction }}</div>
</div>
<!-- 问题列表按分区显示 -->
<div class="preview-questions">
<template v-for="partition in partitions" :key="partition.name || 'default'">
<!-- 分区标题 -->
<div v-if="partition.name" class="partition-title">
{{ partition.name }}
<span class="question-count">({{ partition.questions.length }} 道题)</span>
</div>
<!-- 问题列表 -->
<div class="question-items">
<div
v-for="(questionWithAnswer, index) in partition.questions"
:key="questionWithAnswer.question.id"
class="question-item"
>
<span class="question-index">{{ index + 1 }}.</span>
<span class="question-title">{{ questionWithAnswer.question.title }}</span>
<!-- 帮助说明 -->
<span v-if="questionWithAnswer.question.helpText" class="question-help-inline">
{{ questionWithAnswer.question.helpText }}
</span>
<!-- 单选/多选题 -->
<span v-if="questionWithAnswer.question.type === 1 || questionWithAnswer.question.type === 2" class="question-options-inline">
<span
v-for="option in getQuestionOptions(questionWithAnswer.question)"
:key="option.label"
class="option-item"
>
<span v-if="questionWithAnswer.question.type === 1 && questionWithAnswer.answer?.answerText?.trim() === option.label">
{{ option.label }}
</span>
<span v-if="questionWithAnswer.question.type === 1 && questionWithAnswer.answer?.answerText?.trim() !== option.label">
{{ option.label }}
</span>
<span v-if="questionWithAnswer.question.type === 2 && getSelectedLabels(questionWithAnswer.answer).includes(option.label)">
{{ option.label }}
</span>
<span v-if="questionWithAnswer.question.type === 2 && !getSelectedLabels(questionWithAnswer.answer).includes(option.label)">
{{ option.label }}
</span>
</span>
</span>
<!-- 填空题 -->
<span v-else-if="questionWithAnswer.question.type === 3" class="question-input-inline">
{{ getAnswerDisplayValue(questionWithAnswer.answer) }}
</span>
<!-- 评分题 -->
<span v-else-if="questionWithAnswer.question.type === 4" class="question-rating-inline">
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
</span>
<!-- 日期题 -->
<span v-else-if="questionWithAnswer.question.type === 5" class="question-date-inline">
<span class="date-info" v-if="getRangeValue(questionWithAnswer.question, 'min') || getRangeValue(questionWithAnswer.question, 'max')">
日期范围{{ getRangeValue(questionWithAnswer.question, 'min') || '无限制' }} ~ {{ getRangeValue(questionWithAnswer.question, 'max') || '无限制' }}
</span>
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
</span>
<!-- 数字题 -->
<span v-else-if="questionWithAnswer.question.type === 6" class="question-number-inline">
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
</span>
</div>
</div>
</template>
</div>
<!-- 空状态 -->
<el-empty v-if="partitions.length === 0 && !loading" description="暂无问题" />
</div>
<template #footer>
<el-button type="primary" @click="exportToWord" :loading="loading">导出Word</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { QuestionnaireRecordApi, type QuestionnaireRecord } from '@/api/prison/questionnairerecord'
import { AnswerApi, type Answer } from '@/api/prison/answer'
import { QuestionApi, Question } from '@/api/prison/question'
import { asBlob } from 'html-docx-js-typescript'
import { saveAs } from 'file-saver'
defineOptions({ name: 'QuestionnaireOutput' })
const message = useMessage()
const dialogVisible = ref(false)
const loading = ref(false)
const previewRef = ref<HTMLElement | null>(null)
const recordInfo = ref<QuestionnaireRecord | null>(null)
const answers = ref<Answer[]>([])
const questions = ref<Question[]>([])
const questionnaireInfo = ref<any>(null) // (descriptioninstruction)
//
interface QuestionWithAnswer {
question: Question
answer?: Answer
index: number
}
const partitions = computed(() => {
const partMap = new Map<string, QuestionWithAnswer[]>()
questions.value.forEach((q, index) => {
const partName = q.partName || ''
const answer = answers.value.find(a => a.questionId === q.id)
if (!partMap.has(partName)) {
partMap.set(partName, [])
}
partMap.get(partName)!.push({
question: q,
answer,
index: index + 1
})
})
//
const sortedParts = Array.from(partMap.entries())
.sort((a, b) => {
const sortA = a[1][0]?.question.partSort ?? 0
const sortB = b[1][0]?.question.partSort ?? 0
return sortA - sortB
})
//
const result: Array<{ name: string; questions: QuestionWithAnswer[] }> = []
//
const defaultQuestions = sortedParts.find(([name]) => !name)
if (defaultQuestions) {
result.push({
name: '',
questions: defaultQuestions[1]
})
}
//
sortedParts
.filter(([name]) => name)
.forEach(([name, qs]) => {
result.push({
name,
questions: qs
})
})
return result
})
/** 根据问题ID获取答案 */
const getAnswerByQuestionId = (questionId: number): Answer | undefined => {
return answers.value.find(a => a.questionId === questionId)
}
/** 获取答案显示值(兼容不同字段名) */
const getAnswerDisplayValue = (answer?: Answer): string => {
if (!answer) return ''
// 使 answerText使 optionIds
return answer.answerText || answer.optionIds || '-'
}
/** 问卷类型标签 */
const getTypeLabel = (type: number) => {
const options = getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE)
return options.find(o => o.value === type)?.label || '未知'
}
/** 获取问题选项 */
const getQuestionOptions = (question: Question) => {
if (!question.options) return []
try {
const parsed = JSON.parse(question.options)
// value
if (Array.isArray(parsed)) {
return parsed.map((opt, index) => ({
label: opt.label ?? opt.text ?? opt.name ?? String(opt),
value: String(index), // 使
score: opt.score ?? 0
}))
}
return []
} catch {
return []
}
}
/** 获取范围值(用于日期、数字、评分题) */
const getRangeValue = (question: Question, key: 'min' | 'max') => {
if (!question.options) return undefined
try {
const obj = JSON.parse(question.options)
return obj[key] || undefined
} catch {
return undefined
}
}
/** 安全获取多选答案的标签数组 */
const getSelectedLabels = (answer?: Answer): string[] => {
if (!answer?.answerText) return []
return answer.answerText.split(',').map(s => s.trim())
}
/** 判断选项是否被选中(支持单选和多选)- 已废弃,改用模板直接绑定 */
const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: string, questionType?: number): boolean => {
if (!answer || !optionValue || !optionLabel) {
return false
}
// type === 2answerText
if (questionType === 2 && answer.answerText && answer.answerText.includes(',')) {
const answerText = answer.answerText
const selectedLabels = answerText.split(',').map(s => s.trim())
return selectedLabels.includes(optionLabel)
}
// type === 1answerText
if (questionType === 1 && answer.answerText) {
return answer.answerText.trim() === optionLabel.trim()
}
// questionType
if (answer.answerText && answer.answerText.includes(',')) {
const answerText = answer.answerText
const selectedLabels = answerText.split(',').map(s => s.trim())
return selectedLabels.includes(optionLabel)
}
if (answer.answerText?.trim() === optionLabel?.trim()) return true
return false
}
/** 打开弹窗 */
const open = async (recordId: number) => {
dialogVisible.value = true
loading.value = true
try {
//
const [recordData, answerList] = await Promise.all([
QuestionnaireRecordApi.getQuestionnaireRecord(recordId),
AnswerApi.getAnswersByAssessmentRecordId(recordId)
])
recordInfo.value = recordData
answers.value = answerList
//
if (recordData.questionnaireId) {
const questionsData = await QuestionApi.getQuestionPage({
pageNo: 1,
pageSize: 200,
questionnaireId: recordData.questionnaireId
})
questions.value = questionsData.list
}
// (Worddescriptioninstruction)
if (recordData.questionnaireId) {
try {
const { QuestionnaireApi } = await import('@/api/prison/questionnaire')
questionnaireInfo.value = await QuestionnaireApi.getQuestionnaire(recordData.questionnaireId)
} catch (error) {
console.warn('加载问卷详细信息失败:', error)
}
}
} catch (error) {
console.error('加载答题详情失败:', error)
} finally {
loading.value = false
exportToWord()
dialogVisible.value = false
}
}
/** 跳转到填写页面 */
const openFillPage = () => {
// TODO:
message.info('填写页面功能待实现')
}
/** 导出为Word文档 - 直接使用预览容器的HTML */
const exportToWord = async () => {
try {
loading.value = true
if (!previewRef.value) {
message.error('预览内容未加载完成')
return
}
// HTML
const previewHTML = previewRef.value.innerHTML
// HTML使
const fullHTML = `
<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word'>
<head>
<meta charset="utf-8">
<title>${recordInfo.value?.questionnaireName || '问卷'}</title>
<style>
body {
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
margin: 20px;
padding: 20px;
}
h1 {
font-weight: 500;
color: #000;
text-align: center;
}
.section-title {
margin: 15px 0;
font-size: 16px;
font-weight: 500;
color: #000;
}
.description-content {
color: #000;
line-height: 1.8;
font-size: 14px;
}
.instruction-content {
color: #000;
font-size: 16px;
font-weight: 500;
line-height: 1.8;
}
.partition-title {
margin-bottom: 16px;
margin-top: 24px;
color: #000;
font-size: 15px;
font-weight: 500;
}
.partition-title:first-child {
margin-top: 0;
}
.question-count {
font-size: 12px;
color: #909399;
font-weight: normal;
margin-left: 4px;
}
.question-item {
font-size: 14px;
color: #000;
line-height: 1.6;
margin-bottom: 16px;
page-break-inside: avoid;
}
.question-index {
margin-right: 4px;
font-weight: bold;
}
.question-title {
margin-right: 8px;
}
.question-help-inline {
display: inline-flex;
align-items: center;
gap: 6px;
color: #909399;
font-size: 13px;
padding: 4px 8px;
background: #f4f4f5;
border-radius: 4px;
margin-right: 8px;
}
.question-options-inline {
display: inline-flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.option-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
.question-rating-inline,
.question-date-inline,
.question-number-inline {
display: inline-flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-left: 8px;
}
.rating-info,
.date-info,
.number-info {
display: inline-flex;
gap: 16px;
color: #909399;
font-size: 13px;
}
.question-input-inline {
color: #000;
font-size: 14px;
line-height: 2.2;
text-decoration: underline;
}
</style>
</head>
<body>
${previewHTML}
</body>
</html>
`
// Blob (asBlobPromise)
const converted = await asBlob(fullHTML)
//
const fileName = `${recordInfo.value?.questionnaireName || '问卷'}_${new Date().toLocaleDateString('zh-CN')}.docx`
saveAs(converted as Blob, fileName)
message.success('Word文档导出成功')
} catch (error) {
console.error('导出Word失败:', error)
message.error('导出Word失败请重试')
} finally {
loading.value = false
}
}
defineExpose({ open, exportToWord })
</script>
<style scoped lang="scss">
.questionnaire-preview {
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
h1 {
font-weight: 500;
color: #000;
text-align: center;
}
.section-title {
margin: 15px 0;
font-size: 16px;
font-weight: 500;
color: #000;
}
.description-content {
color: #000;
line-height: 1.8;
font-size: 14px;
}
.instruction-content {
color: #000;
font-size: 16px;
font-weight: 500;
line-height: 1.8;
}
.partition-title {
margin-bottom: 16px;
margin-top: 24px;
color: #000;
font-size: 15px;
font-weight: 500;
}
.partition-title:first-child {
margin-top: 0;
}
.question-count {
font-size: 12px;
color: #909399;
font-weight: normal;
margin-left: 4px;
}
.question-item {
font-size: 14px;
color: #000;
line-height: 1.6;
margin-bottom: 16px;
page-break-inside: avoid;
}
.question-index {
margin-right: 4px;
font-weight: bold;
}
.question-title {
margin-right: 8px;
}
.question-help-inline {
display: inline-flex;
align-items: center;
gap: 6px;
color: #909399;
font-size: 13px;
padding: 4px 8px;
background: #f4f4f5;
border-radius: 4px;
margin-right: 8px;
}
.question-options-inline {
display: inline-flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.option-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
.question-rating-inline,
.question-date-inline,
.question-number-inline {
display: inline-flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-left: 8px;
}
.rating-info,
.date-info,
.number-info {
display: inline-flex;
gap: 16px;
color: #909399;
font-size: 13px;
}
.question-input-inline {
color: #000;
font-size: 14px;
line-height: 2.2;
text-decoration: underline;
}
</style>

View File

@ -170,6 +170,15 @@
>
查看详情
</el-button>
<el-button
v-if="scope.row.status === 3"
link
type="success"
@click="handleViewOutput(scope.row.id)"
v-hasPermi="['prison:questionnaire-record:query']"
>
导出
</el-button>
<el-button
v-if="scope.row.status === 1"
link
@ -245,6 +254,9 @@
<!-- 答题详情弹窗 -->
<AnswerDetailDialog ref="answerDetailDialogRef" />
<!-- 问卷导出弹窗 -->
<QuestionnaireOutput ref="outputDialogRef" />
</template>
<script setup lang="ts">
@ -258,6 +270,7 @@ import QuestionnaireRecordForm from './QuestionnaireRecordForm.vue'
import InitiateAssessmentDialog from './InitiateAssessmentDialog.vue'
import ManualScoreDialog from './ManualScoreDialog.vue'
import AnswerDetailDialog from './AnswerDetailDialog.vue'
import QuestionnaireOutput from './QuestionnaireOutputfile.vue'
/** 问卷答题记录/测评记录 列表 */
defineOptions({ name: 'QuestionnaireRecord' })
@ -367,6 +380,17 @@ const handleViewDetail = (id: number) => {
answerDetailDialogRef.value.open(id)
}
/** 导出答题详情 */
const outputDialogRef = ref()
const handleViewOutput = async (id: number) => {
try {
exportLoading.value = true
await outputDialogRef.value.open(id)
} finally {
exportLoading.value = false
}
}
/** 人工评分 */
const manualScoreDialogRef = ref()
const handleManualScore = (row: QuestionnaireRecord) => {