Compare commits
No commits in common. "master" and "lm/fix/databoard" have entirely different histories.
master
...
lm/fix/dat
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,4 +7,3 @@ pnpm-debug
|
||||
auto-*.d.ts
|
||||
.idea
|
||||
.history
|
||||
.omc/
|
||||
|
||||
@ -34,8 +34,6 @@ const include = [
|
||||
'markmap-toolbar',
|
||||
'highlight.js',
|
||||
'element-plus',
|
||||
'html-docx-js-typescript',
|
||||
'file-saver',
|
||||
'element-plus/es',
|
||||
'element-plus/es/locale/lang/zh-cn',
|
||||
'element-plus/es/locale/lang/en',
|
||||
|
||||
@ -51,7 +51,6 @@
|
||||
"element-plus": "2.11.1",
|
||||
"fast-xml-parser": "^4.3.2",
|
||||
"highlight.js": "^11.9.0",
|
||||
"html-docx-js": "^0.3.1",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jsoneditor": "^10.1.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
@ -113,8 +112,6 @@
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.22.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"html-docx-js-typescript": "^0.1.5",
|
||||
"lint-staged": "^15.2.2",
|
||||
"postcss": "^8.4.35",
|
||||
"postcss-html": "^1.6.0",
|
||||
|
||||
248
pnpm-lock.yaml
generated
248
pnpm-lock.yaml
generated
@ -86,9 +86,6 @@ importers:
|
||||
highlight.js:
|
||||
specifier: ^11.9.0
|
||||
version: 11.10.0
|
||||
html-docx-js:
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1
|
||||
jsencrypt:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
@ -267,12 +264,6 @@ importers:
|
||||
eslint-plugin-vue:
|
||||
specifier: ^9.22.0
|
||||
version: 9.31.0(eslint@8.57.1)
|
||||
file-saver:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
html-docx-js-typescript:
|
||||
specifier: ^0.1.5
|
||||
version: 0.1.5
|
||||
lint-staged:
|
||||
specifier: ^15.2.2
|
||||
version: 15.2.10
|
||||
@ -2575,9 +2566,6 @@ 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'}
|
||||
@ -2770,9 +2758,6 @@ 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'}
|
||||
@ -3441,9 +3426,6 @@ 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==}
|
||||
|
||||
@ -3689,12 +3671,6 @@ 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'}
|
||||
@ -3727,9 +3703,6 @@ 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==}
|
||||
|
||||
@ -3931,9 +3904,6 @@ 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==}
|
||||
|
||||
@ -4026,12 +3996,6 @@ 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
|
||||
@ -4062,9 +4026,6 @@ 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'}
|
||||
@ -4114,33 +4075,6 @@ 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==}
|
||||
|
||||
@ -4150,48 +4084,21 @@ 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==}
|
||||
|
||||
@ -4204,9 +4111,6 @@ 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==}
|
||||
|
||||
@ -4555,9 +4459,6 @@ 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'}
|
||||
@ -4754,9 +4655,6 @@ 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'}
|
||||
@ -4803,9 +4701,6 @@ 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'}
|
||||
@ -4930,9 +4825,6 @@ 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==}
|
||||
|
||||
@ -4988,9 +4880,6 @@ 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'}
|
||||
@ -5121,9 +5010,6 @@ 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'}
|
||||
@ -5282,9 +5168,6 @@ 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==}
|
||||
|
||||
@ -8403,8 +8286,6 @@ 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
|
||||
@ -8605,8 +8486,6 @@ 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
|
||||
@ -9460,8 +9339,6 @@ snapshots:
|
||||
dependencies:
|
||||
flat-cache: 5.0.0
|
||||
|
||||
file-saver@2.0.5: {}
|
||||
|
||||
filelist@1.0.4:
|
||||
dependencies:
|
||||
minimatch: 5.1.6
|
||||
@ -9721,18 +9598,6 @@ 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: {}
|
||||
@ -9760,8 +9625,6 @@ snapshots:
|
||||
|
||||
ignore@6.0.2: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immer@9.0.21: {}
|
||||
|
||||
immutable@5.0.3: {}
|
||||
@ -9944,8 +9807,6 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isarray@2.0.5: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
@ -10029,17 +9890,6 @@ 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
|
||||
@ -10076,10 +9926,6 @@ 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: {}
|
||||
@ -10139,87 +9985,22 @@ 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: {}
|
||||
@ -10228,11 +10009,6 @@ 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: {}
|
||||
@ -10588,8 +10364,6 @@ snapshots:
|
||||
|
||||
package-manager-detector@0.2.5: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@ -10755,8 +10529,6 @@ snapshots:
|
||||
|
||||
prismjs@1.29.0: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
progress@2.0.3: {}
|
||||
@ -10791,16 +10563,6 @@ 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
|
||||
@ -10952,8 +10714,6 @@ 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
|
||||
@ -11017,8 +10777,6 @@ 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
|
||||
@ -11170,10 +10928,6 @@ 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
|
||||
@ -11358,8 +11112,6 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-bom: 3.0.0
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
@ -108,7 +108,7 @@ export const AreaApi = {
|
||||
|
||||
/** 批量删除监区信息 */
|
||||
deleteAreaList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/area/delete-list`, data: ids })
|
||||
return await request.delete({ url: `/prison/area/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
|
||||
// 导出监区信息 Excel
|
||||
|
||||
@ -74,7 +74,7 @@ export const CellApi = {
|
||||
|
||||
// 批量删除监室信息
|
||||
deleteCellList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/cell/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/cell/delete-list', params: { ids: ids.join(',') } })
|
||||
},
|
||||
|
||||
// 导出监室信息 Excel
|
||||
|
||||
@ -79,7 +79,7 @@ export const ConsumptionApi = {
|
||||
|
||||
/** 批量删除消费订单 */
|
||||
deleteConsumptionList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/consumption/delete-list`, data: ids })
|
||||
return await request.delete({ url: `/prison/consumption/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
|
||||
// 查询消费明细列表
|
||||
|
||||
@ -231,13 +231,6 @@ export interface PrisonerDashboardStatsRespVO {
|
||||
riskScore: number // 风险评估分
|
||||
riskLevel: number // 风险等级
|
||||
|
||||
// 累计数据
|
||||
violationCount: number // 累计违规次数
|
||||
praiseDays: string // 累计表扬天数
|
||||
praiseCount: number // 累计表扬次数
|
||||
penaltyCount: number // 累计扣分次数
|
||||
rewardCount: number // 累计加分次数
|
||||
|
||||
// 中心数据
|
||||
centerLeftData: CenterLeftData
|
||||
centerRightData: CenterRightData
|
||||
|
||||
@ -182,7 +182,7 @@ export const TemplateApi = {
|
||||
},
|
||||
|
||||
deleteTemplateList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/template/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/template/delete-list', params: { ids: ids.join(',') } })
|
||||
},
|
||||
|
||||
getEnabledTemplateList: async () => {
|
||||
@ -220,7 +220,7 @@ export const DimensionApi = {
|
||||
},
|
||||
|
||||
deleteDimensionList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/dimension/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/dimension/delete-list', params: { ids: ids.join(',') } })
|
||||
},
|
||||
|
||||
getDimensionsByTemplateId: async (templateId: number) => {
|
||||
@ -262,7 +262,7 @@ export const ReportApi = {
|
||||
},
|
||||
|
||||
deleteReportList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/report/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/report/delete-list', params: { ids: ids.join(',') } })
|
||||
},
|
||||
|
||||
submitReport: async (id: number) => {
|
||||
@ -383,7 +383,7 @@ export const CommentApi = {
|
||||
},
|
||||
|
||||
deleteCommentList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/comment/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/comment/delete-list', params: { ids: ids.join(',') } })
|
||||
},
|
||||
|
||||
getCommentsByTypeAndLevel: async (commentType: number, level?: number) => {
|
||||
|
||||
@ -162,7 +162,7 @@ export const EvaluationTemplateApi = {
|
||||
|
||||
// 批量删除模板
|
||||
deleteTemplateList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/template/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/template/delete-list?ids=' + ids.join(',') })
|
||||
},
|
||||
|
||||
// 获取启用的模板列表
|
||||
@ -216,7 +216,7 @@ export const EvaluationDimensionApi = {
|
||||
|
||||
// 批量删除维度
|
||||
deleteDimensionList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/dimension/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/dimension/delete-list?ids=' + ids.join(',') })
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ export const EvaluationReportApi = {
|
||||
|
||||
// 批量删除报告
|
||||
deleteReportList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/report/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/report/delete-list?ids=' + ids.join(',') })
|
||||
},
|
||||
|
||||
// 提交审核
|
||||
@ -324,7 +324,7 @@ export const ReportCommentApi = {
|
||||
|
||||
// 批量删除评语
|
||||
deleteCommentList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/evaluation-report/comment/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/evaluation-report/comment/delete-list?ids=' + ids.join(',') })
|
||||
},
|
||||
|
||||
// 获取评语详情
|
||||
|
||||
@ -89,18 +89,10 @@ export const PrisonerApi = {
|
||||
getPage: (params: PageParam) => {
|
||||
return request.get({ url: '/prison/prisoner/page', params })
|
||||
},
|
||||
// 分页查询(别名,用于选择犯人弹窗)
|
||||
getPrisonerPage: (params: PageParam) => {
|
||||
return request.get({ url: '/prison/prisoner/page', params })
|
||||
},
|
||||
// 获取详情
|
||||
get: (id: number) => {
|
||||
return request.get({ url: '/prison/prisoner/get', params: { id } })
|
||||
},
|
||||
// 获取详情(别名,与其他模块保持一致)
|
||||
getPrisoner: (id: number) => {
|
||||
return request.get({ url: '/prison/prisoner/get', params: { id } })
|
||||
},
|
||||
// 创建
|
||||
create: (data: PrisonerCreateVO) => {
|
||||
return request.post({ url: '/prison/prisoner/create', data })
|
||||
@ -115,7 +107,7 @@ export const PrisonerApi = {
|
||||
},
|
||||
// 批量删除
|
||||
deleteList: (ids: number[]) => {
|
||||
return request.post({ url: '/prison/prisoner/delete-list', data: ids })
|
||||
return request.delete({ url: '/prison/prisoner/delete-list', params: { ids: ids.join(',') } })
|
||||
},
|
||||
// 导出
|
||||
export: (params: PageParam) => {
|
||||
|
||||
@ -73,7 +73,7 @@ export const QuestionApi = {
|
||||
|
||||
/** 批量删除问卷问题 */
|
||||
deleteQuestionList: async (ids: number[]) => {
|
||||
return await request.post<boolean>({ url: `/prison/question/delete-list`, data: ids })
|
||||
return await request.delete<boolean>({ url: `/prison/question/delete-list`, params: { ids: ids.join(',') } })
|
||||
},
|
||||
|
||||
// 批量更新问卷问题(使用POST方法,与后端一致)
|
||||
@ -84,10 +84,5 @@ export const QuestionApi = {
|
||||
// 导出问卷问题 Excel
|
||||
exportQuestion: async (params: QuestionPageParams) => {
|
||||
return await request.download({ url: `/prison/question/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取问卷问题列表(不分页)
|
||||
getQuestionnaireQuestionList: async (params: QuestionPageParams) => {
|
||||
return await request.get({ url: `/prison/question/page`, params })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,240 +0,0 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
/** 问卷任务信息 */
|
||||
export interface QuestionnaireTask {
|
||||
id?: number
|
||||
taskName: string
|
||||
questionnaireId: number
|
||||
questionnaireName?: string
|
||||
targetType: number // 1-指定犯人 2-指定监区 3-全部犯人
|
||||
areaId?: number
|
||||
areaName?: string
|
||||
prisonerIds?: string
|
||||
startTime?: string
|
||||
deadline: string
|
||||
status: number // 1-草稿 2-进行中 3-已结束 4-已取消
|
||||
totalCount?: number
|
||||
completedCount?: number
|
||||
pendingCount?: number
|
||||
completionRate?: string | number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
/** 问卷任务分页参数 */
|
||||
export interface QuestionnaireTaskPageParams {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
taskName?: string
|
||||
questionnaireId?: number
|
||||
status?: number
|
||||
targetType?: number
|
||||
createTime?: string[]
|
||||
}
|
||||
|
||||
/** 问卷任务创建参数 */
|
||||
export interface QuestionnaireTaskCreateParams {
|
||||
taskName: string
|
||||
questionnaireId: number
|
||||
targetType: number
|
||||
prisonerIds?: number[]
|
||||
areaId?: number
|
||||
startTime?: string
|
||||
deadline: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 问卷任务更新参数 */
|
||||
export interface QuestionnaireTaskUpdateParams {
|
||||
id: number
|
||||
taskName?: string
|
||||
deadline?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 任务进度详情 */
|
||||
export interface TaskProgress {
|
||||
taskId: number
|
||||
taskName: string
|
||||
questionnaireName: string
|
||||
status: number
|
||||
startTime?: string
|
||||
deadline: string
|
||||
totalCount: number
|
||||
completedCount: number
|
||||
pendingCount: number
|
||||
completionRate: string | number
|
||||
statusBreakdown: {
|
||||
pending: number
|
||||
inProgress: number
|
||||
completed: number
|
||||
expired: number
|
||||
cancelled: number
|
||||
}
|
||||
}
|
||||
|
||||
/** 按监区统计 */
|
||||
export interface TaskAreaStatistics {
|
||||
areaId?: number
|
||||
areaName?: string
|
||||
totalCount: number
|
||||
completedCount: number
|
||||
completionRate: string | number
|
||||
avgScore: string | number
|
||||
passRate: string | number
|
||||
riskDistribution: {
|
||||
highRisk: number
|
||||
mediumRisk: number
|
||||
lowRisk: number
|
||||
}
|
||||
}
|
||||
|
||||
/** 按监区统计 */
|
||||
export interface TaskAreaStatistics {
|
||||
areaId?: number
|
||||
areaName?: string
|
||||
totalCount: number
|
||||
completedCount: number
|
||||
completionRate: string | number
|
||||
avgScore: string | number
|
||||
passRate: string | number
|
||||
riskDistribution: {
|
||||
highRisk: number
|
||||
mediumRisk: number
|
||||
lowRisk: number
|
||||
}
|
||||
}
|
||||
|
||||
/** 人员填写进度 */
|
||||
export interface PrisonerProgress {
|
||||
questionnaireId: number
|
||||
questionnaireName?: string
|
||||
id: number
|
||||
prisonerId: number
|
||||
prisonerNo: string
|
||||
prisonerName: string
|
||||
areaId?: number
|
||||
areaName?: string
|
||||
status: number
|
||||
objectiveScore?: number
|
||||
subjectiveScore?: number
|
||||
totalScore?: number
|
||||
riskLevel?: number
|
||||
duration?: number
|
||||
startTime?: string
|
||||
finishTime?: string
|
||||
}
|
||||
|
||||
/** 统计汇总 */
|
||||
export interface TaskStatisticsSummary {
|
||||
taskCount: number
|
||||
totalPrisoners: number
|
||||
totalCompleted: number
|
||||
totalPending: number
|
||||
overallCompletionRate: string | number
|
||||
}
|
||||
|
||||
// 问卷任务 API
|
||||
export const QuestionnaireTaskApi = {
|
||||
// 查询问卷任务分页
|
||||
getQuestionnaireTaskPage: async (params: QuestionnaireTaskPageParams) => {
|
||||
return await request.get({ url: `/prison/questionnaire-task/page`, params })
|
||||
},
|
||||
|
||||
// 查询问卷任务详情
|
||||
getQuestionnaireTask: async (id: number) => {
|
||||
return await request.get({ url: `/prison/questionnaire-task/get`, params: { id } })
|
||||
},
|
||||
|
||||
// 新增问卷任务
|
||||
createQuestionnaireTask: async (data: QuestionnaireTaskCreateParams) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/create`, data })
|
||||
},
|
||||
|
||||
// 修改问卷任务
|
||||
updateQuestionnaireTask: async (data: QuestionnaireTaskUpdateParams) => {
|
||||
return await request.put({ url: `/prison/questionnaire-task/update`, data })
|
||||
},
|
||||
|
||||
// 删除问卷任务
|
||||
deleteQuestionnaireTask: async (id: number) => {
|
||||
return await request.delete({ url: `/prison/questionnaire-task/delete`, params: { id } })
|
||||
},
|
||||
|
||||
// 批量删除问卷任务
|
||||
deleteQuestionnaireTaskList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/delete-list`, data: ids })
|
||||
},
|
||||
|
||||
// 导出问卷任务 Excel
|
||||
exportQuestionnaireTask: async (params: QuestionnaireTaskPageParams) => {
|
||||
return await request.download({ url: `/prison/questionnaire-task/export-excel`, params })
|
||||
},
|
||||
|
||||
// ==================== 任务执行相关 ====================
|
||||
|
||||
// 取消任务
|
||||
cancelTask: async (id: number) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/cancel`, params: { id } })
|
||||
},
|
||||
|
||||
// 结束任务
|
||||
finishTask: async (id: number) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/finish`, params: { id } })
|
||||
},
|
||||
|
||||
// 重新开始任务
|
||||
restartTask: async (id: number) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/restart`, params: { id } })
|
||||
},
|
||||
|
||||
// ==================== 进度跟踪相关 ====================
|
||||
|
||||
// 获取任务进度
|
||||
getTaskProgress: async (id: number) => {
|
||||
return await request.get<TaskProgress>({ url: `/prison/questionnaire-task/progress`, params: { id } })
|
||||
},
|
||||
|
||||
// 获取任务未完成人员
|
||||
getPendingPrisoners: async (id: number, params: any) => {
|
||||
return await request.get({ url: `/prison/questionnaire-task/pending-prisoners`, params: { id, ...params } })
|
||||
},
|
||||
|
||||
// 提醒未完成人员
|
||||
remindPendingPrisoners: async (id: number) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/remind`, params: { id } })
|
||||
},
|
||||
|
||||
// 获取任务的人员填写进度列表
|
||||
getPrisonerProgress: async (id: number) => {
|
||||
return await request.get<PrisonerProgress[]>({ url: `/prison/questionnaire-task/prisoner-progress`, params: { id } })
|
||||
},
|
||||
|
||||
// 通知单个人员
|
||||
notifyPrisoner: async (recordId: number) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/notify-prisoner`, params: { recordId } })
|
||||
},
|
||||
|
||||
// 重置人员答题记录
|
||||
resetPrisonerRecord: async (recordId: number) => {
|
||||
return await request.post({ url: `/prison/questionnaire-task/reset-record`, params: { recordId } })
|
||||
},
|
||||
|
||||
// ==================== 统计相关 ====================
|
||||
|
||||
// 按监区统计任务完成情况
|
||||
getTaskAreaStatistics: async (id: number) => {
|
||||
return await request.get<TaskAreaStatistics[]>({ url: `/prison/questionnaire-task/area-statistics`, params: { id } })
|
||||
},
|
||||
|
||||
// 获取全局任务统计汇总
|
||||
getStatisticsSummary: async () => {
|
||||
return await request.get<TaskStatisticsSummary>({ url: `/prison/questionnaire-task/statistics-summary` })
|
||||
},
|
||||
|
||||
// 按监区对比分析
|
||||
compareAreasByQuestionnaire: async (questionnaireId?: number, areaIds?: number[]) => {
|
||||
return await request.get({ url: `/prison/questionnaire-task/area-comparison`, params: { questionnaireId, areaIds } })
|
||||
}
|
||||
}
|
||||
@ -65,7 +65,7 @@ export const QuestionnaireApi = {
|
||||
|
||||
/** 批量删除问卷模板 */
|
||||
deleteQuestionnaireList: async (ids: number[]) => {
|
||||
return await request.post<boolean>({ url: `/prison/questionnaire/delete-list`, data: ids })
|
||||
return await request.delete<boolean>({ url: `/prison/questionnaire/delete-list`, params: { ids: ids.join(',') } })
|
||||
},
|
||||
|
||||
// 导出问卷模板 Excel
|
||||
|
||||
@ -160,11 +160,6 @@ export const QuestionnaireRecordApi = {
|
||||
return await request.post<boolean>({ url: `/prison/questionnaire-record/submit`, data })
|
||||
},
|
||||
|
||||
/** 代为提交答卷(民警代填) */
|
||||
submitAnswerByAgent: async (data: AssessmentAnswerSubmitReq) => {
|
||||
return await request.post<boolean>({ url: `/prison/questionnaire-record/submit-by-agent`, data })
|
||||
},
|
||||
|
||||
/** 结束测评 */
|
||||
finishAssessment: async (id: number) => {
|
||||
return await request.post<boolean>({ url: `/prison/questionnaire-record/finish`, params: { id } })
|
||||
|
||||
@ -92,7 +92,7 @@ export const QuickCommentApi = {
|
||||
|
||||
// 批量删除评语
|
||||
deleteList: async (ids: number[]) => {
|
||||
return await request.post({ url: '/prison/quick-comment/delete-list', data: ids })
|
||||
return await request.delete({ url: '/prison/quick-comment/delete-list?ids=' + ids.join(',') })
|
||||
},
|
||||
|
||||
// 导入评语
|
||||
|
||||
@ -67,7 +67,7 @@ export const ReleaseApi = {
|
||||
return request.delete({ url: `/prison/release/delete?id=${id}` })
|
||||
},
|
||||
deleteReleaseList: (ids: number[]) => {
|
||||
return request.post({ url: '/prison/release/delete-list', data: ids })
|
||||
return request.delete({ url: `/prison/release/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
doRelease: (id: number) => {
|
||||
return request.post({ url: `/prison/release/do-release?id=${id}` })
|
||||
|
||||
@ -113,7 +113,7 @@ export const RiskApi = {
|
||||
|
||||
/** 批量删除风险评估 */
|
||||
deleteRiskList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/risk/delete-list`, data: ids })
|
||||
return await request.delete({ url: `/prison/risk/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
|
||||
// 导出风险评估 Excel
|
||||
|
||||
@ -20,7 +20,7 @@ export interface RiskAssessment {
|
||||
id: number // 评估ID
|
||||
prisonerId?: number // 罪犯ID
|
||||
prisonerNo?: string // 罪犯编号
|
||||
prisonerName?: string // 罪犯姓名(用于回显)
|
||||
prisonerName?: string // 罪犯姓名
|
||||
assessmentType?: number // 评估类型:1-入狱评估 2-定期评估 3-专项评估
|
||||
assessmentDate?: string // 评估日期
|
||||
violenceScore: number // 暴力倾向得分
|
||||
@ -31,7 +31,7 @@ export interface RiskAssessment {
|
||||
riskFactors: string // 风险因素
|
||||
suggestions: string // 管控建议
|
||||
// assessorId 和 assessorName 由后端自动从登录上下文获取,不需要前端传递
|
||||
nextAssessmentDate?: string // 下次评估日期
|
||||
nextAssessmentDate: string // 下次评估日期
|
||||
status?: number // 状态:1-待审核 2-已通过
|
||||
remark: string // 备注
|
||||
createTime?: string // 创建时间
|
||||
@ -66,7 +66,7 @@ export const RiskAssessmentApi = {
|
||||
|
||||
/** 批量删除危险评估 */
|
||||
deleteRiskAssessmentList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/risk-assessment/delete-list`, data: ids })
|
||||
return await request.delete({ url: `/prison/risk-assessment/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
|
||||
// 导出危险评估 Excel
|
||||
|
||||
@ -58,7 +58,7 @@ export const ScoreDetailApi = {
|
||||
return request.delete({ url: `/prison/score-detail/delete?id=${id}` })
|
||||
},
|
||||
deleteList: (ids: number[]) => {
|
||||
return request.post({ url: '/prison/score-detail/delete-list', data: ids })
|
||||
return request.delete({ url: `/prison/score-detail/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
export: (params: ScoreDetailPageReqVO) => {
|
||||
return request.download({ url: '/prison/score-detail/export-excel', params })
|
||||
|
||||
@ -56,7 +56,7 @@ export const ScoreRuleApi = {
|
||||
return request.delete({ url: `/prison/score-rule/delete?id=${id}` })
|
||||
},
|
||||
deleteRuleList: (ids: number[]) => {
|
||||
return request.post({ url: '/prison/score-rule/delete-list', data: ids })
|
||||
return request.delete({ url: `/prison/score-rule/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
getRuleByCategory: (category: number) => {
|
||||
return request.get({ url: `/prison/score-rule/list-by-category?category=${category}` })
|
||||
|
||||
@ -68,7 +68,7 @@ export const ScoreApi = {
|
||||
|
||||
/** 批量删除计分考核 */
|
||||
deleteScoreList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/score/delete-list`, data: ids })
|
||||
return await request.delete({ url: `/prison/score/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
|
||||
// 导出计分考核 Excel
|
||||
|
||||
@ -106,7 +106,7 @@ export const SituationApi = {
|
||||
|
||||
/** 批量删除狱情收集 */
|
||||
deleteSituationList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/situation/delete-list`, data: ids })
|
||||
return await request.delete({ url: `/prison/situation/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
|
||||
// 导出狱情收集 Excel
|
||||
|
||||
@ -161,7 +161,7 @@ export const WarningApi = {
|
||||
|
||||
/** 批量删除预警 */
|
||||
deleteWarningList: async (ids: number[]) => {
|
||||
return await request.post({ url: `/prison/warning/delete-list`, data: ids })
|
||||
return await request.delete({ url: `/prison/warning/delete-list?ids=${ids.join(',')}` })
|
||||
},
|
||||
|
||||
// 导出预警 Excel
|
||||
|
||||
@ -70,9 +70,10 @@ service.interceptors.request.use(
|
||||
}
|
||||
}
|
||||
// 监狱系统:即使 tenantEnable 为 false,也尝试获取并设置租户 ID
|
||||
// 如果缓存中没有租户 ID,使用默认值 1
|
||||
const tenantId = getTenantId() || 1
|
||||
config.headers['tenant-id'] = tenantId
|
||||
const tenantId = getTenantId()
|
||||
if (tenantId) {
|
||||
config.headers['tenant-id'] = tenantId
|
||||
}
|
||||
const method = config.method?.toUpperCase()
|
||||
// 防止 GET 请求缓存
|
||||
if (method === 'GET') {
|
||||
|
||||
@ -55,15 +55,7 @@ const whiteList = [
|
||||
'/register',
|
||||
'/oauthLogin/gitee',
|
||||
'/prisoner/prisoner/dashboard', // Dashboard 页面
|
||||
'/ai-dash-entry', // DashEntry 页面
|
||||
// 监狱模块路由(解决SPA路由重定向问题)
|
||||
'/prison/template', // 问卷模版管理
|
||||
'/prison/questionnaire', // 问卷任务管理
|
||||
'/prison/guard', // 狱警管理
|
||||
// AI指导建议路由(evaluation-mgmt)
|
||||
'/prison/evaluation-mgmt/template', // 评估模板管理
|
||||
'/prison/evaluation-mgmt/dimension', // 评估维度管理
|
||||
'/prison/evaluation-mgmt/report', // 评估报告管理
|
||||
'/ai-dash-entry' // DashEntry 页面
|
||||
]
|
||||
|
||||
// 路由加载前
|
||||
|
||||
@ -281,32 +281,7 @@ const riskTrendOptions = computed<EChartsOption>(() => {
|
||||
// 计算最大值用于设置Y轴范围
|
||||
const allValues = [...highRiskData, ...warningData, ...normalData]
|
||||
const maxValue = Math.max(...allValues, 10)
|
||||
|
||||
// 动态计算Y轴最大值,根据数据范围自动向上取整到合适的量级
|
||||
let yAxisMax: number
|
||||
if (maxValue === 0) {
|
||||
yAxisMax = 10
|
||||
} else if (maxValue <= 10) {
|
||||
yAxisMax = Math.ceil(maxValue)
|
||||
} else if (maxValue <= 50) {
|
||||
// 对于10-50的数据,向上取整到10的倍数
|
||||
yAxisMax = Math.ceil(maxValue / 10) * 10
|
||||
} else if (maxValue <= 100) {
|
||||
// 对于50-100的数据,向上取整到10的倍数
|
||||
yAxisMax = Math.ceil(maxValue / 10) * 10
|
||||
} else if (maxValue <= 500) {
|
||||
// 对于100-500的数据,向上取整到50的倍数
|
||||
yAxisMax = Math.ceil(maxValue / 50) * 50
|
||||
} else if (maxValue <= 1000) {
|
||||
// 对于500-1000的数据,向上取整到100的倍数
|
||||
yAxisMax = Math.ceil(maxValue / 100) * 100
|
||||
} else {
|
||||
// 对于大于1000的数据,向上取整到500的倍数
|
||||
yAxisMax = Math.ceil(maxValue / 500) * 500
|
||||
}
|
||||
|
||||
// 计算合适的刻度间隔,确保显示6-8个刻度
|
||||
const interval = Math.max(Math.ceil(yAxisMax / 6), 1)
|
||||
const yAxisMax = Math.ceil(maxValue / 50) * 50 + 50
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
@ -331,7 +306,7 @@ const riskTrendOptions = computed<EChartsOption>(() => {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: yAxisMax,
|
||||
interval: interval
|
||||
interval: Math.ceil(yAxisMax / 6)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
|
||||
@ -10,19 +10,20 @@
|
||||
<div class="dashboard-content-top-center">
|
||||
<div class="gauge-container">
|
||||
<div class="dashboard-content-top-center-data">
|
||||
<!-- 左侧区域:上1下2排列 -->
|
||||
<div class="info-field-item top-field">
|
||||
<div class="field-label">累计服刑天数</div>
|
||||
<div class="field-value">{{ servedDays }}</div>
|
||||
<!-- 左侧第一个卡片 -->
|
||||
<div class="info-card-item">
|
||||
<div class="card-number">{{ centerLeftData.top.value }}</div>
|
||||
<div class="card-label">{{ centerLeftData.top.label }}</div>
|
||||
</div>
|
||||
<!-- 左侧第二个卡片 -->
|
||||
<div class="card-row">
|
||||
<div class="info-field-item">
|
||||
<div class="field-label">累计违规次数</div>
|
||||
<div class="field-value">{{ violationCount }}</div>
|
||||
<div class="info-card-item center-right-card-item">
|
||||
<div class="card-number">{{ centerLeftData.middle.left.value }}</div>
|
||||
<div class="card-label">{{ centerLeftData.middle.left.label }}</div>
|
||||
</div>
|
||||
<div class="info-field-item">
|
||||
<div class="field-label">累计表扬次数</div>
|
||||
<div class="field-value">{{ praiseCount }}</div>
|
||||
<div class="info-card-item center-right-card-item">
|
||||
<div class="card-number">{{ centerLeftData.middle.right.value }}</div>
|
||||
<div class="card-label">{{ centerLeftData.middle.right.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -30,19 +31,20 @@
|
||||
<GaugeChart :height="'240px'" :value="gaugeValue" :name="gaugeName" />
|
||||
</div>
|
||||
<div class="dashboard-content-top-center-data">
|
||||
<!-- 右侧区域:上1下2排列 -->
|
||||
<div class="info-field-item top-field">
|
||||
<div class="field-label">剩余刑期天数</div>
|
||||
<div class="field-value">{{ remainingDays }}</div>
|
||||
<!-- 右侧第一个卡片 -->
|
||||
<div class="info-card-item">
|
||||
<div class="card-number">{{ centerRightData.top.value }}</div>
|
||||
<div class="card-label">{{ centerRightData.top.label }}</div>
|
||||
</div>
|
||||
<!-- 右侧第二个卡片 -->
|
||||
<div class="card-row">
|
||||
<div class="info-field-item">
|
||||
<div class="field-label">累计扣分次数</div>
|
||||
<div class="field-value">{{ penaltyCount }}</div>
|
||||
<div class="info-card-item center-right-card-item">
|
||||
<div class="card-number">{{ centerRightData.middle.left.value }}</div>
|
||||
<div class="card-label">{{ centerRightData.middle.left.label }}</div>
|
||||
</div>
|
||||
<div class="info-field-item">
|
||||
<div class="field-label">累计加分次数</div>
|
||||
<div class="field-value">{{ rewardCount }}</div>
|
||||
<div class="info-card-item center-right-card-item">
|
||||
<div class="card-number">{{ centerRightData.middle.right.value }}</div>
|
||||
<div class="card-label">{{ centerRightData.middle.right.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -50,19 +52,19 @@
|
||||
<div class="list-container">
|
||||
<div class="list-card-item">
|
||||
<div class="list-card-item-icon icon-location"></div>
|
||||
<div class="list-card-item-value">-</div>
|
||||
<div class="list-card-item-value">108</div>
|
||||
</div>
|
||||
<div class="list-card-item">
|
||||
<div class="list-card-item-icon icon-person"></div>
|
||||
<div class="list-card-item-value">-</div>
|
||||
<div class="list-card-item-value">108</div>
|
||||
</div>
|
||||
<div class="list-card-item">
|
||||
<div class="list-card-item-icon icon-person2"></div>
|
||||
<div class="list-card-item-value">-</div>
|
||||
<div class="list-card-item-value">108</div>
|
||||
</div>
|
||||
<div class="list-card-item">
|
||||
<div class="list-card-item-icon icon-car"></div>
|
||||
<div class="list-card-item-value">-</div>
|
||||
<div class="list-card-item-value">108</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -83,7 +85,7 @@
|
||||
</div>
|
||||
<div class="dashboard-content-bottom-right">
|
||||
<div class="dashboard-content-bottom-right-title">大帐统计</div>
|
||||
<BarChart :data="barChartData" :balance="balance" />
|
||||
<BarChart :height="'200px'" :data="barChartData" :card-data="barCardData" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -107,8 +109,6 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { DashboardApi } from '@/api/prison/dashboard'
|
||||
import { SituationApi } from '@/api/prison/situation'
|
||||
import { ScoreApi } from '@/api/prison/score'
|
||||
|
||||
defineOptions({ name: 'Dashboard' })
|
||||
|
||||
@ -118,59 +118,63 @@ const route = useRoute()
|
||||
const gaugeValue = ref(0)
|
||||
const gaugeName = ref('')
|
||||
|
||||
// 中心左侧数据 - 根据原型设计更新
|
||||
// 中心左侧数据
|
||||
const centerLeftData = ref({
|
||||
top: {
|
||||
value: '0',
|
||||
label: '累计服刑天数'
|
||||
label: '加载中...'
|
||||
},
|
||||
middle: {
|
||||
left: {
|
||||
value: '0',
|
||||
label: '剩余刑期天数'
|
||||
label: '加载中...'
|
||||
},
|
||||
right: {
|
||||
value: '0',
|
||||
label: '累计违规次数'
|
||||
value: '0%',
|
||||
label: '加载中...'
|
||||
}
|
||||
},
|
||||
bottom: {
|
||||
value: '-',
|
||||
label: '累计表扬天数'
|
||||
value: '0位',
|
||||
label: '加载中...'
|
||||
}
|
||||
})
|
||||
|
||||
// 中心右侧数据 - 根据原型设计更新
|
||||
// 中心右侧数据
|
||||
const centerRightData = ref({
|
||||
top: {
|
||||
value: '0',
|
||||
label: '累计扣分次数'
|
||||
label: '加载中...'
|
||||
},
|
||||
middle: {
|
||||
left: {
|
||||
value: '0',
|
||||
label: '累计加分次数'
|
||||
label: '加载中...'
|
||||
},
|
||||
right: {
|
||||
value: '-',
|
||||
label: '本月消费'
|
||||
value: '0',
|
||||
label: '加载中...'
|
||||
}
|
||||
},
|
||||
bottomLeft: {
|
||||
value: '-',
|
||||
label: '本月奖励'
|
||||
value: '0位',
|
||||
label: '加载中...'
|
||||
},
|
||||
bottomRight: {
|
||||
value: '-',
|
||||
label: '本月惩罚'
|
||||
value: '0辆',
|
||||
label: '加载中...'
|
||||
}
|
||||
})
|
||||
|
||||
// 柱状图数据
|
||||
const barChartData = ref<{ category: string; monthlyStandard: number; perCapita: number }[]>([])
|
||||
|
||||
// 账户余额
|
||||
const balance = ref(0)
|
||||
// 卡片数据
|
||||
const barCardData = ref({
|
||||
inProgress: 0,
|
||||
toWarehouse: 0,
|
||||
outWarehouse: 0
|
||||
})
|
||||
|
||||
// 基本信息数据
|
||||
const basicInfo = ref({
|
||||
@ -240,16 +244,6 @@ const rewardsPunishments = ref<{
|
||||
const currentTime = ref('')
|
||||
const prisonerName = ref('加载中...')
|
||||
|
||||
// 左侧区域三个字段数据
|
||||
const servedDays = ref(0)
|
||||
const violationCount = ref(0)
|
||||
const praiseCount = ref(0)
|
||||
|
||||
// 右侧区域三个字段数据
|
||||
const remainingDays = ref(0)
|
||||
const penaltyCount = ref(0)
|
||||
const rewardCount = ref(0)
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = () => {
|
||||
const now = new Date()
|
||||
@ -278,80 +272,30 @@ const loadData = async (prisonerId: number) => {
|
||||
// 更新罪犯名称
|
||||
prisonerName.value = res.prisonerName || '未知'
|
||||
|
||||
// 更新仪表盘 - 危险评估分数
|
||||
// 更新仪表盘
|
||||
gaugeValue.value = res.riskScore || 0
|
||||
gaugeName.value = ''
|
||||
|
||||
// 计算累计服刑天数和剩余刑期天数
|
||||
const servedDaysValue = res.servedDays || 0;
|
||||
servedDays.value = servedDaysValue
|
||||
remainingDays.value = 0;
|
||||
if (res.imprisonmentDate && res.releaseDate) {
|
||||
const startDate = new Date(res.imprisonmentDate);
|
||||
const endDate = new Date(res.releaseDate);
|
||||
const totalDays = Math.floor((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||
remainingDays.value = Math.max(0, totalDays - servedDaysValue);
|
||||
}
|
||||
|
||||
// 获取计分考核数据 - 累计扣分次数和累计加分次数
|
||||
let totalPenaltyCount = 0;
|
||||
let totalRewardCount = 0;
|
||||
try {
|
||||
const scoreRes = await ScoreApi.getScorePage({
|
||||
pageNo: 1,
|
||||
pageSize: 200,
|
||||
prisonerNo: res.prisonerNo
|
||||
})
|
||||
if (scoreRes.list && scoreRes.list.length > 0) {
|
||||
totalRewardCount = scoreRes.list.filter((item: any) => item.rewardScore > 0).length
|
||||
totalPenaltyCount = scoreRes.list.filter((item: any) => item.penaltyScore > 0).length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取计分考核数据失败:', error)
|
||||
}
|
||||
|
||||
// 更新右侧区域三个字段数据
|
||||
penaltyCount.value = totalPenaltyCount
|
||||
rewardCount.value = totalRewardCount
|
||||
|
||||
// 获取狱情收集数据 - 累计违规次数
|
||||
let totalViolationCount = 0
|
||||
try {
|
||||
const situationRes = await SituationApi.getSituationPage({
|
||||
pageNo: 1,
|
||||
pageSize: 200
|
||||
})
|
||||
if (situationRes.list && situationRes.list.length > 0) {
|
||||
totalViolationCount = situationRes.list.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取狱情收集数据失败:', error)
|
||||
}
|
||||
|
||||
// 更新左侧区域三个字段数据
|
||||
violationCount.value = totalViolationCount
|
||||
praiseCount.value = res.praiseCount || 0
|
||||
|
||||
// 更新中心左侧数据
|
||||
if (res.centerLeftData) {
|
||||
centerLeftData.value = {
|
||||
top: {
|
||||
value: res.centerLeftData.topValue || '0',
|
||||
label: res.centerLeftData.topLabel || '本月消费'
|
||||
label: res.centerLeftData.topLabel || ''
|
||||
},
|
||||
middle: {
|
||||
left: {
|
||||
value: res.centerLeftData.middleLeftValue || '0',
|
||||
label: res.centerLeftData.middleLeftLabel || '本月奖励'
|
||||
label: res.centerLeftData.middleLeftLabel || ''
|
||||
},
|
||||
right: {
|
||||
value: res.centerLeftData.middleRightValue || '0',
|
||||
label: res.centerLeftData.middleRightLabel || '本月惩罚'
|
||||
label: res.centerLeftData.middleRightLabel || ''
|
||||
}
|
||||
},
|
||||
bottom: {
|
||||
value: res.centerLeftData.bottomValue || '0',
|
||||
label: res.centerLeftData.bottomLabel || '账户余额'
|
||||
value: res.centerLeftData.bottomValue || '0位',
|
||||
label: res.centerLeftData.bottomLabel || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -361,25 +305,25 @@ const loadData = async (prisonerId: number) => {
|
||||
centerRightData.value = {
|
||||
top: {
|
||||
value: res.centerRightData.topValue || '0',
|
||||
label: res.centerRightData.topLabel || '本月得分'
|
||||
label: res.centerRightData.topLabel || ''
|
||||
},
|
||||
middle: {
|
||||
left: {
|
||||
value: res.centerRightData.middleLeftValue || '0',
|
||||
label: res.centerRightData.middleLeftLabel || '基础分'
|
||||
label: res.centerRightData.middleLeftLabel || ''
|
||||
},
|
||||
right: {
|
||||
value: res.centerRightData.middleRightValue || '0',
|
||||
label: res.centerRightData.middleRightLabel || '加分项'
|
||||
label: res.centerRightData.middleRightLabel || ''
|
||||
}
|
||||
},
|
||||
bottomLeft: {
|
||||
value: res.centerRightData.bottomLeftValue || '0',
|
||||
label: res.centerRightData.bottomLeftLabel || '扣分项'
|
||||
value: res.centerRightData.bottomLeftValue || '0位',
|
||||
label: res.centerRightData.bottomLeftLabel || ''
|
||||
},
|
||||
bottomRight: {
|
||||
value: res.centerRightData.bottomRightValue || '暂无',
|
||||
label: res.centerRightData.bottomRightLabel || '考核等级'
|
||||
value: res.centerRightData.bottomRightValue || '0辆',
|
||||
label: res.centerRightData.bottomRightLabel || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -387,8 +331,14 @@ const loadData = async (prisonerId: number) => {
|
||||
// 更新柱状图数据
|
||||
barChartData.value = res.consumptionMonthlyData || []
|
||||
|
||||
// 更新账户余额
|
||||
balance.value = res.balance || 0
|
||||
// 更新消费汇总
|
||||
if (res.consumptionSummary) {
|
||||
barCardData.value = {
|
||||
inProgress: res.consumptionSummary.inProgress || 0,
|
||||
toWarehouse: res.consumptionSummary.toWarehouse || 0,
|
||||
outWarehouse: res.consumptionSummary.outWarehouse || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 更新基本信息
|
||||
basicInfo.value = {
|
||||
@ -396,7 +346,7 @@ const loadData = async (prisonerId: number) => {
|
||||
prisonNumber: res.prisonerNo || '',
|
||||
sentenceStart: res.imprisonmentDate || '',
|
||||
sentenceEnd: res.releaseDate || '',
|
||||
sentenceDays: res.sentenceDays || 0,
|
||||
sentenceDays: res.servedDays || 0,
|
||||
age: res.age || 0,
|
||||
hometown: res.nativePlace || '',
|
||||
education: res.education || '',
|
||||
@ -557,7 +507,7 @@ onUnmounted(() => {
|
||||
.list-card-item {
|
||||
width: 25%;
|
||||
height: 90%;
|
||||
background: rgba(45, 65, 131, 0.6);
|
||||
background: #2d3d5f;
|
||||
border: 1px solid rgba(56, 102, 141, 0.5);
|
||||
display: flex;
|
||||
padding-left: 15px;
|
||||
@ -600,55 +550,6 @@ onUnmounted(() => {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// 纵向字段项样式
|
||||
.info-field-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 16px;
|
||||
background: rgba(45, 65, 131, 0.6);
|
||||
border: 1px solid rgba(56, 102, 141, 0.5);
|
||||
border-radius: 6px;
|
||||
box-shadow: inset 0 0 10px 0 rgba(43, 65, 131, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(56, 102, 141, 0.8);
|
||||
box-shadow: inset 0 0 15px 0 rgba(43, 65, 131, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// 顶部字段样式(用于上1下2布局中的顶部字段)
|
||||
.top-field {
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 字段标签样式
|
||||
.field-label {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 字段数值样式
|
||||
.field-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dashboard-content-top-center-center {
|
||||
width: 40%;
|
||||
height: 220px;
|
||||
|
||||
@ -1,7 +1,22 @@
|
||||
<template>
|
||||
<div class="supply-chart-container" ref="containerRef">
|
||||
<div class="supply-chart-container">
|
||||
<!-- 卡片统计 -->
|
||||
<div class="chart-cards">
|
||||
<div class="chart-card-item">
|
||||
<div class="card-value">{{ cardData.inProgress }}</div>
|
||||
<div class="card-label">进行中</div>
|
||||
</div>
|
||||
<div class="chart-card-item">
|
||||
<div class="card-value">{{ cardData.toWarehouse }}</div>
|
||||
<div class="card-label">待入库</div>
|
||||
</div>
|
||||
<div class="chart-card-item">
|
||||
<div class="card-value">{{ cardData.outWarehouse }}</div>
|
||||
<div class="card-label">已出库</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 柱状图 -->
|
||||
<EChart ref="chartRef" :options="barOption" :height="height" />
|
||||
<EChart :options="barOption" :height="height" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -9,7 +24,7 @@
|
||||
import type { EChartsOption } from 'echarts'
|
||||
// @ts-ignore
|
||||
import EChart from '@/components/Echart/src/Echart.vue'
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
defineOptions({ name: 'BarChart' })
|
||||
|
||||
@ -19,45 +34,49 @@ interface ChartDataItem {
|
||||
perCapita: number
|
||||
}
|
||||
|
||||
interface CardData {
|
||||
inProgress: number
|
||||
toWarehouse: number
|
||||
outWarehouse: number
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
width?: number | string
|
||||
width?: number
|
||||
height?: string
|
||||
data?: ChartDataItem[]
|
||||
balance?: number
|
||||
cardData?: CardData
|
||||
}>(),
|
||||
{
|
||||
width: '100%',
|
||||
width: 400,
|
||||
height: '300px',
|
||||
data: () => [],
|
||||
balance: () => 0
|
||||
cardData: () => ({
|
||||
inProgress: 5,
|
||||
toWarehouse: 5,
|
||||
outWarehouse: 5
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const containerRef = ref<HTMLElement>()
|
||||
const chartRef = ref()
|
||||
|
||||
// 创建图表配置
|
||||
const createChartOption = (): EChartsOption => {
|
||||
const categories = props.data.map((item) => item.category)
|
||||
const monthlyStandardData = props.data.map((item) => item.monthlyStandard ?? 0)
|
||||
const perCapitaData = props.data.map((item) => item.perCapita ?? 0)
|
||||
const monthlyStandardData = props.data.map((item) => item.monthlyStandard)
|
||||
const perCapitaData = props.data.map((item) => item.perCapita)
|
||||
|
||||
// 动态计算最大值,确保能够显示所有数据
|
||||
const maxDataValue = Math.max(...monthlyStandardData, ...perCapitaData, 100)
|
||||
// 向上取整到百位,并留出 20% 空间
|
||||
const maxValue = Math.ceil(maxDataValue * 1.2 / 100) * 100
|
||||
|
||||
// 创建底色数据(填充到 maxValue)
|
||||
const monthlyStandardBgData = categories.map((_, index) => Math.max(0, maxValue - monthlyStandardData[index]))
|
||||
const perCapitaBgData = categories.map((_, index) => Math.max(0, maxValue - perCapitaData[index]))
|
||||
// 创建底色数据(最大值50)
|
||||
const maxValue = 50
|
||||
const monthlyStandardBgData = categories.map((_, index) => maxValue - monthlyStandardData[index])
|
||||
const perCapitaBgData = categories.map((_, index) => maxValue - perCapitaData[index])
|
||||
|
||||
return {
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
top: '25%',
|
||||
bottom: '18%',
|
||||
left: '10%',
|
||||
right: '15%',
|
||||
top: '20%',
|
||||
bottom: '15%',
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
@ -81,8 +100,8 @@ const createChartOption = (): EChartsOption => {
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: maxValue,
|
||||
interval: Math.ceil(maxValue / 5 / 100) * 100,
|
||||
max: 50,
|
||||
interval: 10,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
@ -91,8 +110,7 @@ const createChartOption = (): EChartsOption => {
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#D8F0FF',
|
||||
fontSize: 10,
|
||||
formatter: (value: number) => value.toString()
|
||||
fontSize: 10
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
@ -101,33 +119,17 @@ const createChartOption = (): EChartsOption => {
|
||||
}
|
||||
}
|
||||
},
|
||||
// 图例和余额放在同一行
|
||||
legend: {
|
||||
data: [
|
||||
{ name: '支出', icon: 'rect' },
|
||||
{ name: '收入', icon: 'rect' }
|
||||
],
|
||||
top: '3%',
|
||||
left: '8%',
|
||||
data: ['支出', '收入'],
|
||||
top: '5%',
|
||||
right: '10%',
|
||||
textStyle: {
|
||||
color: '#6D869A',
|
||||
fontSize: 10
|
||||
fontSize: 9
|
||||
},
|
||||
itemWidth: 12,
|
||||
itemHeight: 8,
|
||||
itemGap: 15
|
||||
},
|
||||
// 使用 title 显示余额,放在图例右侧
|
||||
title: {
|
||||
text: `账户余额: ${props.balance}元`,
|
||||
left: 'auto',
|
||||
right: '8%',
|
||||
top: '3%',
|
||||
textStyle: {
|
||||
color: '#00d4ff',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
itemWidth: 9,
|
||||
itemHeight: 9,
|
||||
itemGap: 25
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
@ -142,6 +144,7 @@ const createChartOption = (): EChartsOption => {
|
||||
},
|
||||
formatter: function (params: any) {
|
||||
let result = params[0].name + '<br/>'
|
||||
// 只显示数据系列,不显示底色系列
|
||||
params.forEach((param: any) => {
|
||||
if (param.seriesName === '支出' || param.seriesName === '收入') {
|
||||
result += param.marker + param.seriesName + ': ' + param.value + '<br/>'
|
||||
@ -151,7 +154,7 @@ const createChartOption = (): EChartsOption => {
|
||||
}
|
||||
},
|
||||
series: [
|
||||
// 支出数据
|
||||
// 支出数据(渐变)- 先绘制,作为底层
|
||||
{
|
||||
name: '支出',
|
||||
type: 'bar',
|
||||
@ -162,36 +165,40 @@ const createChartOption = (): EChartsOption => {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 1,
|
||||
y2: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#10A0F2' },
|
||||
{ offset: 0.5, color: '#0D8BD9' },
|
||||
{ offset: 1, color: '#0A6EB0' }
|
||||
{
|
||||
offset: 0,
|
||||
color: '#10A0F2'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(0, 82, 184, 0)'
|
||||
}
|
||||
]
|
||||
},
|
||||
borderRadius: [2, 2, 0, 0]
|
||||
}
|
||||
},
|
||||
barWidth: '25%',
|
||||
barGap: '30%'
|
||||
barWidth: '20%',
|
||||
barGap: '20%'
|
||||
},
|
||||
// 支出底色
|
||||
// 支出底色 - 后绘制,堆叠在数据上方
|
||||
{
|
||||
name: '支出底色',
|
||||
type: 'bar',
|
||||
stack: 'monthly',
|
||||
data: monthlyStandardBgData,
|
||||
itemStyle: {
|
||||
color: 'rgba(56, 102, 141, 0.3)'
|
||||
color: '#38668D70'
|
||||
},
|
||||
barWidth: '25%',
|
||||
barGap: '30%',
|
||||
barWidth: '20%',
|
||||
barGap: '20%',
|
||||
silent: true,
|
||||
tooltip: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
// 收入数据
|
||||
// 收入数据(渐变)
|
||||
{
|
||||
name: '收入',
|
||||
type: 'bar',
|
||||
@ -202,30 +209,33 @@ const createChartOption = (): EChartsOption => {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 1,
|
||||
y2: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: '#FFA58D' },
|
||||
{ offset: 0.5, color: '#E88F5A' },
|
||||
{ offset: 1, color: '#D07530' }
|
||||
{
|
||||
offset: 0,
|
||||
color: '#FFA58D'
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(87, 140, 205, 0)'
|
||||
}
|
||||
]
|
||||
},
|
||||
borderRadius: [2, 2, 0, 0]
|
||||
}
|
||||
},
|
||||
barWidth: '25%',
|
||||
barGap: '30%'
|
||||
barWidth: '20%',
|
||||
barGap: '80%'
|
||||
},
|
||||
// 收入底色
|
||||
{
|
||||
name: '收入底色',
|
||||
type: 'bar',
|
||||
stack: 'perCapita',
|
||||
data: perCapitaBgData,
|
||||
itemStyle: {
|
||||
color: 'rgba(56, 102, 141, 0.3)'
|
||||
color: '#38668D70'
|
||||
},
|
||||
barWidth: '25%',
|
||||
barGap: '30%',
|
||||
barWidth: '20%',
|
||||
barGap: '80%',
|
||||
silent: true,
|
||||
tooltip: {
|
||||
show: false
|
||||
@ -237,6 +247,15 @@ const createChartOption = (): EChartsOption => {
|
||||
|
||||
// 柱状图配置
|
||||
const barOption = computed(() => createChartOption())
|
||||
|
||||
// 监听数据变化,更新图表
|
||||
watch(
|
||||
() => [props.data, props.cardData],
|
||||
() => {
|
||||
// 数据变化时,computed 会自动更新
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -246,4 +265,30 @@ const barOption = computed(() => createChartOption())
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-cards {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chart-card-item {
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
background: rgba(56, 102, 141, 0.3);
|
||||
border-radius: 8px;
|
||||
min-width: 100px;
|
||||
|
||||
.card-value {
|
||||
font-size: 1.6vh;
|
||||
font-weight: bold;
|
||||
color: #00d4ff;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 1.5vh;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<div class="info-tag">{{ basicInfo.district }}</div>
|
||||
<div class="info-tag">狱政编号: {{ basicInfo.prisonNumber }}</div>
|
||||
<div class="info-tag">
|
||||
刑期:{{ basicInfo.sentenceStart }} --- {{ basicInfo.sentenceEnd }} (总天数:{{
|
||||
刑期起/止日:{{ basicInfo.sentenceStart }}---{{ basicInfo.sentenceEnd }} ({{
|
||||
basicInfo.sentenceDays
|
||||
}}天)
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,18 @@
|
||||
<div class="rewards-punishments-container">
|
||||
<!-- 标题栏 -->
|
||||
<div class="rewards-header">
|
||||
<span class="header-title">风险评估</span>
|
||||
<span class="header-title">近期奖惩</span>
|
||||
<div class="filter-tabs">
|
||||
<div
|
||||
v-for="tab in filterTabs"
|
||||
:key="tab.value"
|
||||
class="filter-tab"
|
||||
:class="{ active: activeFilter === tab.value }"
|
||||
@click="activeFilter = tab.value"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间线列表 -->
|
||||
@ -10,7 +21,7 @@
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-line"></div>
|
||||
<div class="timeline-items">
|
||||
<div v-for="(item, index) in listData" :key="index" class="timeline-item">
|
||||
<div v-for="(item, index) in filteredList" :key="index" class="timeline-item">
|
||||
<div class="timeline-dot" :class="item.type"></div>
|
||||
<div class="timeline-card">
|
||||
<div class="card-type" :class="item.type">{{ item.typeText }}</div>
|
||||
@ -24,7 +35,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
defineOptions({ name: 'RecentRewardsPunishments' })
|
||||
|
||||
@ -35,9 +46,29 @@ interface RewardPunishmentItem {
|
||||
content: string // 内容
|
||||
}
|
||||
|
||||
// 过滤标签
|
||||
const filterTabs = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '奖励记录', value: 'reward' },
|
||||
{ label: '惩罚记录', value: 'punishment' }
|
||||
]
|
||||
|
||||
const activeFilter = ref<string>('all')
|
||||
|
||||
// 数据列表 - 使用 ref 存储
|
||||
const listData = ref<RewardPunishmentItem[]>([])
|
||||
|
||||
// 过滤后的列表
|
||||
const filteredList = computed(() => {
|
||||
if (activeFilter.value === 'all') {
|
||||
return listData.value
|
||||
} else if (activeFilter.value === 'reward') {
|
||||
return listData.value.filter((item) => item.type === 'reward')
|
||||
} else {
|
||||
return listData.value.filter((item) => item.type === 'danger')
|
||||
}
|
||||
})
|
||||
|
||||
// 可以通过 props 接收外部数据
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -87,6 +118,24 @@ watch(
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: rgba(56, 102, 141, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
&.active {
|
||||
background: #37599d;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
// 时间线容器
|
||||
.timeline-container {
|
||||
flex: 1 1 0;
|
||||
|
||||
@ -351,10 +351,6 @@ watch(
|
||||
onMounted(() => {
|
||||
getLoginFormCache()
|
||||
getTenantByWebsite()
|
||||
// 初始化租户 ID - 确保登录前缓存中有租户 ID
|
||||
if (!authUtil.getTenantId()) {
|
||||
authUtil.setTenantId(Number(loginData.loginForm.tenantName) || 1)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,16 +1,8 @@
|
||||
<template>
|
||||
<Dialog title="消费明细" v-model="dialogVisible" width="600px">
|
||||
<el-table :data="detailList" v-loading="loading">
|
||||
<el-table-column label="商品名称" prop="goodsName" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.goodsName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品编码" prop="goodsCode" align="center" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.goodsCode || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品名称" prop="goodsName" align="center" />
|
||||
<el-table-column label="商品编码" prop="goodsCode" align="center" width="120" />
|
||||
<el-table-column label="单价" prop="goodsPrice" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
¥{{ row.goodsPrice?.toFixed(2) }}
|
||||
|
||||
@ -9,13 +9,13 @@
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="服刑人员">
|
||||
<el-input v-model="formData.prisonerName" placeholder="服刑人员姓名" disabled />
|
||||
<el-form-item label="罪犯ID" prop="prisonerId">
|
||||
<el-input v-model="formData.prisonerId" placeholder="请输入罪犯ID" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="罪犯编号" prop="prisonerNo">
|
||||
<el-input v-model="formData.prisonerNo" placeholder="罪犯编号" disabled />
|
||||
<el-input v-model="formData.prisonerNo" placeholder="请输入罪犯编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
@ -135,7 +135,6 @@ const formType = ref('')
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
prisonerId: undefined,
|
||||
prisonerName: undefined,
|
||||
prisonerNo: undefined,
|
||||
orderNo: undefined,
|
||||
type: undefined,
|
||||
@ -229,7 +228,6 @@ const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
prisonerId: undefined,
|
||||
prisonerName: undefined,
|
||||
prisonerNo: undefined,
|
||||
orderNo: undefined,
|
||||
type: undefined,
|
||||
|
||||
@ -139,13 +139,6 @@
|
||||
>
|
||||
<Icon icon="ep:view" /> 查看
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
@click="handlePreview(row)"
|
||||
>
|
||||
<Icon icon="ep:view" /> 预览
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1 || row.status === 4"
|
||||
link
|
||||
@ -190,9 +183,6 @@
|
||||
|
||||
<!-- 报告详情弹窗 -->
|
||||
<ReportDetailDialog ref="detailDialogRef" />
|
||||
|
||||
<!-- 报告预览弹窗 -->
|
||||
<ReportPreviewDialog ref="previewDialogRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -201,7 +191,6 @@ import download from '@/utils/download'
|
||||
import { EvaluationReportApi, EvaluationReport, EvaluationTemplateApi } from '@/api/prison/evaluation'
|
||||
import CreateReportDialog from './CreateReportDialog.vue'
|
||||
import ReportDetailDialog from './ReportDetailDialog.vue'
|
||||
import ReportPreviewDialog from './components/ReportPreviewDialog.vue'
|
||||
|
||||
defineOptions({ name: 'EvaluationReport' })
|
||||
|
||||
@ -294,12 +283,6 @@ const handleView = (row: EvaluationReport) => {
|
||||
detailDialogRef.value?.open(row.id)
|
||||
}
|
||||
|
||||
/** 预览报告 */
|
||||
const previewDialogRef = ref()
|
||||
const handlePreview = (row: EvaluationReport) => {
|
||||
previewDialogRef.value?.open(row.id!)
|
||||
}
|
||||
|
||||
/** 编辑报告 */
|
||||
const handleEdit = (row: EvaluationReport) => {
|
||||
router.push({
|
||||
|
||||
@ -77,14 +77,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { EvaluationTemplateApi } from '@/api/prison/evaluation'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
|
||||
defineOptions({ name: 'EvaluationTemplateForm' })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 保存当前编辑的模板ID,用于刷新数据
|
||||
const currentTemplateId = ref<number | undefined>(undefined)
|
||||
|
||||
|
||||
@ -122,14 +122,6 @@
|
||||
>
|
||||
编辑
|
||||
</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
|
||||
@ -161,9 +153,6 @@
|
||||
|
||||
<!-- 新建报告对话框 -->
|
||||
<CreateReportDialog ref="createDialogRef" @success="loadReports" />
|
||||
|
||||
<!-- 导出报告对话框 -->
|
||||
<CreateReportOutput ref="exportDialogRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -174,7 +163,6 @@ 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' })
|
||||
|
||||
@ -200,7 +188,6 @@ const reportList = ref<ReportVO[]>([])
|
||||
const selectedPrisoner = ref<PrisonerVO | null>(null)
|
||||
|
||||
const editDrawerRef = ref()
|
||||
const exportDialogRef = ref()
|
||||
const createDialogRef = ref()
|
||||
|
||||
/** 获取服刑人员列表 */
|
||||
@ -269,11 +256,6 @@ 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 {
|
||||
|
||||
@ -1,355 +0,0 @@
|
||||
<template>
|
||||
<Dialog style="display: none" :title="'评估报告'" v-model="dialogVisible" width="900px">
|
||||
<div v-loading="loading" class="report-edit-container" ref="previewRef">
|
||||
<template v-if="selectedReport">
|
||||
<div class="basic-info-title">{{ selectedReport.templateName }}</div>
|
||||
<!-- 基本信息区 -->
|
||||
<div class="basic-info-section">
|
||||
<div class="basic-info-item"
|
||||
>服刑人员:{{ selectedReport.prisonerName }} ({{ selectedReport.prisonerNo }})</div
|
||||
>
|
||||
<div class="basic-info-item">监区:{{ selectedReport.areaName || '-' }}</div>
|
||||
<div class="basic-info-item"
|
||||
>评估日期:{{ formatDateTime(selectedReport.evaluationDate, 'YYYY-MM-DD') }}</div
|
||||
>
|
||||
<div class="basic-info-item"
|
||||
>风险等级:{{
|
||||
getDictLabel(DICT_TYPE.PRISON_RISK_LEVEL, selectedReport.riskLevel)
|
||||
}}</div
|
||||
>
|
||||
<div class="basic-info-item"
|
||||
>状态:{{ getDictLabel(DICT_TYPE.PRISON_REPORT_STATUS, selectedReport.status) }}</div
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in dimensionAnalysisPanelRef"
|
||||
:key="item.id"
|
||||
class="dimension-item"
|
||||
>
|
||||
<div class="dimension-item-title"
|
||||
>{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}、{{
|
||||
item.name
|
||||
}}</div
|
||||
>
|
||||
<div style="white-space: pre-line; line-height: 1.5">{{
|
||||
// 移除所有以 '##' 开头、并以两个连续换行结尾的标题块,兼容 CRLF/LF
|
||||
item.aiAnalysis?.replace(/(^|\r?\n)##.*?\r?\n\r?\n/gm, '$1')
|
||||
}}</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)
|
||||
// 等待视图渲染完成,避免导出时 DOM 仍为空
|
||||
await nextTick()
|
||||
try {
|
||||
await 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>
|
||||
/* 使用 pt 单位以避免 Word/WPS 在 px->pt 换算时产生差异 */
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.basic-info-title{
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
color: black;
|
||||
text-align: center;
|
||||
margin-bottom: 15pt;
|
||||
}
|
||||
.basic-info-section {
|
||||
color: black;
|
||||
font-size: 11pt;
|
||||
}
|
||||
.basic-info-item{
|
||||
margin-right: 25pt;
|
||||
}
|
||||
|
||||
.dimension-item {
|
||||
font-size: 11pt;
|
||||
color: black;
|
||||
}
|
||||
.dimension-item-title {
|
||||
font-size: 15pt;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
margin-top: 15pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 转换为Blob (asBlob返回Promise)
|
||||
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-item {
|
||||
padding: 0 40px;
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
}
|
||||
.dimension-item-title {
|
||||
font-size: 15px;
|
||||
padding: 15px 0;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
@ -163,13 +163,12 @@ const open = async (type: string, templateId: number, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
formData.value.templateId = templateId
|
||||
resetForm()
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const existingDimension = await DimensionApi.getDimension(id)
|
||||
formData.value = { ...existingDimension, templateId }
|
||||
formData.value = await DimensionApi.getDimension(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
<!-- 维度配置 Tab -->
|
||||
<el-tab-pane label="维度配置" name="dimension">
|
||||
<div class="dimension-header mb-15px">
|
||||
<el-button type="primary" @click="openDimensionForm(selectedTemplate.id!)" v-hasPermi="['prison:evaluation-report:template:update']">
|
||||
<el-button type="primary" @click="openDimensionForm(selectedTemplate.id!)" v-hasPermi="['prison:evaluation-report:dimension:create']">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增维度
|
||||
</el-button>
|
||||
</div>
|
||||
@ -111,7 +111,7 @@
|
||||
<el-button type="primary" link size="small" @click="openDimensionForm(selectedTemplate.id!, dimension.id)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDeleteDimension(dimension.id!)" v-hasPermi="['prison:evaluation-report:template:update']">
|
||||
<el-button type="danger" link size="small" @click="handleDeleteDimension(dimension.id!)" v-hasPermi="['prison:evaluation-report:dimension:delete']">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<el-empty description="问卷题目管理功能已整合到问卷管理中" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({ name: 'PrisonQuestion' })
|
||||
</script>
|
||||
@ -1,442 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="`代为填写 - ${prisonerInfo?.prisonerName || ''}`"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="loading">
|
||||
<!-- 罪犯信息 -->
|
||||
<el-descriptions :column="3" border class="mb-20px">
|
||||
<el-descriptions-item label="罪犯编号">{{ prisonerInfo?.prisonerNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="罪犯姓名">{{ prisonerInfo?.prisonerName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="监区">{{ prisonerInfo?.areaName }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 问卷题目 -->
|
||||
<div v-if="questions.length > 0" class="questionnaire-content">
|
||||
<div
|
||||
v-for="(question, index) in questions"
|
||||
:key="question.id"
|
||||
class="question-item"
|
||||
>
|
||||
<div class="question-header">
|
||||
<span class="question-index">{{ index + 1 }}</span>
|
||||
<span class="question-title">{{ question.title }}</span>
|
||||
<el-tag v-if="question.required" type="danger" size="small" class="required-tag">必填</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 单选题 -->
|
||||
<div v-if="question.type === 1" class="answer-area">
|
||||
<div class="options-container">
|
||||
<el-radio-group v-model="answers[question.id]" :disabled="disabled">
|
||||
<el-radio
|
||||
v-for="(opt, optIdx) in parseOptions(question.options)"
|
||||
:key="optIdx"
|
||||
:value="optIdx"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<!-- 其他选项输入框 -->
|
||||
<div v-if="getOtherOptionIndex(question) !== undefined && answers[question.id] === getOtherOptionIndex(question)" class="other-input">
|
||||
<el-input
|
||||
v-model="otherAnswers[question.id]"
|
||||
placeholder="请输入其他选项内容"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 多选题 -->
|
||||
<div v-else-if="question.type === 2" class="answer-area">
|
||||
<div class="options-container">
|
||||
<el-checkbox-group v-model="multiAnswers[question.id]">
|
||||
<el-checkbox
|
||||
v-for="(opt, optIdx) in parseOptions(question.options)"
|
||||
:key="optIdx"
|
||||
:value="optIdx"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<!-- 其他选项输入框 -->
|
||||
<div
|
||||
v-if="isOtherOptionSelected(question, multiAnswers[question.id])"
|
||||
class="other-input"
|
||||
>
|
||||
<el-input
|
||||
v-model="otherAnswers[question.id]"
|
||||
placeholder="请输入其他选项内容"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 填空题 -->
|
||||
<div v-else-if="question.type === 3" class="answer-area">
|
||||
<el-input
|
||||
v-model="answers[question.id]"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入答案"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 评分题 -->
|
||||
<div v-else-if="question.type === 4" class="answer-area">
|
||||
<el-rate v-model="answers[question.id]" :disabled="disabled" />
|
||||
</div>
|
||||
|
||||
<!-- 日期题 -->
|
||||
<div v-else-if="question.type === 5" class="answer-area">
|
||||
<el-date-picker
|
||||
v-model="answers[question.id]"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 数字题 -->
|
||||
<div v-else-if="question.type === 6" class="answer-area">
|
||||
<el-input-number v-model="answers[question.id]" :min="0" :disabled="disabled" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="暂无问卷题目" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确认代填
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { QuestionApi } from '@/api/prison/question'
|
||||
import { QuestionnaireRecordApi } from '@/api/prison/questionnairerecord'
|
||||
|
||||
defineOptions({ name: 'AgentFillDialog' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
|
||||
// 罪犯信息
|
||||
const prisonerInfo = ref<any>(null)
|
||||
const recordId = ref<number>()
|
||||
const questionnaireId = ref<number>()
|
||||
|
||||
// 问卷题目
|
||||
const questions = ref<any[]>([])
|
||||
|
||||
// 答案
|
||||
const answers = reactive<Record<number, any>>({})
|
||||
const multiAnswers = reactive<Record<number, number[]>>({})
|
||||
const otherAnswers = reactive<Record<number, string>>({}) // 其他选项输入框的答案
|
||||
|
||||
// 是否禁用
|
||||
const disabled = computed(() => submitLoading.value)
|
||||
|
||||
/** 解析选项JSON */
|
||||
const parseOptions = (optionsStr: string) => {
|
||||
try {
|
||||
if (!optionsStr) return []
|
||||
return JSON.parse(optionsStr)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取其他选项的索引 */
|
||||
const getOtherOptionIndex = (question: any): number | undefined => {
|
||||
const options = parseOptions(question.options)
|
||||
return options.findIndex((opt: any) => opt.isOther === true)
|
||||
}
|
||||
|
||||
/** 检查多选题的其他选项是否被选中 */
|
||||
const isOtherOptionSelected = (question: any, selectedIndices: number[] | undefined): boolean => {
|
||||
const otherIdx = getOtherOptionIndex(question)
|
||||
if (otherIdx === undefined || otherIdx === -1) return false
|
||||
return selectedIndices?.includes(otherIdx) ?? false
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (record: any) => {
|
||||
console.log('=== 代填弹窗打开 ===')
|
||||
console.log('传入的record:', record)
|
||||
console.log('record.questionnaireId:', record.questionnaireId)
|
||||
|
||||
dialogVisible.value = true
|
||||
recordId.value = record.id
|
||||
prisonerInfo.value = record
|
||||
questionnaireId.value = record.questionnaireId
|
||||
|
||||
console.log('设置的questionnaireId.value:', questionnaireId.value)
|
||||
|
||||
// 重置答案
|
||||
Object.keys(answers).forEach(key => delete answers[key])
|
||||
Object.keys(multiAnswers).forEach(key => delete multiAnswers[key])
|
||||
Object.keys(otherAnswers).forEach(key => delete otherAnswers[key])
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
// 从后端重新获取最新的记录信息,确保状态是最新的
|
||||
const latestRecord = await QuestionnaireRecordApi.getQuestionnaireRecord(record.id)
|
||||
console.log('获取的最新记录状态:', latestRecord.status)
|
||||
prisonerInfo.value = { ...record, ...latestRecord }
|
||||
|
||||
// 先获取问卷题目总数
|
||||
const countData = await QuestionApi.getQuestionnaireQuestionList(
|
||||
{ questionnaireId: questionnaireId.value, pageNo: 1, pageSize: 1 }
|
||||
)
|
||||
console.log('获取的countData:', countData)
|
||||
const totalCount = countData.total || 0
|
||||
console.log('题目总数:', totalCount)
|
||||
|
||||
// 根据总数获取所有题目(后端限制每页最大200条)
|
||||
const maxPageSize = 200
|
||||
const pageSize = Math.min(totalCount, maxPageSize)
|
||||
|
||||
const questionData = await QuestionApi.getQuestionnaireQuestionList(
|
||||
{ questionnaireId: questionnaireId.value, pageNo: 1, pageSize: pageSize }
|
||||
)
|
||||
console.log('获取的questionData:', questionData)
|
||||
console.log('实际获取到的题目数量:', questionData.list?.length || 0)
|
||||
questions.value = questionData.list || []
|
||||
|
||||
// 初始化答案结构
|
||||
questions.value.forEach(q => {
|
||||
if (q.type === 2) {
|
||||
// 多选题初始化为空数组
|
||||
multiAnswers[q.id] = []
|
||||
} else {
|
||||
answers[q.id] = q.type === 4 ? 0 : null // 评分题默认0分
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('获取问卷题目失败', e)
|
||||
ElMessage.error('获取问卷题目失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交答案 */
|
||||
const handleSubmit = async () => {
|
||||
// 校验必填
|
||||
for (const question of questions.value) {
|
||||
if (question.required) {
|
||||
const answer = question.type === 2 ? multiAnswers[question.id] : answers[question.id]
|
||||
if (answer === null || answer === undefined ||
|
||||
(Array.isArray(answer) && answer.length === 0) ||
|
||||
(typeof answer === 'string' && !answer.trim())) {
|
||||
ElMessage.warning(`第${questions.value.indexOf(question) + 1}题为必填项`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建答案列表
|
||||
const answerList = questions.value.map(q => {
|
||||
const answerItem: any = {
|
||||
questionId: q.id
|
||||
}
|
||||
|
||||
const options = parseOptions(q.options)
|
||||
const otherIndex = getOtherOptionIndex(q)
|
||||
const hasOtherSelected = otherIndex !== undefined && (
|
||||
(q.type === 1 && answers[q.id] === otherIndex) ||
|
||||
(q.type === 2 && multiAnswers[q.id]?.includes(otherIndex))
|
||||
)
|
||||
|
||||
if (q.type === 2) {
|
||||
// 多选题:直接使用选项索引作为 optionIds(纯数字)
|
||||
answerItem.optionIds = multiAnswers[q.id] || []
|
||||
// 拼接所有选中的选项文字到 answer 字段
|
||||
const selectedIndices = multiAnswers[q.id] || []
|
||||
const selectedLabels = selectedIndices.map((idx: number) => {
|
||||
const opt = options[idx]
|
||||
return opt ? (opt.value !== undefined ? opt.value : opt.label) : String(idx)
|
||||
})
|
||||
if (selectedLabels.length > 0) {
|
||||
answerItem.answer = selectedLabels.join('、')
|
||||
}
|
||||
} else if (q.type === 1) {
|
||||
// 单选题:检查是否选择了"其他"选项
|
||||
const selectedIndex = answers[q.id]
|
||||
const otherIndex = getOtherOptionIndex(q)
|
||||
const isOtherSelected = otherIndex !== undefined && selectedIndex === otherIndex
|
||||
|
||||
if (isOtherSelected) {
|
||||
// 选择了"其他"选项,需要发送 optionIds
|
||||
answerItem.optionIds = [selectedIndex]
|
||||
}
|
||||
// 普通单选题不发送 optionIds
|
||||
}
|
||||
// 填空题、评分题、日期题、数字题不发送 optionIds
|
||||
|
||||
// 如果选中了其他选项,将其他输入框的内容通过 answerText 传递
|
||||
if (hasOtherSelected && otherAnswers[q.id]) {
|
||||
const otherOptValue = options[otherIndex] ? (options[otherIndex].value !== undefined ? options[otherIndex].value : options[otherIndex].label) : '其他'
|
||||
const otherText = `${otherOptValue}:${otherAnswers[q.id]}`
|
||||
if (q.type === 2) {
|
||||
// 多选题:追加其他选项的文字(前面已经有选中选项的文字了)
|
||||
answerItem.answer = answerItem.answer
|
||||
? `${answerItem.answer}、${otherText}`
|
||||
: otherText
|
||||
} else {
|
||||
// 单选题:直接设置其他选项的文字
|
||||
answerItem.answer = otherText
|
||||
}
|
||||
} else if (q.type === 2 && answerItem.answer) {
|
||||
// 多选题选中了"其他"选项但没有输入文字,也要显示"其他"选项的文字
|
||||
if (hasOtherSelected) {
|
||||
const otherOptValue = options[otherIndex] ? (options[otherIndex].value !== undefined ? options[otherIndex].value : options[otherIndex].label) : '其他'
|
||||
answerItem.answer = `${answerItem.answer}、${otherOptValue}`
|
||||
}
|
||||
} else if (q.type === 3 || q.type === 4 || q.type === 5 || q.type === 6) {
|
||||
// 填空题、评分题、日期题、数字题直接使用 answerText
|
||||
answerItem.answer = answers[q.id] !== undefined ? String(answers[q.id]) : ''
|
||||
} else if (q.type === 1 && !hasOtherSelected) {
|
||||
// 单选题没有选择其他选项时,将选项值设置为 answerText
|
||||
const selectedIndex = answers[q.id]
|
||||
if (selectedIndex !== undefined && selectedIndex !== null) {
|
||||
const opt = options[selectedIndex]
|
||||
answerItem.answer = opt ? (opt.value !== undefined ? opt.value : opt.label) : String(selectedIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return answerItem
|
||||
})
|
||||
|
||||
// 根据记录状态决定是否需要先开始测评
|
||||
const needStartAssessment = prisonerInfo.value?.status === 1 // 只有"待测评"状态需要先开始测评
|
||||
|
||||
if (needStartAssessment) {
|
||||
try {
|
||||
await QuestionnaireRecordApi.startAssessment(recordId.value!, prisonerInfo.value.prisonerId)
|
||||
} catch (e) {
|
||||
console.error('开始测评失败', e)
|
||||
ElMessage.error('开始测评失败')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 提交答案
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await QuestionnaireRecordApi.submitAnswerByAgent({
|
||||
recordId: recordId.value!,
|
||||
prisonerId: prisonerInfo.value.prisonerId,
|
||||
answers: answerList
|
||||
})
|
||||
ElMessage.success('代填成功')
|
||||
dialogVisible.value = false
|
||||
// 触发成功回调
|
||||
emit('success')
|
||||
} catch (e) {
|
||||
console.error('提交失败', e)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.questionnaire-content {
|
||||
overflow-y: auto;
|
||||
max-height: 55vh;
|
||||
padding-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.question-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.question-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.question-index {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.answer-area {
|
||||
padding-left: 32px;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-radio),
|
||||
:deep(.el-checkbox) {
|
||||
margin-right: 0;
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:deep(.el-radio__label),
|
||||
:deep(.el-checkbox__label) {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.other-input {
|
||||
margin-top: 12px;
|
||||
margin-left: 24px;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,336 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑任务' : '创建问卷任务'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<el-divider content-position="left">基本信息</el-divider>
|
||||
|
||||
<el-form-item label="任务名称" prop="taskName">
|
||||
<el-input v-model="formData.taskName" placeholder="请输入任务名称" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择问卷" prop="questionnaireId">
|
||||
<el-select
|
||||
v-model="formData.questionnaireId"
|
||||
placeholder="请选择问卷"
|
||||
filterable
|
||||
style="width: calc(100% - 90px)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in questionnaireList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="!formData.questionnaireId"
|
||||
@click="handlePreviewQuestionnaire"
|
||||
>
|
||||
<Icon icon="ep:view" class="mr-3px" />预览
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 目标范围 -->
|
||||
<el-divider content-position="left">目标范围</el-divider>
|
||||
|
||||
<el-form-item label="目标类型" prop="targetType">
|
||||
<el-radio-group v-model="formData.targetType">
|
||||
<el-radio :value="1">指定犯人</el-radio>
|
||||
<el-radio :value="2">指定监区</el-radio>
|
||||
<el-radio :value="3">全部犯人</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 指定犯人 -->
|
||||
<el-form-item v-if="formData.targetType === 1" label="选择犯人" prop="prisonerIds">
|
||||
<el-button type="primary" plain @click="openPrisonerSelector">
|
||||
<Icon icon="ep:user" class="mr-5px" />
|
||||
选择犯人(已选 {{ formData.prisonerIds.length }} 人)
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 指定监区 -->
|
||||
<el-form-item v-if="formData.targetType === 2" label="选择监区" prop="areaId">
|
||||
<el-select
|
||||
v-model="formData.areaId"
|
||||
placeholder="请选择监区"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in areaList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 时间设置 -->
|
||||
<el-divider content-position="left">时间设置</el-divider>
|
||||
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-date-picker
|
||||
v-model="formData.startTime"
|
||||
type="datetime"
|
||||
placeholder="选择开始时间"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="截止时间" prop="deadline">
|
||||
<el-date-picker
|
||||
v-model="formData.deadline"
|
||||
type="datetime"
|
||||
placeholder="选择截止时间"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-divider content-position="left">备注</el-divider>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 犯人选择器弹窗 -->
|
||||
<PrisonerSelectorDialog
|
||||
ref="prisonerSelectorRef"
|
||||
:selected-ids="formData.prisonerIds"
|
||||
@confirm="handlePrisonerSelect"
|
||||
/>
|
||||
|
||||
<!-- 问卷预览弹窗 -->
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="问卷预览"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<QuestionnairePreview :id="previewQuestionnaireId" />
|
||||
<template #footer>
|
||||
<el-button @click="previewVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import PrisonerSelectorDialog from './PrisonerSelectorDialog.vue'
|
||||
import QuestionnairePreview from '@/views/prison/questionnaire/components/QuestionnairePreview.vue'
|
||||
import { QuestionnaireTaskApi } from '@/api/prison/questionnaire-task'
|
||||
import { QuestionnaireApi } from '@/api/prison/questionnaire'
|
||||
import { AreaApi } from '@/api/prison/area'
|
||||
|
||||
defineOptions({ name: 'CreateTaskDialog' })
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
// 预览相关
|
||||
const previewVisible = ref(false)
|
||||
const previewQuestionnaireId = ref<number>()
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: undefined,
|
||||
taskName: '',
|
||||
questionnaireId: undefined as number | undefined,
|
||||
targetType: 2, // 默认指定监区
|
||||
prisonerIds: [] as number[],
|
||||
areaId: undefined as number | undefined,
|
||||
startTime: '',
|
||||
deadline: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单校验规则
|
||||
const formRules = {
|
||||
taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||||
questionnaireId: [{ required: true, message: '请选择问卷', trigger: 'change' }],
|
||||
targetType: [{ required: true, message: '请选择目标类型', trigger: 'change' }],
|
||||
deadline: [{ required: true, message: '请选择截止时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 数据列表
|
||||
const questionnaireList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
const prisonerSelectorRef = ref()
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (row?: any) => {
|
||||
dialogVisible.value = true
|
||||
isEdit.value = !!row
|
||||
|
||||
if (row) {
|
||||
// 编辑模式
|
||||
formData.id = row.id
|
||||
formData.taskName = row.taskName
|
||||
formData.questionnaireId = row.questionnaireId
|
||||
formData.targetType = row.targetType
|
||||
formData.areaId = row.areaId
|
||||
formData.startTime = row.startTime || ''
|
||||
formData.deadline = row.deadline || ''
|
||||
formData.remark = row.remark || ''
|
||||
} else {
|
||||
// 重置表单
|
||||
formData.id = undefined
|
||||
formData.taskName = ''
|
||||
formData.questionnaireId = undefined
|
||||
formData.targetType = 2
|
||||
formData.prisonerIds = []
|
||||
formData.areaId = undefined
|
||||
formData.startTime = ''
|
||||
formData.deadline = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
await Promise.all([getQuestionnaires(), getAreas()])
|
||||
}
|
||||
|
||||
/** 获取问卷列表 */
|
||||
const getQuestionnaires = async () => {
|
||||
try {
|
||||
const data = await QuestionnaireApi.getQuestionnairePage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
status: 2
|
||||
})
|
||||
questionnaireList.value = data.list || []
|
||||
} catch (e) {
|
||||
console.error('获取问卷列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取监区列表 */
|
||||
const getAreas = async () => {
|
||||
try {
|
||||
const data = await AreaApi.getAreaTree({})
|
||||
// 提取所有监区用于下拉选择
|
||||
const extractAreas = (nodes: any[]): any[] => {
|
||||
const result: any[] = []
|
||||
nodes.forEach(node => {
|
||||
result.push({ id: node.id, name: node.name })
|
||||
if (node.children && node.children.length > 0) {
|
||||
result.push(...extractAreas(node.children))
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
areaList.value = extractAreas(data || [])
|
||||
} catch (e) {
|
||||
console.error('获取监区列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开犯人选择器 */
|
||||
const openPrisonerSelector = () => {
|
||||
prisonerSelectorRef.value?.open(formData.prisonerIds)
|
||||
}
|
||||
|
||||
/** 预览问卷 */
|
||||
const handlePreviewQuestionnaire = () => {
|
||||
if (!formData.questionnaireId) {
|
||||
ElMessage.warning('请先选择问卷')
|
||||
return
|
||||
}
|
||||
previewQuestionnaireId.value = formData.questionnaireId
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
/** 犯人选择确认 */
|
||||
const handlePrisonerSelect = (selectedIds: number[]) => {
|
||||
formData.prisonerIds = selectedIds
|
||||
}
|
||||
|
||||
/** 提交 */
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const data = {
|
||||
taskName: formData.taskName,
|
||||
questionnaireId: formData.questionnaireId,
|
||||
targetType: formData.targetType,
|
||||
prisonerIds: formData.targetType === 1 ? formData.prisonerIds : undefined,
|
||||
areaId: formData.targetType === 2 ? formData.areaId : undefined,
|
||||
startTime: formData.startTime || undefined,
|
||||
deadline: formData.deadline,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await QuestionnaireTaskApi.updateQuestionnaireTask({
|
||||
id: formData.id,
|
||||
taskName: formData.taskName,
|
||||
deadline: formData.deadline,
|
||||
remark: formData.remark
|
||||
})
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await QuestionnaireTaskApi.createQuestionnaireTask(data)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} catch (e) {
|
||||
console.error('提交失败', e)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 监听目标类型变化,重置相关字段
|
||||
watch(() => formData.targetType, (val) => {
|
||||
if (val === 1) {
|
||||
formData.areaId = undefined
|
||||
} else if (val === 2) {
|
||||
formData.prisonerIds = []
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@ -1,200 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="选择犯人"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<!-- 搜索区域 -->
|
||||
<el-form :model="queryParams" :inline="true" class="mb-20px">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="queryParams.name" placeholder="请输入姓名" clearable style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="编号">
|
||||
<el-input v-model="queryParams.prisonerNo" placeholder="请输入编号" clearable style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="监区">
|
||||
<el-select v-model="queryParams.areaId" placeholder="请选择监区" clearable style="width: 150px">
|
||||
<el-option v-for="item in areaList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
|
||||
<el-button @click="handleReset"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
@selection-change="handleSelectionChange"
|
||||
:row-key="(row: any) => row.id"
|
||||
>
|
||||
<el-table-column type="selection" width="55" :reserve-selection="true" />
|
||||
<el-table-column label="编号" align="center" prop="prisonerNo" width="100" />
|
||||
<el-table-column label="姓名" align="center" prop="name" width="100" />
|
||||
<el-table-column label="监区" align="center" prop="prisonAreaName" width="120" />
|
||||
<el-table-column label="性别" align="center" prop="gender" width="80">
|
||||
<template #default="scope">
|
||||
{{ scope.row.gender === 1 ? '男' : '女' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入狱日期" align="center" prop="prisonDate" width="120">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.prisonDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.status === 1 ? '在押' : '释放' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<!-- 已选统计 -->
|
||||
<div class="mt-20px">
|
||||
<el-tag type="primary" size="large">已选择 {{ selectedList.length }} 人</el-tag>
|
||||
<el-button v-if="selectedList.length > 0" type="warning" text @click="clearSelection" class="ml-10px">
|
||||
清空选择
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleConfirm">
|
||||
确定(已选 {{ selectedList.length }} 人)
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { PrisonerApi } from '@/api/prison/prisoner'
|
||||
import { AreaApi } from '@/api/prison/area'
|
||||
|
||||
defineOptions({ name: 'PrisonerSelectorDialog' })
|
||||
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const tableRef = ref()
|
||||
const list = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const selectedList = ref<any[]>([])
|
||||
const areaList = ref<any[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: '',
|
||||
prisonerNo: '',
|
||||
areaId: undefined as number | undefined,
|
||||
status: 1 // 只查在押犯人
|
||||
})
|
||||
|
||||
// 当前选中的ID列表(用于回显)
|
||||
const currentSelectedIds = ref<number[]>([])
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = (selectedIds: number[]) => {
|
||||
dialogVisible.value = true
|
||||
currentSelectedIds.value = [...selectedIds]
|
||||
selectedList.value = []
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
getAreas()
|
||||
}
|
||||
|
||||
/** 获取列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await PrisonerApi.getPrisonerPage(queryParams)
|
||||
list.value = data.list || []
|
||||
total.value = data.total
|
||||
|
||||
// 回显已选中的行
|
||||
nextTick(() => {
|
||||
currentSelectedIds.value.forEach(id => {
|
||||
const row = list.value.find((item: any) => item.id === id)
|
||||
if (row) {
|
||||
tableRef.value?.toggleRowSelection(row, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取监区列表 */
|
||||
const getAreas = async () => {
|
||||
try {
|
||||
const data = await AreaApi.getAreaTree({})
|
||||
// 提取所有监区用于下拉选择
|
||||
const extractAreas = (nodes: any[]): any[] => {
|
||||
const result: any[] = []
|
||||
nodes.forEach(node => {
|
||||
result.push({ id: node.id, name: node.name })
|
||||
if (node.children && node.children.length > 0) {
|
||||
result.push(...extractAreas(node.children))
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
areaList.value = extractAreas(data || [])
|
||||
} catch (e) {
|
||||
console.error('获取监区列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
const handleSearch = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
const handleReset = () => {
|
||||
queryParams.name = ''
|
||||
queryParams.prisonerNo = ''
|
||||
queryParams.areaId = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
/** 选择变化 */
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectedList.value = rows
|
||||
}
|
||||
|
||||
/** 清空选择 */
|
||||
const clearSelection = () => {
|
||||
selectedList.value = []
|
||||
tableRef.value?.clearSelection()
|
||||
}
|
||||
|
||||
/** 确认选择 */
|
||||
const handleConfirm = () => {
|
||||
const ids = selectedList.value.map(row => row.id)
|
||||
emit('confirm', ids)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@ -1,708 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="taskDetail?.taskName || '任务详情'"
|
||||
width="1100px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-loading="loading">
|
||||
<!-- 基本信息 -->
|
||||
<el-descriptions title="基本信息" :column="3" border class="mb-20px">
|
||||
<el-descriptions-item label="任务ID">{{ taskDetail?.taskId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务名称">{{ taskDetail?.taskName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="问卷名称">{{ taskDetail?.questionnaireName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusTag(taskDetail?.status)">
|
||||
{{ getStatusLabel(taskDetail?.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="开始时间">{{ formatDateTime(taskDetail?.startTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="截止时间">{{ formatDateTime(taskDetail?.deadline) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<el-tabs v-model="activeTab" class="task-tabs" @tab-change="handleTabChange">
|
||||
<!-- 任务概览 -->
|
||||
<el-tab-pane label="任务概览" name="overview">
|
||||
<!-- 完成进度 -->
|
||||
<el-card class="mb-20px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>完成进度</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="progress-item">
|
||||
<div class="progress-value">{{ taskProgress?.totalCount || 0 }}</div>
|
||||
<div class="progress-label">目标人数</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="progress-item">
|
||||
<div class="progress-value text-success">{{ taskProgress?.completedCount || 0 }}</div>
|
||||
<div class="progress-label">已完成</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="progress-item">
|
||||
<div class="progress-value text-warning">{{ taskProgress?.pendingCount || 0 }}</div>
|
||||
<div class="progress-label">待完成</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="progress-item">
|
||||
<div class="progress-value text-primary">{{ taskProgress?.completionRate }}%</div>
|
||||
<div class="progress-label">完成率</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="mt-20px">
|
||||
<el-progress
|
||||
:percentage="Number(taskProgress?.completionRate) || 0"
|
||||
:stroke-width="20"
|
||||
status="success"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 状态分布 -->
|
||||
<el-row :gutter="20" class="mt-20px">
|
||||
<el-col :span="8">
|
||||
<el-statistic title="待测评" :value="taskProgress?.statusBreakdown?.pending || 0">
|
||||
<template #suffix>
|
||||
<span class="text-gray">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-statistic title="测评中" :value="taskProgress?.statusBreakdown?.inProgress || 0">
|
||||
<template #suffix>
|
||||
<span class="text-gray">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-statistic title="已完成" :value="taskProgress?.statusBreakdown?.completed || 0">
|
||||
<template #suffix>
|
||||
<span class="text-gray">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 按监区统计 -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>按监区统计</span>
|
||||
<el-button
|
||||
v-if="taskProgress?.pendingCount > 0"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleRemind"
|
||||
:loading="remindLoading"
|
||||
>
|
||||
提醒未完成
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="areaStatistics" stripe>
|
||||
<el-table-column label="监区" align="center" prop="areaName" width="120" />
|
||||
<el-table-column label="目标人数" align="center" prop="totalCount" width="100" />
|
||||
<el-table-column label="已完成" align="center" prop="completedCount" width="100">
|
||||
<template #default="scope">
|
||||
<span class="text-success">{{ scope.row.completedCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成率" align="center" width="150">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="Number(scope.row.completionRate) || 0"
|
||||
:stroke-width="6"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平均分" align="center" prop="avgScore" width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.avgScore || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="及格率" align="center" prop="passRate" width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.passRate ? scope.row.passRate + '%' : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="风险分布" align="center" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-tag type="danger" size="small" class="mr-5px">
|
||||
高{{ scope.row.riskDistribution?.highRisk || 0 }}
|
||||
</el-tag>
|
||||
<el-tag type="warning" size="small" class="mr-5px">
|
||||
中{{ scope.row.riskDistribution?.mediumRisk || 0 }}
|
||||
</el-tag>
|
||||
<el-tag type="success" size="small">
|
||||
低{{ scope.row.riskDistribution?.lowRisk || 0 }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 人员填写进度 -->
|
||||
<el-tab-pane label="人员填写进度" name="prisoners">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>人员填写进度列表</span>
|
||||
<div class="filter-actions">
|
||||
<el-select
|
||||
v-model="prisonerFilter.status"
|
||||
placeholder="填写状态"
|
||||
clearable
|
||||
size="small"
|
||||
class="filter-select"
|
||||
@change="handlePrisonerFilterChange"
|
||||
>
|
||||
<el-option label="全部" :value="undefined" />
|
||||
<el-option label="待测评" :value="1" />
|
||||
<el-option label="测评中" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已取消" :value="4" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="prisonerFilter.areaId"
|
||||
placeholder="监区"
|
||||
clearable
|
||||
size="small"
|
||||
class="filter-select"
|
||||
@change="handlePrisonerFilterChange"
|
||||
>
|
||||
<el-option label="全部" :value="undefined" />
|
||||
<el-option
|
||||
v-for="area in areaStatistics"
|
||||
:key="area.areaId"
|
||||
:label="area.areaName"
|
||||
:value="area.areaId ?? ''"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="prisonerFilter.riskLevel"
|
||||
placeholder="风险等级"
|
||||
clearable
|
||||
size="small"
|
||||
class="filter-select"
|
||||
@change="handlePrisonerFilterChange"
|
||||
>
|
||||
<el-option label="全部" :value="undefined" />
|
||||
<el-option label="低风险" :value="1" />
|
||||
<el-option label="中风险" :value="2" />
|
||||
<el-option label="高风险" :value="3" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 统计摘要 -->
|
||||
<el-row :gutter="20" class="mb-20px">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="总人数" :value="filteredPrisoners.length">
|
||||
<template #suffix>
|
||||
<span class="text-gray">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="待测评" :value="prisonerStats.pending">
|
||||
<template #suffix>
|
||||
<span class="text-gray">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="测评中" :value="prisonerStats.inProgress">
|
||||
<template #suffix>
|
||||
<span class="text-gray">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="已完成" :value="prisonerStats.completed">
|
||||
<template #suffix>
|
||||
<span class="text-gray">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 人员列表 -->
|
||||
<el-table :data="filteredPrisoners" stripe v-loading="prisonersLoading">
|
||||
<el-table-column label="罪犯编号" align="center" prop="prisonerNo" width="120" />
|
||||
<el-table-column label="罪犯姓名" align="center" prop="prisonerName" width="100" />
|
||||
<el-table-column label="监区" align="center" prop="areaName" width="120" />
|
||||
<el-table-column label="填写状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTag(scope.row.status)" size="small">
|
||||
{{ getStatusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客观分" align="center" prop="objectiveScore" width="90" />
|
||||
<el-table-column label="主观分" align="center" prop="subjectiveScore" width="90" />
|
||||
<el-table-column label="总分" align="center" prop="totalScore" width="90" />
|
||||
<el-table-column label="风险等级" align="center" prop="riskLevel" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.riskLevel" :type="getRiskTag(scope.row.riskLevel)" size="small">
|
||||
{{ getRiskLabel(scope.row.riskLevel) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="答题用时" align="center" prop="duration" width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.duration ? formatDuration(scope.row.duration) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成时间" align="center" prop="finishTime" width="160">
|
||||
<template #default="scope">
|
||||
{{ scope.row.finishTime ? formatDateTime(scope.row.finishTime) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="400" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 3"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleViewAnswer(scope.row)"
|
||||
>
|
||||
查看答案
|
||||
</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
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleNotifyPrisoner(scope.row)"
|
||||
>
|
||||
通知
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 1 || scope.row.status === 2"
|
||||
link
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleAgentFill(scope.row)"
|
||||
v-hasPermi="['prison:questionnaire-record:agent-fill']"
|
||||
>
|
||||
代填
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === 2 || scope.row.status === 3"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
@click="handleResetRecord(scope.row)"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleViewPrisoner(scope.row)"
|
||||
v-hasPermi="['prison:prisoner:query']"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 答案详情弹窗 -->
|
||||
<AnswerDetailDialog ref="answerDetailDialogRef" />
|
||||
|
||||
<!-- 代填弹窗 -->
|
||||
<AgentFillDialog ref="agentFillDialogRef" @success="loadPrisonerProgress" />
|
||||
|
||||
<!-- 服刑人员详情弹窗 -->
|
||||
<PrisonerDetail ref="prisonerDetailRef" />
|
||||
<!-- 问卷导出弹窗 -->
|
||||
<QuestionnaireOutput ref="outputDialogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatDateTime } from '@/utils/formatTime'
|
||||
import AnswerDetailDialog from '@/views/prison/questionnairerecord/AnswerDetailDialog.vue'
|
||||
import AgentFillDialog from './AgentFillDialog.vue'
|
||||
import PrisonerDetail from '@/views/prison/prisoner/PrisonerDetail.vue'
|
||||
import { QuestionnaireTaskApi } from '@/api/prison/questionnaire-task'
|
||||
import QuestionnaireOutput from '@/views/prison/questionnairerecord/QuestionnaireOutputfile.vue'
|
||||
|
||||
defineOptions({ name: 'TaskDetailDialog' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const remindLoading = ref(false)
|
||||
const prisonersLoading = ref(false)
|
||||
const taskId = ref<number>()
|
||||
const activeTab = ref('overview')
|
||||
|
||||
// 答案详情弹窗
|
||||
const answerDetailDialogRef = ref()
|
||||
|
||||
// 代填弹窗
|
||||
const agentFillDialogRef = ref()
|
||||
|
||||
// 服刑人员详情弹窗
|
||||
const prisonerDetailRef = ref()
|
||||
|
||||
// 任务详情
|
||||
const taskDetail = ref<any>(null)
|
||||
|
||||
// 进度详情
|
||||
const taskProgress = ref<any>(null)
|
||||
|
||||
// 按监区统计
|
||||
const areaStatistics = ref<any[]>([])
|
||||
|
||||
// 人员填写进度列表
|
||||
const prisonerProgressList = ref<any[]>([])
|
||||
|
||||
// 人员筛选条件
|
||||
const prisonerFilter = reactive({
|
||||
status: undefined as number | undefined,
|
||||
areaId: undefined as number | undefined,
|
||||
riskLevel: undefined as number | undefined
|
||||
})
|
||||
|
||||
// 任务状态选项
|
||||
const taskStatusOptions = [
|
||||
{ value: 1, label: '草稿', type: 'info' },
|
||||
{ value: 2, label: '进行中', type: 'primary' },
|
||||
{ value: 3, label: '已结束', type: 'success' },
|
||||
{ value: 4, label: '已取消', type: 'danger' }
|
||||
]
|
||||
|
||||
// 风险等级选项
|
||||
const riskLevelOptions = [
|
||||
{ value: 1, label: '低风险', type: 'success' },
|
||||
{ value: 2, label: '中风险', type: 'warning' },
|
||||
{ value: 3, label: '高风险', type: 'danger' }
|
||||
]
|
||||
|
||||
/** 筛选后的人员列表 */
|
||||
const filteredPrisoners = computed(() => {
|
||||
return prisonerProgressList.value.filter(item => {
|
||||
if (prisonerFilter.status !== undefined && item.status !== prisonerFilter.status) {
|
||||
return false
|
||||
}
|
||||
if (prisonerFilter.areaId !== undefined && item.areaId !== prisonerFilter.areaId) {
|
||||
return false
|
||||
}
|
||||
if (prisonerFilter.riskLevel !== undefined && item.riskLevel !== prisonerFilter.riskLevel) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
/** 人员统计 */
|
||||
const prisonerStats = computed(() => {
|
||||
const stats = {
|
||||
pending: 0,
|
||||
inProgress: 0,
|
||||
completed: 0
|
||||
}
|
||||
filteredPrisoners.value.forEach(item => {
|
||||
if (item.status === 1) stats.pending++
|
||||
else if (item.status === 2) stats.inProgress++
|
||||
else if (item.status === 3) stats.completed++
|
||||
})
|
||||
return stats
|
||||
})
|
||||
|
||||
/** 获取状态标签类型 */
|
||||
const getStatusTag = (status: number | undefined) => {
|
||||
if (!status) return 'info'
|
||||
const item = taskStatusOptions.find(item => item.value === status)
|
||||
return item?.type || 'info'
|
||||
}
|
||||
|
||||
/** 获取状态标签文本 */
|
||||
const getStatusLabel = (status: number | undefined) => {
|
||||
if (!status) return '未知'
|
||||
const item = taskStatusOptions.find(item => item.value === status)
|
||||
return item?.label || '未知'
|
||||
}
|
||||
|
||||
/** 获取风险标签类型 */
|
||||
const getRiskTag = (level: number) => {
|
||||
const item = riskLevelOptions.find(item => item.value === level)
|
||||
return item?.type || 'info'
|
||||
}
|
||||
|
||||
/** 获取风险标签文本 */
|
||||
const getRiskLabel = (level: number) => {
|
||||
const item = riskLevelOptions.find(item => item.value === level)
|
||||
return item?.label || '-'
|
||||
}
|
||||
|
||||
/** 格式化时长 */
|
||||
const formatDuration = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = seconds % 60
|
||||
if (hours > 0) {
|
||||
return `${hours}时${minutes}分${secs}秒`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分${secs}秒`
|
||||
} else {
|
||||
return `${secs}秒`
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
taskId.value = id
|
||||
activeTab.value = 'overview'
|
||||
loading.value = true
|
||||
|
||||
// 清空之前的任务数据
|
||||
prisonerProgressList.value = []
|
||||
|
||||
try {
|
||||
// 并行加载数据
|
||||
const [progressData, areaData] = await Promise.all([
|
||||
QuestionnaireTaskApi.getTaskProgress(id),
|
||||
QuestionnaireTaskApi.getTaskAreaStatistics(id)
|
||||
])
|
||||
|
||||
taskProgress.value = progressData
|
||||
taskDetail.value = {
|
||||
taskId: progressData.taskId,
|
||||
taskName: progressData.taskName,
|
||||
questionnaireName: progressData.questionnaireName,
|
||||
status: progressData.status,
|
||||
startTime: progressData.startTime,
|
||||
deadline: progressData.deadline
|
||||
}
|
||||
areaStatistics.value = areaData || []
|
||||
} catch (e) {
|
||||
console.error('获取任务详情失败', e)
|
||||
ElMessage.error('获取任务详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换到人员标签页时加载人员数据 */
|
||||
const loadPrisonerProgress = async () => {
|
||||
if (!taskId.value) return
|
||||
|
||||
prisonersLoading.value = true
|
||||
try {
|
||||
const data = await QuestionnaireTaskApi.getPrisonerProgress(taskId.value)
|
||||
prisonerProgressList.value = data || []
|
||||
} catch (e) {
|
||||
console.error('获取人员进度失败', e)
|
||||
ElMessage.error('获取人员进度失败')
|
||||
} finally {
|
||||
prisonersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听标签页切换 */
|
||||
const handleTabChange = (tabName: string) => {
|
||||
if (tabName === 'prisoners' && prisonerProgressList.value.length === 0) {
|
||||
loadPrisonerProgress()
|
||||
}
|
||||
}
|
||||
|
||||
/** 筛选条件变化 */
|
||||
const handlePrisonerFilterChange = () => {
|
||||
// 筛选是自动的,通过计算属性实现
|
||||
}
|
||||
|
||||
/** 查看答案 */
|
||||
const handleViewAnswer = (row: any) => {
|
||||
if (!row.id) {
|
||||
ElMessage.warning('该人员暂无答题记录')
|
||||
return
|
||||
}
|
||||
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 {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要通知「${row.prisonerName}」完成问卷吗?`,
|
||||
'通知确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
await QuestionnaireTaskApi.notifyPrisoner(row.id)
|
||||
ElMessage.success('通知已发送')
|
||||
} catch (e) {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置答题记录 */
|
||||
const handleResetRecord = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要重置「${row.prisonerName}」的答题记录吗?重置后需要重新填写。`,
|
||||
'重置确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await QuestionnaireTaskApi.resetPrisonerRecord(row.id)
|
||||
ElMessage.success('重置成功')
|
||||
loadPrisonerProgress()
|
||||
} catch (e) {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
/** 代为填写 */
|
||||
const handleAgentFill = (row: any) => {
|
||||
agentFillDialogRef.value?.open(row)
|
||||
}
|
||||
|
||||
/** 查看服刑人员详情 */
|
||||
const handleViewPrisoner = (row: any) => {
|
||||
prisonerDetailRef.value?.open(row.prisonerId)
|
||||
}
|
||||
|
||||
/** 提醒未完成人员 */
|
||||
const handleRemind = async () => {
|
||||
if (!taskId.value) return
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要提醒该任务中 ${taskProgress.value?.pendingCount || 0} 位未完成人员吗?`,
|
||||
'提醒确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
)
|
||||
|
||||
remindLoading.value = true
|
||||
const count = await QuestionnaireTaskApi.remindPendingPrisoners(taskId.value)
|
||||
ElMessage.success(`已提醒 ${count} 位未完成人员`)
|
||||
} catch (e) {
|
||||
// 用户取消
|
||||
} finally {
|
||||
remindLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.progress-item {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.filter-select {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-tabs {
|
||||
:deep(.el-tabs__content) {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,551 +0,0 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="任务名称" prop="taskName">
|
||||
<el-input
|
||||
v-model="queryParams.taskName"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="问卷" prop="questionnaireId">
|
||||
<el-select
|
||||
v-model="queryParams.questionnaireId"
|
||||
placeholder="请选择问卷"
|
||||
clearable
|
||||
class="!w-200px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in questionnaireList"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
class="!w-120px"
|
||||
@change="handleQuery"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in taskStatusOptions"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="handleCreate"
|
||||
v-hasPermi="['prison:questionnaire-task:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 创建任务
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['prison:questionnaire-task:export']"
|
||||
>
|
||||
<Icon icon="ep:download" class="mr-5px" /> 导出
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<ContentWrap v-if="statistics">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>任务总数</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ statistics.taskCount }}</div>
|
||||
<div class="stat-label">个</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>目标人数</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ statistics.totalPrisoners }}</div>
|
||||
<div class="stat-label">人</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>已完成人数</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value text-success">{{ statistics.totalCompleted }}</div>
|
||||
<div class="stat-label">人</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>整体完成率</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value text-primary">{{ statistics.overallCompletionRate }}%</div>
|
||||
<div class="stat-label">完成率</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table
|
||||
row-key="id"
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
:stripe="true"
|
||||
:show-overflow-tooltip="true"
|
||||
highlight-current-row
|
||||
@selection-change="handleRowCheckboxChange"
|
||||
:scroll-x="true"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="任务ID" align="center" prop="id" width="80" />
|
||||
<el-table-column label="任务名称" align="center" prop="taskName" width="200">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="handleDetail(scope.row)">
|
||||
{{ scope.row.taskName }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="问卷名称" align="center" prop="questionnaireName" width="180" />
|
||||
<el-table-column label="目标范围" align="center" prop="targetType" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getTargetTypeTag(scope.row.targetType)">
|
||||
{{ getTargetTypeLabel(scope.row.targetType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="目标人数" align="center" prop="totalCount" width="100" />
|
||||
<el-table-column label="已完成" align="center" prop="completedCount" width="100">
|
||||
<template #default="scope">
|
||||
<span class="text-success">{{ scope.row.completedCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成率" align="center" prop="completionRate" width="120">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="Number(scope.row.completionRate) || 0"
|
||||
:stroke-width="6"
|
||||
:status="getProgressStatus(scope.row.completionRate)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTag(scope.row.status)">
|
||||
{{ getStatusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="截止时间" align="center" prop="deadline" width="160">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.deadline) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
|
||||
<template #default="scope">
|
||||
{{ formatDateTime(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" fixed="right" width="200">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleRestart(scope.row)"
|
||||
v-if="scope.row.status === 3"
|
||||
v-hasPermi="['prison:questionnaire-task:restart']"
|
||||
>
|
||||
重启
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
@click="handleFinish(scope.row)"
|
||||
v-if="scope.row.status === 2"
|
||||
v-hasPermi="['prison:questionnaire-task:finish']"
|
||||
>
|
||||
结束
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleCancel(scope.row)"
|
||||
v-if="scope.row.status === 2"
|
||||
v-hasPermi="['prison:questionnaire-task:cancel']"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['prison:questionnaire-task:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 创建任务弹窗 -->
|
||||
<CreateTaskDialog ref="createTaskDialogRef" @success="getList" />
|
||||
|
||||
<!-- 任务详情弹窗 -->
|
||||
<TaskDetailDialog ref="taskDetailDialogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatDateTime } from '@/utils/formatTime'
|
||||
import CreateTaskDialog from './components/CreateTaskDialog.vue'
|
||||
import TaskDetailDialog from './components/TaskDetailDialog.vue'
|
||||
import { QuestionnaireTaskApi } from '@/api/prison/questionnaire-task'
|
||||
import { QuestionnaireApi } from '@/api/prison/questionnaire'
|
||||
|
||||
defineOptions({ name: 'QuestionnaireTask' })
|
||||
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const checkedIds = ref<number[]>([])
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
taskName: undefined,
|
||||
questionnaireId: undefined,
|
||||
status: undefined,
|
||||
targetType: undefined,
|
||||
createTime: undefined
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const statistics = ref<any>(null)
|
||||
|
||||
// 问卷列表
|
||||
const questionnaireList = ref<any[]>([])
|
||||
|
||||
// 任务状态选项
|
||||
const taskStatusOptions = [
|
||||
{ value: 1, label: '草稿', type: 'info' },
|
||||
{ value: 2, label: '进行中', type: 'primary' },
|
||||
{ value: 3, label: '已结束', type: 'success' },
|
||||
{ value: 4, label: '已取消', type: 'danger' }
|
||||
]
|
||||
|
||||
// 目标类型选项
|
||||
const targetTypeOptions = [
|
||||
{ value: 1, label: '指定犯人' },
|
||||
{ value: 2, label: '指定监区' },
|
||||
{ value: 3, label: '全部犯人' }
|
||||
]
|
||||
|
||||
// 弹窗引用
|
||||
const createTaskDialogRef = ref()
|
||||
const taskDetailDialogRef = ref()
|
||||
|
||||
/** 获取状态标签类型 */
|
||||
const getStatusTag = (status: number) => {
|
||||
const item = taskStatusOptions.find(item => item.value === status)
|
||||
return item?.type || 'info'
|
||||
}
|
||||
|
||||
/** 获取状态标签文本 */
|
||||
const getStatusLabel = (status: number) => {
|
||||
const item = taskStatusOptions.find(item => item.value === status)
|
||||
return item?.label || '未知'
|
||||
}
|
||||
|
||||
/** 获取目标类型标签文本 */
|
||||
const getTargetTypeLabel = (type: number) => {
|
||||
const item = targetTypeOptions.find(item => item.value === type)
|
||||
return item?.label || '未知'
|
||||
}
|
||||
|
||||
/** 获取目标类型标签样式 */
|
||||
const getTargetTypeTag = (type: number) => {
|
||||
const map: Record<number, string> = {
|
||||
1: 'warning',
|
||||
2: 'primary',
|
||||
3: 'success'
|
||||
}
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
/** 获取进度条状态 */
|
||||
const getProgressStatus = (rate: number | string) => {
|
||||
const r = Number(rate) || 0
|
||||
if (r >= 80) return 'success'
|
||||
if (r >= 50) return 'warning'
|
||||
return 'exception'
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await QuestionnaireTaskApi.getQuestionnaireTaskPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取统计数据 */
|
||||
const getStatistics = async () => {
|
||||
try {
|
||||
statistics.value = await QuestionnaireTaskApi.getStatisticsSummary()
|
||||
} catch (e) {
|
||||
console.error('获取统计数据失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取问卷列表 */
|
||||
const getQuestionnaireList = async () => {
|
||||
try {
|
||||
const data = await QuestionnaireApi.getQuestionnairePage({
|
||||
pageNo: 1,
|
||||
pageSize: 100,
|
||||
status: 2 // 已发布
|
||||
})
|
||||
questionnaireList.value = data.list || []
|
||||
} catch (e) {
|
||||
console.error('获取问卷列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
const resetQuery = () => {
|
||||
queryParams.taskName = undefined
|
||||
queryParams.questionnaireId = undefined
|
||||
queryParams.status = undefined
|
||||
queryParams.targetType = undefined
|
||||
queryParams.createTime = undefined
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 行checkbox选择 */
|
||||
const handleRowCheckboxChange = (rows: any[]) => {
|
||||
checkedIds.value = rows.map(row => row.id)
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
const handleDetail = (row: any) => {
|
||||
taskDetailDialogRef.value?.open(row.id)
|
||||
}
|
||||
|
||||
/** 创建任务 */
|
||||
const handleCreate = () => {
|
||||
createTaskDialogRef.value?.open()
|
||||
}
|
||||
|
||||
/** 重新开始任务 */
|
||||
const handleRestart = (row: any) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要重新开始任务「${row.taskName}」吗?`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
await QuestionnaireTaskApi.restartTask(row.id)
|
||||
ElMessage.success('重启成功')
|
||||
getList()
|
||||
getStatistics()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 结束任务 */
|
||||
const handleFinish = (row: any) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要结束任务「${row.taskName}」吗?结束后未完成的问卷将标记为已过期。`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
await QuestionnaireTaskApi.finishTask(row.id)
|
||||
ElMessage.success('结束成功')
|
||||
getList()
|
||||
getStatistics()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 取消任务 */
|
||||
const handleCancel = (row: any) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要取消任务「${row.taskName}」吗?`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
await QuestionnaireTaskApi.cancelTask(row.id)
|
||||
ElMessage.success('取消成功')
|
||||
getList()
|
||||
getStatistics()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除任务「${row.taskName}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
await QuestionnaireTaskApi.deleteQuestionnaireTask(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
getStatistics()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 批量删除 */
|
||||
const handleDeleteBatch = () => {
|
||||
if (checkedIds.value.length === 0) {
|
||||
ElMessage.warning('请选择要删除的任务')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要批量删除选中的 ${checkedIds.value.length} 个任务吗?`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
await QuestionnaireTaskApi.deleteQuestionnaireTaskList(checkedIds.value)
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
getStatistics()
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
/** 导出 */
|
||||
const handleExport = async () => {
|
||||
exportLoading.value = true
|
||||
try {
|
||||
await QuestionnaireTaskApi.exportQuestionnaireTask(queryParams)
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
getStatistics()
|
||||
getQuestionnaireList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stat-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -117,35 +117,29 @@ const open = async (type: string, id?: number) => {
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
await formRef.value.validate()
|
||||
// 提交请求:将 coverImage 数组转换为字符串(取第一个元素或null)
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = { ...formData.value } as unknown as Questionnaire
|
||||
// 处理 coverImage 数组转换为字符串
|
||||
if (Array.isArray(data.coverImage) && data.coverImage.length > 0) {
|
||||
data.coverImage = data.coverImage[0] as unknown as string
|
||||
} else {
|
||||
data.coverImage = undefined as unknown as string
|
||||
}
|
||||
if (formType.value === 'create') {
|
||||
await QuestionnaireApi.createQuestionnaire(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await QuestionnaireApi.updateQuestionnaire(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
await formRef.value.validate()
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as Questionnaire
|
||||
if (formType.value === 'create') {
|
||||
await QuestionnaireApi.createQuestionnaire(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await QuestionnaireApi.updateQuestionnaire(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['prison:question:create', 'prison:questionnaire:update']"
|
||||
v-hasPermi="['prison:question:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新建问题
|
||||
</el-button>
|
||||
@ -15,7 +15,7 @@
|
||||
type="success"
|
||||
plain
|
||||
@click="openPartDialog"
|
||||
v-hasPermi="['prison:question:create', 'prison:questionnaire:update']"
|
||||
v-hasPermi="['prison:question:create']"
|
||||
>
|
||||
<Icon icon="ep:folder" class="mr-5px" /> 分区管理
|
||||
</el-button>
|
||||
@ -24,7 +24,7 @@
|
||||
plain
|
||||
:disabled="checkedIds.length === 0"
|
||||
@click="handleDeleteBatch"
|
||||
v-hasPermi="['prison:question:delete', 'prison:questionnaire:update']"
|
||||
v-hasPermi="['prison:question:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" class="mr-5px" /> 批量删除
|
||||
</el-button>
|
||||
@ -135,7 +135,7 @@
|
||||
link
|
||||
size="small"
|
||||
@click="openForm('update', question.id)"
|
||||
v-hasPermi="['prison:question:update', 'prison:questionnaire:update']"
|
||||
v-hasPermi="['prison:question:update']"
|
||||
>
|
||||
<Icon icon="ep:edit" /> 修改
|
||||
</el-button>
|
||||
@ -144,7 +144,7 @@
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(question.id)"
|
||||
v-hasPermi="['prison:question:delete', 'prison:questionnaire:update']"
|
||||
v-hasPermi="['prison:question:delete']"
|
||||
>
|
||||
<Icon icon="ep:delete" /> 删除
|
||||
</el-button>
|
||||
@ -192,10 +192,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" plain :icon="Plus" @click="addPartition" v-hasPermi="['prison:question:create']">添加分区</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="savePartitions" type="primary" v-hasPermi="['prison:question:update', 'prison:questionnaire:update']">保存设置</el-button>
|
||||
<el-button @click="partDialogVisible = false">关闭</el-button>
|
||||
<el-button @click="savePartitions" type="primary" v-hasPermi="['prison:question:update']">保存设置</el-button>
|
||||
<el-button @click="partDialogVisible = false">取消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
@ -328,30 +331,30 @@ const getList = async () => {
|
||||
list.value = data.list
|
||||
partitions.value = extractPartitions(data.list)
|
||||
|
||||
// 每次都重新初始化分区管理列表(切换问卷时需要重载)
|
||||
allPartList.value = []
|
||||
// 初始化分区管理列表(如果还没有初始化)
|
||||
if (allPartList.value.length === 0) {
|
||||
// 添加默认分区
|
||||
allPartList.value.push({
|
||||
id: 'default',
|
||||
name: '',
|
||||
sort: 0,
|
||||
isDefault: true
|
||||
})
|
||||
|
||||
// 添加默认分区
|
||||
allPartList.value.push({
|
||||
id: 'default',
|
||||
name: '',
|
||||
sort: 0,
|
||||
isDefault: true
|
||||
})
|
||||
|
||||
// 添加已存在的分区
|
||||
const existingNames = new Set<string>()
|
||||
partitions.value.forEach(p => {
|
||||
if (p.name && !existingNames.has(p.name)) {
|
||||
existingNames.add(p.name)
|
||||
allPartList.value.push({
|
||||
id: `part_${p.name}`,
|
||||
name: p.name,
|
||||
sort: p.sort,
|
||||
isDefault: false
|
||||
})
|
||||
}
|
||||
})
|
||||
// 添加已存在的分区
|
||||
const existingNames = new Set<string>()
|
||||
partitions.value.forEach(p => {
|
||||
if (p.name && !existingNames.has(p.name)) {
|
||||
existingNames.add(p.name)
|
||||
allPartList.value.push({
|
||||
id: `part_${p.name}`,
|
||||
name: p.name,
|
||||
sort: p.sort,
|
||||
isDefault: false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -483,39 +486,26 @@ const savePartitions = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 收集所有需要更新的问题(包括默认分区)
|
||||
// 收集所有需要更新的问题
|
||||
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
||||
for (let i = 0; i < allPartList.value.length; i++) {
|
||||
const part = allPartList.value[i]
|
||||
for (const p of partitions.value) {
|
||||
// 匹配分区:两个 name 相等,或者两个都是空(默认分区)
|
||||
if ((p.name === part.name) || (p.name === '' && part.name === '')) {
|
||||
p.questions.forEach((q, sortIndex) => {
|
||||
updates.push({
|
||||
id: q.id!,
|
||||
partName: part.name || undefined,
|
||||
partSort: i,
|
||||
sort: sortIndex
|
||||
if (!part.isDefault && part.name) {
|
||||
for (const p of partitions.value) {
|
||||
if (p.name === part.name) {
|
||||
p.questions.forEach((q, sortIndex) => {
|
||||
updates.push({
|
||||
id: q.id!,
|
||||
partName: part.name,
|
||||
partSort: i,
|
||||
sort: sortIndex
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 没有需要更新的问题(可能是添加了空分区)
|
||||
if (updates.length === 0) {
|
||||
// 检查是否有新建的空分区
|
||||
const hasNewPartition = allPartList.value.some(p => !p.isDefault && p.id?.toString().startsWith('part_'))
|
||||
if (hasNewPartition) {
|
||||
message.error('请先在该分区下添加问题后再保存')
|
||||
return
|
||||
}
|
||||
// 如果确实没有变化,直接关闭弹窗
|
||||
partDialogVisible.value = false
|
||||
message.success('保存成功')
|
||||
return
|
||||
}
|
||||
|
||||
// 批量更新
|
||||
await QuestionApi.batchUpdate({ questions: updates })
|
||||
await getList()
|
||||
@ -528,22 +518,20 @@ const savePartitions = async () => {
|
||||
|
||||
/** 分区拖拽排序完成 */
|
||||
const onPartitionDragEnd = async () => {
|
||||
// 收集所有需要更新的问题(包括默认分区)
|
||||
// 收集所有需要更新的问题
|
||||
const updates: Array<{ id: number; partName?: string; partSort?: number; sort?: number }> = []
|
||||
for (let i = 0; i < partitions.value.length; i++) {
|
||||
const part = partitions.value[i]
|
||||
part.questions.forEach((q, sortIndex) => {
|
||||
updates.push({
|
||||
id: q.id!,
|
||||
partName: part.name || undefined,
|
||||
partSort: i,
|
||||
sort: sortIndex
|
||||
if (part.name) {
|
||||
part.questions.forEach((q, sortIndex) => {
|
||||
updates.push({
|
||||
id: q.id!,
|
||||
partName: part.name,
|
||||
partSort: i,
|
||||
sort: sortIndex
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
// 空数组无需调用后端
|
||||
if (updates.length === 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 批量更新
|
||||
await QuestionApi.batchUpdate({ questions: updates })
|
||||
|
||||
@ -1,524 +0,0 @@
|
||||
<template>
|
||||
<Dialog title="问卷预览" v-model="dialogVisible" width="900px" :fullscreen="false">
|
||||
<div class="questionnaire-preview" v-loading="loading">
|
||||
<!-- 问卷头部信息 -->
|
||||
<div class="preview-header">
|
||||
<h2 class="title">{{ questionnaire.title }}</h2>
|
||||
<div class="meta-info">
|
||||
<el-tag v-if="questionnaire.type" type="primary" size="large">
|
||||
{{ getTypeLabel(questionnaire.type) }}
|
||||
</el-tag>
|
||||
<span class="info-item" v-if="questionnaire.estimatedTime">
|
||||
<Icon icon="ep:clock" /> 预计 {{ questionnaire.estimatedTime }} 分钟
|
||||
</span>
|
||||
<span class="info-item" v-if="questionnaire.totalScore">
|
||||
<Icon icon="ep:coin" /> 总分 {{ questionnaire.totalScore }} 分
|
||||
</span>
|
||||
<span class="info-item" v-if="questionnaire.passScore">
|
||||
<Icon icon="ep:check" /> 及格分 {{ questionnaire.passScore }} 分
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 问卷说明 -->
|
||||
<div v-if="questionnaire.description" class="preview-description">
|
||||
<div class="section-title">问卷说明</div>
|
||||
<div class="description-content" v-html="questionnaire.description"></div>
|
||||
</div>
|
||||
|
||||
<!-- 填写说明 -->
|
||||
<div v-if="questionnaire.instruction" class="preview-instruction">
|
||||
<div class="section-title">填写说明</div>
|
||||
<div class="instruction-content">{{ questionnaire.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">
|
||||
<Icon icon="ep:folder" />
|
||||
{{ partition.name }}
|
||||
<span class="question-count">({{ partition.questions.length }} 道题)</span>
|
||||
</div>
|
||||
|
||||
<!-- 问题列表 -->
|
||||
<div class="question-items">
|
||||
<div
|
||||
v-for="(question, index) in partition.questions"
|
||||
:key="question.id"
|
||||
class="question-item"
|
||||
>
|
||||
<!-- 问题标题 -->
|
||||
<div class="question-header">
|
||||
<span class="question-index">
|
||||
{{ partition.name ? '' : (index + 1) }}
|
||||
</span>
|
||||
<span class="question-title">
|
||||
{{ question.title }}
|
||||
<el-tag v-if="question.isRequired" type="danger" size="small" class="required-tag">
|
||||
必填
|
||||
</el-tag>
|
||||
<el-tag v-if="question.score" type="info" size="small">
|
||||
{{ question.score }}分
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 帮助说明 -->
|
||||
<div v-if="question.helpText" class="question-help">
|
||||
<Icon icon="ep:info-filled" />
|
||||
{{ question.helpText }}
|
||||
</div>
|
||||
|
||||
<!-- 单选/多选题 -->
|
||||
<div v-if="question.type === 1 || question.type === 2" class="question-options">
|
||||
<div
|
||||
v-for="option in getQuestionOptions(question)"
|
||||
:key="option.label"
|
||||
class="option-item"
|
||||
>
|
||||
<el-radio v-if="question.type === 1" :value="false" disabled>
|
||||
{{ option.label }}
|
||||
<span v-if="option.score > 0" class="option-score">({{ option.score }}分)</span>
|
||||
</el-radio>
|
||||
<el-checkbox v-else disabled>
|
||||
{{ option.label }}
|
||||
<span v-if="option.score > 0" class="option-score">({{ option.score }}分)</span>
|
||||
</el-checkbox>
|
||||
<div v-if="option.isOther" class="other-input">
|
||||
<el-input placeholder="其他,请说明" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 填空题 -->
|
||||
<div v-else-if="question.type === 3" class="question-input">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:placeholder="question.placeholder || '请输入'"
|
||||
:rows="3"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 评分题 -->
|
||||
<div v-else-if="question.type === 4" class="question-rating">
|
||||
<div class="rating-info">
|
||||
<span>最低分:{{ getRangeValue(question, 'min') }}</span>
|
||||
<span>最高分:{{ getRangeValue(question, 'max') }}</span>
|
||||
<span>默认值:{{ question.score || getRangeValue(question, 'max') }}</span>
|
||||
</div>
|
||||
<el-input-number disabled :min="getRangeValue(question, 'min')" :max="getRangeValue(question, 'max')" />
|
||||
</div>
|
||||
|
||||
<!-- 日期题 -->
|
||||
<div v-else-if="question.type === 5" class="question-date">
|
||||
<div class="date-info" v-if="getRangeValue(question, 'min') || getRangeValue(question, 'max')">
|
||||
日期范围:{{ getRangeValue(question, 'min') || '无限制' }} ~ {{ getRangeValue(question, 'max') || '无限制' }}
|
||||
</div>
|
||||
<el-date-picker
|
||||
type="date"
|
||||
placeholder="请选择日期"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 数字题 -->
|
||||
<div v-else-if="question.type === 6" class="question-number">
|
||||
<div class="number-info" v-if="getRangeValue(question, 'min') !== undefined || getRangeValue(question, 'max') !== undefined">
|
||||
数值范围:{{ getRangeValue(question, 'min') ?? '无限制' }} ~ {{ getRangeValue(question, 'max') ?? '无限制' }}
|
||||
</div>
|
||||
<el-input-number
|
||||
:placeholder="question.placeholder || '请输入数字'"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty v-if="partitions.length === 0 && !loading" description="暂无问题" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">关 闭</el-button>
|
||||
<el-button type="primary" @click="openFillPage" :disabled="!questionnaire.id">
|
||||
<Icon icon="ep:document" /> 去填写
|
||||
</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { QuestionnaireApi, Questionnaire } from '@/api/prison/questionnaire'
|
||||
import { QuestionApi, Question } from '@/api/prison/question'
|
||||
|
||||
defineOptions({ name: 'QuestionnairePreview' })
|
||||
|
||||
const message = useMessage()
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const questionnaire = ref<Questionnaire>({
|
||||
id: undefined,
|
||||
title: '',
|
||||
type: undefined,
|
||||
description: '',
|
||||
coverImage: [],
|
||||
instruction: '',
|
||||
estimatedTime: undefined,
|
||||
totalScore: undefined,
|
||||
passScore: undefined,
|
||||
allowAnonymous: false,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
const questions = ref<Question[]>([])
|
||||
const partitions = ref<Array<{
|
||||
name: string
|
||||
sort: number
|
||||
questions: Question[]
|
||||
}>>([])
|
||||
|
||||
/** 问卷类型标签 */
|
||||
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 {
|
||||
return JSON.parse(question.options)
|
||||
} 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 extractPartitions = (questionList: Question[]) => {
|
||||
const partMap = new Map<string, Question[]>()
|
||||
|
||||
questionList.forEach(q => {
|
||||
const partName = q.partName || ''
|
||||
if (!partMap.has(partName)) {
|
||||
partMap.set(partName, [])
|
||||
}
|
||||
partMap.get(partName)!.push(q)
|
||||
})
|
||||
|
||||
// 按分区排序
|
||||
const sortedParts = Array.from(partMap.entries())
|
||||
.sort((a, b) => {
|
||||
const sortA = a[1][0]?.partSort ?? 0
|
||||
const sortB = b[1][0]?.partSort ?? 0
|
||||
return sortA - sortB
|
||||
})
|
||||
|
||||
// 构建分区列表
|
||||
const result = []
|
||||
|
||||
// 添加默认分区(如果有)
|
||||
const defaultQuestions = sortedParts.find(([name]) => !name)
|
||||
if (defaultQuestions) {
|
||||
result.push({
|
||||
name: '',
|
||||
sort: 0,
|
||||
questions: defaultQuestions[1]
|
||||
})
|
||||
}
|
||||
|
||||
// 添加其他分区
|
||||
sortedParts
|
||||
.filter(([name]) => name)
|
||||
.forEach(([name, qs]) => {
|
||||
result.push({
|
||||
name,
|
||||
sort: qs[0]?.partSort ?? 0,
|
||||
questions: qs
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/** 打开预览 */
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 加载问卷信息
|
||||
const qData = await QuestionnaireApi.getQuestionnaire(id)
|
||||
questionnaire.value = {
|
||||
...qData,
|
||||
coverImage: qData.coverImage || []
|
||||
}
|
||||
|
||||
// 加载问题列表
|
||||
const questionsData = await QuestionApi.getQuestionPage({
|
||||
pageNo: 1,
|
||||
pageSize: 200,
|
||||
questionnaireId: id
|
||||
})
|
||||
questions.value = questionsData.list
|
||||
partitions.value = extractPartitions(questionsData.list)
|
||||
} catch (error) {
|
||||
message.error('加载失败')
|
||||
dialogVisible.value = false
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到填写页面 */
|
||||
const openFillPage = () => {
|
||||
// TODO: 实现跳转到填写页面的逻辑
|
||||
message.info('填写页面功能待实现')
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.questionnaire-preview {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-description,
|
||||
.preview-instruction {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.description-content {
|
||||
color: #606266;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.instruction-content {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
background: #f4f4f5;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #e4e7ed;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-questions {
|
||||
.partition-title {
|
||||
background: linear-gradient(135deg, #e8f4fd 0%, #f0f7ff 100%);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 24px;
|
||||
font-weight: 600;
|
||||
color: #1a5cb8;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.question-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.question-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.question-item {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.question-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.question-index {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: #409eff;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.required-tag {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.question-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #f4f4f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.option-score {
|
||||
color: #67c23a;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.other-input {
|
||||
margin-left: 24px;
|
||||
flex: 1;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.question-rating,
|
||||
.question-date,
|
||||
.question-number {
|
||||
.rating-info,
|
||||
.date-info,
|
||||
.number-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.question-input {
|
||||
:deep(.el-textarea) {
|
||||
.el-textarea__inner {
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -101,11 +101,7 @@
|
||||
<dict-tag :type="DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE" :value="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="问卷说明" align="center" prop="description" width="200">
|
||||
<template #default="scope">
|
||||
<div v-html="scope.row.description"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="问卷说明" align="center" prop="description" width="200" />
|
||||
<el-table-column label="总分" align="center" prop="totalScore" width="80" />
|
||||
<el-table-column label="及格分" align="center" prop="passScore" width="80" />
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
@ -118,16 +114,8 @@
|
||||
{{ formatDateTime(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="220" fixed="right">
|
||||
<el-table-column label="操作" align="center" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="success"
|
||||
link
|
||||
@click="openPreview(scope.row.id)"
|
||||
v-hasPermi="['prison:questionnaire:query']"
|
||||
>
|
||||
预览
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@ -166,9 +154,6 @@
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<QuestionnaireForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 预览弹窗 -->
|
||||
<QuestionnairePreview ref="previewRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -178,7 +163,6 @@ import download from '@/utils/download'
|
||||
import { QuestionnaireApi, Questionnaire } from '@/api/prison/questionnaire'
|
||||
import QuestionnaireForm from './QuestionnaireForm.vue'
|
||||
import QuestionList from './components/QuestionList.vue'
|
||||
import QuestionnairePreview from './components/QuestionnairePreview.vue'
|
||||
|
||||
defineOptions({ name: 'Questionnaire' })
|
||||
|
||||
@ -277,12 +261,6 @@ const handleCurrentChange = (row: Questionnaire | undefined) => {
|
||||
currentRow.value = row || {} as Questionnaire
|
||||
}
|
||||
|
||||
/** 预览问卷 */
|
||||
const previewRef = ref()
|
||||
const openPreview = (id: number) => {
|
||||
previewRef.value.open(id)
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@ -1,320 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="`答题详情 - ${recordInfo.prisonerName || ''}`"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="loading" class="answer-detail-dialog">
|
||||
<!-- 记录基本信息 -->
|
||||
<el-descriptions :column="3" border class="mb-20px">
|
||||
<el-descriptions-item label="问卷名称">{{ recordInfo.questionnaireName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="罪犯编号">{{ recordInfo.prisonerNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完成时间">{{ formatDateTime(recordInfo.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客观分">{{ recordInfo.objectiveScore || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="主观分">{{ recordInfo.subjectiveScore || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="总分">{{ recordInfo.totalScore || 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="答题用时">{{ formatDuration(recordInfo.duration) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="及格状态">
|
||||
<el-tag :type="getPassStatusTag(recordInfo.passStatus)" size="small">
|
||||
{{ getPassStatusText(recordInfo.passStatus) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 问卷题目 -->
|
||||
<div v-if="questions.length > 0" class="questionnaire-content">
|
||||
<div
|
||||
v-for="(item, index) in questions"
|
||||
:key="item.id"
|
||||
class="question-item"
|
||||
>
|
||||
<div class="question-header">
|
||||
<span class="question-index">{{ index + 1 }}</span>
|
||||
<span class="question-title">{{ item.title }}</span>
|
||||
<el-tag v-if="item.isRequired" type="danger" size="small" class="required-tag">必填</el-tag>
|
||||
<el-tag v-if="item.score" type="info" size="small">{{ item.score }}分</el-tag>
|
||||
</div>
|
||||
|
||||
<!-- 答案展示 -->
|
||||
<div class="answer-area">
|
||||
<!-- 单选题 -->
|
||||
<div v-if="item.type === 1" class="options-container">
|
||||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||
{{ getAnswerText(item.id) }}
|
||||
</span>
|
||||
<span v-else class="empty-answer">未作答</span>
|
||||
</div>
|
||||
|
||||
<!-- 多选题 -->
|
||||
<div v-else-if="item.type === 2" class="options-container">
|
||||
<span v-if="getMultiAnswerText(item.id)" class="answer-text">
|
||||
{{ getMultiAnswerText(item.id) }}
|
||||
</span>
|
||||
<span v-else class="empty-answer">未作答</span>
|
||||
</div>
|
||||
|
||||
<!-- 填空题 -->
|
||||
<div v-else-if="item.type === 3" class="options-container">
|
||||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||
{{ getAnswerText(item.id) }}
|
||||
</span>
|
||||
<span v-else class="empty-answer">未作答</span>
|
||||
</div>
|
||||
|
||||
<!-- 评分题 -->
|
||||
<div v-else-if="item.type === 4" class="options-container">
|
||||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||
{{ getAnswerText(item.id) }} 分
|
||||
</span>
|
||||
<span v-else class="empty-answer">未作答</span>
|
||||
</div>
|
||||
|
||||
<!-- 日期题 -->
|
||||
<div v-else-if="item.type === 5" class="options-container">
|
||||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||
{{ getAnswerText(item.id) }}
|
||||
</span>
|
||||
<span v-else class="empty-answer">未作答</span>
|
||||
</div>
|
||||
|
||||
<!-- 数字题 -->
|
||||
<div v-else-if="item.type === 6" class="options-container">
|
||||
<span v-if="getAnswerText(item.id)" class="answer-text">
|
||||
{{ getAnswerText(item.id) }}
|
||||
</span>
|
||||
<span v-else class="empty-answer">未作答</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="暂无答题记录" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDateTime } from '@/utils/formatTime'
|
||||
import { QuestionnaireRecordApi, type QuestionnaireRecord } from '@/api/prison/questionnairerecord'
|
||||
import { QuestionApi, type Question } from '@/api/prison/question'
|
||||
import { AnswerApi, type Answer } from '@/api/prison/answer'
|
||||
import { getIntDictOptions } from '@/utils/dict'
|
||||
|
||||
defineOptions({ name: 'AnswerDetailDialog' })
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
// 记录信息
|
||||
const recordInfo = ref<QuestionnaireRecord>({
|
||||
id: undefined,
|
||||
questionnaireName: '',
|
||||
prisonerName: '',
|
||||
prisonerNo: '',
|
||||
objectiveScore: 0,
|
||||
subjectiveScore: 0,
|
||||
totalScore: 0,
|
||||
passStatus: undefined,
|
||||
duration: 0,
|
||||
createTime: ''
|
||||
})
|
||||
|
||||
// 问题列表
|
||||
const questions = ref<Question[]>([])
|
||||
|
||||
// 答案列表
|
||||
const answers = ref<Answer[]>([])
|
||||
|
||||
/** 格式化时长 */
|
||||
const formatDuration = (seconds: number | undefined): string => {
|
||||
if (!seconds) return '-'
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = seconds % 60
|
||||
if (hours > 0) {
|
||||
return `${hours}时${minutes}分${secs}秒`
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}分${secs}秒`
|
||||
} else {
|
||||
return `${secs}秒`
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取及格状态文本 */
|
||||
const getPassStatusText = (status: number | undefined) => {
|
||||
const options = getIntDictOptions(DICT_TYPE.PRISON_RECORD_PASS_STATUS)
|
||||
return options.find(o => o.value === status)?.label || '-'
|
||||
}
|
||||
|
||||
/** 获取及格状态标签类型 */
|
||||
const getPassStatusTag = (status: number | undefined): 'success' | 'danger' | 'warning' | 'info' => {
|
||||
const tagMap: Record<number, 'success' | 'danger' | 'warning' | 'info'> = {
|
||||
1: 'success',
|
||||
2: 'danger',
|
||||
3: 'warning'
|
||||
}
|
||||
return tagMap[status || 0] || 'info'
|
||||
}
|
||||
|
||||
/** 获取单选/填空/评分/日期/数字题的答案文本 */
|
||||
const getAnswerText = (questionId: number | undefined): string | null => {
|
||||
if (questionId === undefined) return null
|
||||
const answer = answers.value.find(a => a.questionId === questionId)
|
||||
return answer?.answerText || null
|
||||
}
|
||||
|
||||
/** 获取多选题的答案文本 */
|
||||
const getMultiAnswerText = (questionId: number | undefined): string | null => {
|
||||
if (questionId === undefined) return null
|
||||
const answer = answers.value.find(a => a.questionId === questionId)
|
||||
if (!answer?.optionIds || !Array.isArray(answer.optionIds) || answer.optionIds.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 找到对应的问题
|
||||
const question = questions.value.find(q => q.id === questionId)
|
||||
if (!question?.options) return null
|
||||
|
||||
try {
|
||||
// 解析选项
|
||||
const options = JSON.parse(question.options) as Array<{ label: string; value?: string; isOther?: boolean }>
|
||||
|
||||
// 根据 optionIds(索引数组)获取对应的选项文字
|
||||
const selectedLabels = answer.optionIds
|
||||
.map((idx: number) => options[idx])
|
||||
.filter((opt): opt is { label: string } => !!opt)
|
||||
.map(opt => opt.label)
|
||||
|
||||
return selectedLabels.length > 0 ? selectedLabels.join('、') : null
|
||||
} catch (e) {
|
||||
console.error('解析选项失败:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
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
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载答题详情失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.answer-detail-dialog {
|
||||
overflow-y: auto;
|
||||
max-height: 55vh;
|
||||
padding-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.questionnaire-content {
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
padding-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ed;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.question-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.question-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.question-index {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.answer-area {
|
||||
padding-left: 32px;
|
||||
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.answer-text {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.empty-answer {
|
||||
color: #c0c4cc;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -283,60 +283,12 @@ const handleSubmit = async () => {
|
||||
// 构建答案列表
|
||||
const answerList = questions.value.map(q => {
|
||||
const answer = answers.value[q.id!]
|
||||
|
||||
// 处理多选题(type === 2)
|
||||
if (q.type === 2 && Array.isArray(answer)) {
|
||||
// 解析选项获取文字
|
||||
let optionLabels: string[] = []
|
||||
try {
|
||||
const options = JSON.parse(q.options || '[]') as Array<{ label: string }>
|
||||
optionLabels = answer
|
||||
.map((idx: string) => options[parseInt(idx)]?.label)
|
||||
.filter((label): label is string => !!label)
|
||||
} catch (e) {
|
||||
console.error('解析选项失败:', e)
|
||||
}
|
||||
|
||||
return {
|
||||
questionId: q.id!,
|
||||
answer: optionLabels.join('、') || '',
|
||||
optionIds: answer.map((idx: string) => parseInt(idx, 10))
|
||||
}
|
||||
}
|
||||
|
||||
// 单选题(type === 1):如果选择了"其他"选项,需要发送 optionIds
|
||||
if (q.type === 1 && answer) {
|
||||
const answerIndex = parseInt(answer as string, 10)
|
||||
let isOtherOption = false
|
||||
|
||||
try {
|
||||
const options = JSON.parse(q.options || '[]') as Array<{ isOther?: boolean }>
|
||||
isOtherOption = options[answerIndex]?.isOther === true
|
||||
} catch (e) {
|
||||
console.error('解析选项失败:', e)
|
||||
}
|
||||
|
||||
// 解析选项获取文字
|
||||
let answerText = answer as string
|
||||
try {
|
||||
const options = JSON.parse(q.options || '[]') as Array<{ label: string }>
|
||||
answerText = options[answerIndex]?.label || answer as string
|
||||
} catch (e) {
|
||||
console.error('解析选项失败:', e)
|
||||
}
|
||||
|
||||
return {
|
||||
questionId: q.id!,
|
||||
answer: answerText,
|
||||
optionIds: isOtherOption ? [answerIndex] : undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 填空题、评分题、日期题、数字题(type 3/4/5/6)
|
||||
// 多选时用逗号分隔
|
||||
const answerStr = Array.isArray(answer) ? answer.join(',') : answer
|
||||
return {
|
||||
questionId: q.id!,
|
||||
answer: String(answer || ''),
|
||||
optionIds: undefined
|
||||
answer: answerStr || '',
|
||||
optionIds: undefined as number[] | undefined
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -1,639 +0,0 @@
|
||||
<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, index) in partitions" :key="partition.name || 'default'">
|
||||
<!-- 分区标题 -->
|
||||
<div v-if="partition.name" class="partition-title">
|
||||
{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}、
|
||||
{{ 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) // 问卷详细信息(包含description和instruction)
|
||||
|
||||
// 分区列表(带答案)
|
||||
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,并去除前后空格和特殊字符
|
||||
const value = answer.answerText || answer.optionIds || ''
|
||||
return String(value).trim().replace(/^["']|["']$/g, '')
|
||||
}
|
||||
|
||||
/** 问卷类型标签 */
|
||||
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 === 2):answerText 是逗号分隔的标签列表
|
||||
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 === 1):answerText 等于选项标签(去除首尾空格后比较)
|
||||
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
|
||||
}
|
||||
|
||||
// 加载问卷详细信息(用于导出Word时获取description和instruction)
|
||||
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内容
|
||||
let previewHTML = previewRef.value.innerHTML
|
||||
|
||||
// 清理可能导致WPS显示引号的特殊字符
|
||||
previewHTML = previewHTML.replace(/[\u201C\u201D\u2018\u2019]/g, '') // 移除中文引号
|
||||
previewHTML = previewHTML.replace(/^["']|["']$/gm, '') // 移除行首尾的引号
|
||||
|
||||
// 构建完整的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>
|
||||
/* 使用 pt 单位以避免 Word/WPS 在 px->pt 换算时产生差异 */
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
margin: 15pt;
|
||||
padding: 15pt;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||
font-size: 18pt;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
}
|
||||
.section-title {
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||
margin-bottom: 15pt;
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
.description-content {
|
||||
color: #000;
|
||||
font-size: 11pt;
|
||||
margin-bottom: 6pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.instruction-content {
|
||||
color: #000;
|
||||
font-size: 11pt;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.partition-title {
|
||||
margin-top: 15pt;
|
||||
color: #000;
|
||||
font-size: 15pt;
|
||||
font-weight: 500;
|
||||
}
|
||||
.partition-title:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.question-count {
|
||||
font-size: 11pt;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
margin-left: 2pt;
|
||||
}
|
||||
.question-item {
|
||||
font-size: 11pt;
|
||||
color: #000;
|
||||
margin-bottom: 8pt;
|
||||
line-height: 1.5;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.question-index {
|
||||
margin-right: 2pt;
|
||||
}
|
||||
.question-title {
|
||||
margin-right: 4pt;
|
||||
}
|
||||
.question-help-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3pt;
|
||||
color: #909399;
|
||||
font-size: 11pt;
|
||||
background: #f4f4f5;
|
||||
border-radius: 2pt;
|
||||
margin-right: 6pt;
|
||||
}
|
||||
.question-options-inline {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8pt;
|
||||
align-items: center;
|
||||
}
|
||||
.option-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3pt;
|
||||
}
|
||||
.question-rating-inline,
|
||||
.question-date-inline,
|
||||
.question-number-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8pt;
|
||||
flex-wrap: wrap;
|
||||
margin-left: 3pt;
|
||||
}
|
||||
.rating-info,
|
||||
.date-info,
|
||||
.number-info {
|
||||
display: inline-flex;
|
||||
gap: 11pt;
|
||||
color: #909399;
|
||||
font-size: 11pt;
|
||||
}
|
||||
.question-input-inline {
|
||||
color: #000;
|
||||
font-size: 11pt;
|
||||
line-height: 1.5;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${previewHTML}
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// 转换为Blob (asBlob返回Promise)
|
||||
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-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||
font-size: 18pt;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
}
|
||||
.section-title {
|
||||
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
|
||||
margin-bottom: 15pt;
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
.description-content {
|
||||
color: #000;
|
||||
line-height: 3;
|
||||
font-size: 11pt;
|
||||
}
|
||||
.instruction-content {
|
||||
color: #000;
|
||||
font-size: 11pt;
|
||||
font-weight: 500;
|
||||
line-height: 3;
|
||||
}
|
||||
.partition-title {
|
||||
margin-top: 15pt;
|
||||
color: #000;
|
||||
font-size: 15pt;
|
||||
font-weight: 500;
|
||||
}
|
||||
.partition-title:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.question-count {
|
||||
font-size: 11pt;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
margin-left: 2pt;
|
||||
}
|
||||
.question-item {
|
||||
font-size: 11pt;
|
||||
color: #000;
|
||||
line-height: 3;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.question-index {
|
||||
margin-right: 2pt;
|
||||
}
|
||||
.question-title {
|
||||
margin-right: 4pt;
|
||||
}
|
||||
.question-help-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3pt;
|
||||
color: #909399;
|
||||
font-size: 11pt;
|
||||
background: #f4f4f5;
|
||||
border-radius: 2pt;
|
||||
margin-right: 6pt;
|
||||
}
|
||||
.question-options-inline {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8pt;
|
||||
align-items: center;
|
||||
}
|
||||
.option-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3pt;
|
||||
}
|
||||
.question-rating-inline,
|
||||
.question-date-inline,
|
||||
.question-number-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8pt;
|
||||
flex-wrap: wrap;
|
||||
margin-left: 3pt;
|
||||
}
|
||||
.rating-info,
|
||||
.date-info,
|
||||
.number-info {
|
||||
display: inline-flex;
|
||||
gap: 11pt;
|
||||
color: #909399;
|
||||
font-size: 11pt;
|
||||
}
|
||||
.question-input-inline {
|
||||
color: #000;
|
||||
font-size: 11pt;
|
||||
line-height: 3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@ -159,26 +159,8 @@
|
||||
{{ formatDateTime(scope.row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" min-width="280px" fixed="right">
|
||||
<el-table-column label="操作" align="center" min-width="200px" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.status === 3"
|
||||
link
|
||||
type="success"
|
||||
@click="handleViewDetail(scope.row.id)"
|
||||
v-hasPermi="['prison:questionnaire-record:query']"
|
||||
>
|
||||
查看详情
|
||||
</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
|
||||
@ -251,12 +233,6 @@
|
||||
|
||||
<!-- 人工评分弹窗 -->
|
||||
<ManualScoreDialog ref="manualScoreDialogRef" @success="getList" />
|
||||
|
||||
<!-- 答题详情弹窗 -->
|
||||
<AnswerDetailDialog ref="answerDetailDialogRef" />
|
||||
|
||||
<!-- 问卷导出弹窗 -->
|
||||
<QuestionnaireOutput ref="outputDialogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -269,8 +245,6 @@ import { QuestionnaireApi } from '@/api/prison/questionnaire'
|
||||
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' })
|
||||
@ -374,23 +348,6 @@ const handleCancel = async (row: QuestionnaireRecord) => {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 查看答题详情 */
|
||||
const answerDetailDialogRef = ref()
|
||||
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) => {
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<el-date-picker
|
||||
v-model="formData.assessmentDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
value-format="x"
|
||||
placeholder="选择评估日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
@ -72,12 +72,17 @@
|
||||
<el-form-item label="管控建议" prop="suggestions">
|
||||
<el-input v-model="formData.suggestions" placeholder="请输入管控建议" />
|
||||
</el-form-item>
|
||||
<!-- 评估人信息由后端自动从登录上下文获取,不在前端显示 -->
|
||||
<el-form-item label="评估人ID" prop="assessorId">
|
||||
<el-input v-model="formData.assessorId" placeholder="请输入评估人ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="评估人姓名" prop="assessorName">
|
||||
<el-input v-model="formData.assessorName" placeholder="请输入评估人姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="下次评估日期" prop="nextAssessmentDate">
|
||||
<el-date-picker
|
||||
v-model="formData.nextAssessmentDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
value-format="x"
|
||||
placeholder="选择下次评估日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
@ -124,9 +129,8 @@ const formData = ref({
|
||||
id: undefined,
|
||||
prisonerId: undefined as number | undefined,
|
||||
prisonerNo: undefined as string | undefined,
|
||||
prisonerName: undefined as string | undefined, // 罪犯姓名(用于显示)
|
||||
assessmentType: undefined as number | undefined,
|
||||
assessmentDate: undefined as string | undefined,
|
||||
assessmentDate: undefined as number | undefined,
|
||||
violenceScore: undefined as number | undefined,
|
||||
escapeScore: undefined as number | undefined,
|
||||
suicideScore: undefined as number | undefined,
|
||||
@ -134,8 +138,9 @@ const formData = ref({
|
||||
riskLevel: undefined as number | undefined,
|
||||
riskFactors: undefined as string | undefined,
|
||||
suggestions: undefined as string | undefined,
|
||||
// assessorId 和 assessorName 由后端自动从登录上下文获取,不从前端传递
|
||||
nextAssessmentDate: undefined as string | undefined,
|
||||
assessorId: undefined as number | undefined,
|
||||
assessorName: undefined as string | undefined,
|
||||
nextAssessmentDate: undefined as number | undefined,
|
||||
status: 1 as number | undefined,
|
||||
remark: undefined as string | undefined
|
||||
})
|
||||
@ -148,7 +153,7 @@ const formRules = reactive({
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 搜索罪犯 - 支持编号和姓名双条件搜索 */
|
||||
/** 搜索罪犯 */
|
||||
const searchPrisoner = async (query: string) => {
|
||||
if (!query) {
|
||||
prisonerList.value = []
|
||||
@ -159,7 +164,6 @@ const searchPrisoner = async (query: string) => {
|
||||
const data = await PrisonerApi.getPage({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
prisonerNo: query, // 按罪犯编号搜索
|
||||
name: query // 按姓名搜索
|
||||
} as any)
|
||||
prisonerList.value = data.list || []
|
||||
@ -232,7 +236,6 @@ const resetForm = () => {
|
||||
id: undefined,
|
||||
prisonerId: undefined,
|
||||
prisonerNo: undefined,
|
||||
prisonerName: undefined,
|
||||
assessmentType: undefined,
|
||||
assessmentDate: undefined,
|
||||
violenceScore: undefined,
|
||||
@ -242,7 +245,8 @@ const resetForm = () => {
|
||||
riskLevel: undefined,
|
||||
riskFactors: undefined,
|
||||
suggestions: undefined,
|
||||
// assessorId 和 assessorName 由后端自动从登录上下文获取,不从前端传递
|
||||
assessorId: undefined,
|
||||
assessorName: undefined,
|
||||
nextAssessmentDate: undefined,
|
||||
status: 1,
|
||||
remark: undefined
|
||||
|
||||
@ -290,21 +290,9 @@ const submitForm = async () => {
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
// 格式化时间,保持中国时区
|
||||
const formatDateTime = (timestamp: number) => {
|
||||
if (!timestamp) return undefined
|
||||
const date = new Date(timestamp)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
const data = {
|
||||
...formData.value,
|
||||
occurTime: formatDateTime(formData.value.occurTime)
|
||||
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString() : undefined
|
||||
} as unknown as SituationSaveReqVO
|
||||
if (formType.value === 'create') {
|
||||
await SituationApi.createSituation(data)
|
||||
|
||||
@ -385,8 +385,8 @@ const submitForm = async () => {
|
||||
try {
|
||||
const data = {
|
||||
...formData.value,
|
||||
alertTime: formData.value.alertTime ? new Date(formData.value.alertTime).toISOString().slice(0, 19).replace('T', ' ') : undefined,
|
||||
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString().slice(0, 19).replace('T', ' ') : undefined
|
||||
alertTime: formData.value.alertTime ? new Date(formData.value.alertTime).toISOString() : undefined,
|
||||
occurTime: formData.value.occurTime ? new Date(formData.value.occurTime).toISOString() : undefined
|
||||
} as unknown as WarningSaveReqVO
|
||||
if (formType.value === 'create') {
|
||||
await WarningApi.createWarning(data)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user