add wc
This commit is contained in:
93
package-lock.json
generated
93
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"axios": "^1.16.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-chart-wordcloud": "^4.4.5",
|
||||
"client-zip": "^2.5.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"copy-text-to-clipboard": "^3.2.2",
|
||||
@@ -523,6 +524,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz",
|
||||
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
@@ -532,6 +534,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.4.tgz",
|
||||
"integrity": "sha512-hduz0suCcUSC/kM8Fq3A9iLwInJDl8fD1xLpTIk+5xkNm8z/FT7UsIa9sOXrkpChh+XXc18RzswE8QqELsVl+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
@@ -1184,6 +1187,7 @@
|
||||
"integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
@@ -1280,6 +1284,21 @@
|
||||
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-cloud": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-cloud/-/d3-cloud-1.2.9.tgz",
|
||||
"integrity": "sha512-5EWJvnlCrqTThGp8lYHx+DL00sOjx2HTlXH1WRe93k5pfOIhPQaL63NttaKYIbT7bTXp/USiunjNS/N4ipttIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-cloud/node_modules/@types/d3": {
|
||||
"version": "3.5.53",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-3.5.53.tgz",
|
||||
"integrity": "sha512-8yKQA9cAS6+wGsJpBysmnhlaaxlN42Qizqkw+h2nILSlS+MAG2z4JdO6p+PJrJ+ACvimkmLJL281h157e52psQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
@@ -1559,6 +1578,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
|
||||
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "0.0.7",
|
||||
"@uppy/store-default": "^2.1.1",
|
||||
@@ -1608,6 +1628,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
|
||||
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@uppy/companion-client": "^2.2.2",
|
||||
"@uppy/utils": "^4.1.2",
|
||||
@@ -1682,6 +1703,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.2.tgz",
|
||||
"integrity": "sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"d3-drag": "^3.0.0",
|
||||
@@ -2033,6 +2055,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz",
|
||||
"integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.3.0",
|
||||
@@ -2087,6 +2110,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor-next/basic-modules/-/basic-modules-2.0.0.tgz",
|
||||
"integrity": "sha512-oH7Cv6mHorvBkj5t3isP9wncgWABYLlQpoQZYOIFtWVwgsQatwoGVFHF6PoJzz+mTkt5UaJcyfDxFSo9Thvhdw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"is-url": "^1.2.4"
|
||||
},
|
||||
@@ -2119,6 +2143,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor-next/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-U2TlQ0Lpo6aLb0KD8oJgzG/rFAYO61cy+qbZu+t5lDfS3CECNjOhGIC3C7/dXIhiMQ8V/LY4cvrPt9R5G4vLjA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/event-emitter": "^0.3.3",
|
||||
"event-emitter": "^0.3.5",
|
||||
@@ -2148,6 +2173,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor-next/editor/-/editor-5.7.0.tgz",
|
||||
"integrity": "sha512-bxkw/TeWBJz7AU4qXZnx5tx/s1yzx8XdLmSRN9ev4btArKnfXR/6hr3dqxUSsyea8q//IpEv1qr9tc2ureEAsA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@uppy/core": "^2.1.1",
|
||||
"@uppy/xhr-upload": "^2.0.3",
|
||||
@@ -2439,6 +2465,7 @@
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
|
||||
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
},
|
||||
@@ -2446,6 +2473,19 @@
|
||||
"pnpm": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chartjs-chart-wordcloud": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/chartjs-chart-wordcloud/-/chartjs-chart-wordcloud-4.4.5.tgz",
|
||||
"integrity": "sha512-x7gdE5BZyj31+bjHbZ/0tX4hB6un7TQhvwdO5qHhqpmJCS6bONHjMuDzSjL/Qw2uI6CT2/U04LGqBREHwBiK3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-cloud": "^1.2.9",
|
||||
"d3-cloud": "^1.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"chart.js": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
@@ -2472,6 +2512,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
@@ -2591,6 +2632,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
|
||||
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emotion/hash": "~0.8.0",
|
||||
"csstype": "~3.0.5"
|
||||
@@ -2619,6 +2661,7 @@
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz",
|
||||
"integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -2765,6 +2808,21 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-cloud": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-cloud/-/d3-cloud-1.2.9.tgz",
|
||||
"integrity": "sha512-leL1GLneC9ZQtnV+6TGWrNlGfI1WX7S2arcTv2vae12DaXo5wjm6GBCkskXbrDlyOymd/A75Pyj1H37MW4BZ/Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-cloud/node_modules/d3-dispatch": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
|
||||
"integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-collection": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
|
||||
@@ -3047,6 +3105,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
@@ -3481,6 +3540,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
@@ -3541,6 +3601,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-4.0.6.tgz",
|
||||
"integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ssr-window": "^4.0.0"
|
||||
}
|
||||
@@ -4059,7 +4120,8 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
|
||||
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/is-url": {
|
||||
"version": "1.2.4",
|
||||
@@ -4235,37 +4297,43 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.foreach": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
||||
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.toarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lucide-vue-next": {
|
||||
"version": "0.543.0",
|
||||
@@ -4305,6 +4373,7 @@
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
@@ -4596,6 +4665,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
@@ -4673,6 +4743,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
@@ -5034,7 +5105,8 @@
|
||||
"version": "0.123.0",
|
||||
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.123.0.tgz",
|
||||
"integrity": "sha512-Oon3HR/QzJQBjuOUJT1jGGlp8Ff7t3Bkr/rJ2lDqxNT4H+cBnXpEVQ/si6hn1ZCHhD2xY/2N91PQoH/rD7kxTg==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/slate-history": {
|
||||
"version": "0.115.0",
|
||||
@@ -5050,6 +5122,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.3.tgz",
|
||||
"integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
@@ -5196,6 +5269,7 @@
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -5410,6 +5484,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz",
|
||||
"integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.34",
|
||||
"@vue/compiler-sfc": "3.5.34",
|
||||
@@ -5457,6 +5532,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.6.tgz",
|
||||
"integrity": "sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.28.6",
|
||||
"@vue-macros/common": "^3.1.1",
|
||||
@@ -5709,6 +5785,7 @@
|
||||
"resolved": "https://registry.npmmirror.com/yjs/-/yjs-13.6.30.tgz",
|
||||
"integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.99"
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"axios": "^1.16.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-chart-wordcloud": "^4.4.5",
|
||||
"client-zip": "^2.5.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"copy-text-to-clipboard": "^3.2.2",
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Python流程图作业 - 学情分析看板</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts-wordcloud@2.1.0/dist/echarts-wordcloud.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #f0f2f5;
|
||||
--card-bg: #ffffff;
|
||||
--primary: #1890ff;
|
||||
--text-main: #333;
|
||||
--text-secondary: #666;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.header h1 { color: var(--text-main); margin: 0; }
|
||||
.header p { color: var(--text-secondary); margin-top: 5px; }
|
||||
|
||||
/* 顶部概览卡片 */
|
||||
.overview-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
flex: 1;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.card:hover { transform: translateY(-5px); }
|
||||
|
||||
.stat-title { font-size: 14px; color: var(--text-secondary); }
|
||||
.stat-value { font-size: 28px; font-weight: bold; color: var(--text-main); margin-top: 10px; }
|
||||
.stat-sub { font-size: 12px; color: #52c41a; margin-top: 5px; }
|
||||
|
||||
/* 图表布局 */
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-left: 4px solid var(--primary);
|
||||
padding-left: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* 学生列表 */
|
||||
.student-list {
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
|
||||
th { background-color: #fafafa; color: var(--text-secondary); }
|
||||
|
||||
.tag {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tag-S { background: #fff7e6; color: #fa8c16; }
|
||||
.tag-A { background: #e6f7ff; color: #1890ff; }
|
||||
.tag-B { background: #f6ffed; color: #52c41a; }
|
||||
.tag-C { background: #fff1f0; color: #f5222d; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="overview-container">
|
||||
<div class="card">
|
||||
<div class="stat-title">班级平均分</div>
|
||||
<div class="stat-value" id="avgScore">0</div>
|
||||
<div class="stat-sub">↑ 比上周For循环 +2.5分</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-title">S+A 级别(卓越+优秀)人数</div>
|
||||
<div class="stat-value" id="countA">0</div>
|
||||
<div class="stat-sub">占比 35.7%</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="stat-title">未掌握核心难点</div>
|
||||
<div class="stat-value" style="color: #f5222d; font-size: 24px;">循环条件</div>
|
||||
<div class="stat-sub">需重点讲解 a<100 边界</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-grid">
|
||||
<div class="chart-box">
|
||||
<div class="chart-title">作业评级分布</div>
|
||||
<div id="pieChart" style="width: 100%; height: 340px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="chart-box">
|
||||
<div class="chart-title">薄弱知识点词云 (AI分析)</div>
|
||||
<div id="wordCloud" style="width: 100%; height: 340px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-grid">
|
||||
<div class="chart-box" style="grid-column: span 2;">
|
||||
<div class="chart-title">全班分数段统计</div>
|
||||
<div id="barChart" style="width: 100%; height: 340px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- 1. 模拟数据生成 (40位同学) ---
|
||||
const totalStudents = 40;
|
||||
const students = [];
|
||||
const familyNames = "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨";
|
||||
|
||||
// 精确分配等级人数:S 10% (4人), A 15% (6人), B 50% (20人), C 15% (6人), D 10% (4人)
|
||||
const gradeDistribution = {
|
||||
S: 4, // 10% = 4人
|
||||
A: 6, // 15% = 6人
|
||||
B: 20, // 50% = 20人
|
||||
C: 6, // 15% = 6人
|
||||
D: 4 // 10% = 4人
|
||||
};
|
||||
|
||||
let gradeCounts = { S: 0, A: 0, B: 0, C: 0, D: 0 };
|
||||
let totalScore = 0;
|
||||
|
||||
// 创建等级数组,确保精确分配
|
||||
let levelQueue = [];
|
||||
for (let level in gradeDistribution) {
|
||||
for (let i = 0; i < gradeDistribution[level]; i++) {
|
||||
levelQueue.push(level);
|
||||
}
|
||||
}
|
||||
// 打乱顺序
|
||||
for (let i = levelQueue.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[levelQueue[i], levelQueue[j]] = [levelQueue[j], levelQueue[i]];
|
||||
}
|
||||
|
||||
for (let i = 1; i <= totalStudents; i++) {
|
||||
let score;
|
||||
let level = levelQueue[i - 1];
|
||||
|
||||
// 根据等级生成对应分数范围
|
||||
if (level === 'S') {
|
||||
// S级:95-100
|
||||
score = Math.floor(Math.random() * (100 - 95 + 1) + 95);
|
||||
} else if (level === 'A') {
|
||||
// A级:85-94
|
||||
score = Math.floor(Math.random() * (95 - 85) + 85);
|
||||
} else if (level === 'B') {
|
||||
// B级:70-84
|
||||
score = Math.floor(Math.random() * (85 - 70) + 70);
|
||||
} else if (level === 'C') {
|
||||
// C级:60-69
|
||||
score = Math.floor(Math.random() * (70 - 60) + 60);
|
||||
} else {
|
||||
// D级:50-59
|
||||
score = Math.floor(Math.random() * (60 - 50) + 50);
|
||||
}
|
||||
|
||||
gradeCounts[level]++;
|
||||
totalScore += score;
|
||||
|
||||
let comment = "";
|
||||
if (level === 'S') comment = "完美!逻辑清晰,变量初始化正确,闭环完美,代码规范。";
|
||||
else if (level === 'A') comment = "逻辑清晰,变量初始化正确,闭环完美。";
|
||||
else if (level === 'B') comment = "整体逻辑正确,但部分连线方向有误。";
|
||||
else if (level === 'C') comment = "循环条件判断错误,导致死循环或无法进入。";
|
||||
else comment = "基础概念理解不足,需要重新学习。";
|
||||
|
||||
students.push({
|
||||
id: 2025000 + i,
|
||||
name: familyNames[i % familyNames.length] + "同学",
|
||||
score: score,
|
||||
level: level,
|
||||
comment: comment
|
||||
});
|
||||
}
|
||||
|
||||
// --- 2. 填充顶部数据 ---
|
||||
document.getElementById('avgScore').innerText = (totalScore / totalStudents).toFixed(1);
|
||||
const excellentCount = gradeCounts.S + gradeCounts.A; // S级和A级合计
|
||||
document.getElementById('countA').innerText = excellentCount;
|
||||
// 更新占比显示
|
||||
const excellentPercent = ((excellentCount / totalStudents) * 100).toFixed(1);
|
||||
const countACard = document.getElementById('countA').parentElement;
|
||||
countACard.querySelector('.stat-sub').innerText = `占比 ${excellentPercent}%`;
|
||||
|
||||
// --- 3. 初始化图表 ---
|
||||
|
||||
// A. 饼图 - 等级分布
|
||||
const pieChart = echarts.init(document.getElementById('pieChart'));
|
||||
pieChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { top: '5%', left: 'center' },
|
||||
color: ['#fa8c16', '#1890ff', '#52c41a', '#faad14', '#f5222d'],
|
||||
series: [{
|
||||
name: '评级占比',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: { label: { show: true, fontSize: 20, fontWeight: 'bold' } },
|
||||
data: [
|
||||
{ value: gradeCounts.S, name: 'S级 (卓越)' },
|
||||
{ value: gradeCounts.A, name: 'A级 (优秀)' },
|
||||
{ value: gradeCounts.B, name: 'B级 (良好)' },
|
||||
{ value: gradeCounts.C, name: 'C级 (待改进)' }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// B. 词云图 - 知识点掌握情况
|
||||
// 这里重点突出“循环条件”
|
||||
const wordChart = echarts.init(document.getElementById('wordCloud'));
|
||||
wordChart.setOption({
|
||||
tooltip: {},
|
||||
series: [{
|
||||
type: 'wordCloud',
|
||||
gridSize: 2,
|
||||
sizeRange: [12, 60], // 字体大小范围
|
||||
rotationRange: [-45, 45],
|
||||
shape: 'circle',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
textStyle: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontWeight: 'bold',
|
||||
color: function () {
|
||||
return 'rgb(' + [
|
||||
Math.round(Math.random() * 160),
|
||||
Math.round(Math.random() * 160),
|
||||
Math.round(Math.random() * 160)
|
||||
].join(',') + ')';
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{ name: '循环条件', value: 150, textStyle: { color: 'red' } }, // 核心痛点
|
||||
{ name: '变量初始化', value: 80 },
|
||||
{ name: 'i=i+1', value: 70 },
|
||||
{ name: 'a<100', value: 65 },
|
||||
{ name: '死循环', value: 60 },
|
||||
{ name: '连线方向', value: 50 },
|
||||
{ name: '退出逻辑', value: 45 },
|
||||
{ name: 'While语法', value: 40 },
|
||||
{ name: 'Print缩进', value: 35 },
|
||||
{ name: 'Yes/No分支', value: 30 },
|
||||
{ name: '流程结束符', value: 25 },
|
||||
{ name: '变量定义', value: 20 }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// C. 柱状图 - 分数段
|
||||
const barChart = echarts.init(document.getElementById('barChart'));
|
||||
// 简单的分段统计
|
||||
let ranges = { '90-100': 0, '80-89': 0, '70-79': 0, '60-69': 0, '<60': 0 };
|
||||
students.forEach(s => {
|
||||
if (s.score >= 90) ranges['90-100']++;
|
||||
else if (s.score >= 80) ranges['80-89']++;
|
||||
else if (s.score >= 70) ranges['70-79']++;
|
||||
else if (s.score >= 60) ranges['60-69']++;
|
||||
else ranges['<60']++;
|
||||
});
|
||||
|
||||
barChart.setOption({
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: Object.keys(ranges) },
|
||||
yAxis: { type: 'value' },
|
||||
series: [{
|
||||
name: '人数',
|
||||
type: 'bar',
|
||||
barWidth: '50%',
|
||||
data: Object.values(ranges),
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#83bff6' },
|
||||
{ offset: 0.5, color: '#188df0' },
|
||||
{ offset: 1, color: '#188df0' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
// 窗口缩放适配
|
||||
window.addEventListener('resize', function() {
|
||||
pieChart.resize();
|
||||
wordChart.resize();
|
||||
barChart.resize();
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -353,6 +353,20 @@ export function getFlowchartSubmissions(params: {
|
||||
return http.get("flowchart/submissions", { params })
|
||||
}
|
||||
|
||||
export function getFlowchartStatistics(
|
||||
duration: { start?: string; end: string },
|
||||
problemID?: string,
|
||||
username?: string,
|
||||
) {
|
||||
return http.get("admin/flowchart/statistics", {
|
||||
params: {
|
||||
...duration,
|
||||
problem_id: problemID,
|
||||
username,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function retryFlowchartSubmission(submissionId: string) {
|
||||
return http.post("flowchart/submission/retry", {
|
||||
submission_id: submissionId,
|
||||
|
||||
@@ -22,6 +22,7 @@ import { LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||
import { renderTableTitle } from "utils/renders"
|
||||
import ButtonWithSearch from "./components/ButtonWithSearch.vue"
|
||||
import StatisticsPanel from "shared/components/StatisticsPanel.vue"
|
||||
import FlowchartStatisticsPanel from "shared/components/FlowchartStatisticsPanel.vue"
|
||||
import SubmissionLink from "./components/SubmissionLink.vue"
|
||||
import SubmissionDetail from "./detail.vue"
|
||||
import Grade from "./components/Grade.vue"
|
||||
@@ -448,9 +449,14 @@ const flowchartColumns: DataTableColumn<FlowchartSubmissionListItem>[] = [
|
||||
preset="card"
|
||||
:style="{ maxWidth: isDesktop && '800px', maxHeight: '80vh' }"
|
||||
:content-style="{ overflow: 'auto' }"
|
||||
title="提交记录的统计"
|
||||
:title="query.language === 'Flowchart' ? '流程图提交的统计' : '提交记录的统计'"
|
||||
>
|
||||
<StatisticsPanel :problem="query.problem" :username="query.username" />
|
||||
<FlowchartStatisticsPanel
|
||||
v-if="query.language === 'Flowchart'"
|
||||
:problem="query.problem"
|
||||
:username="query.username"
|
||||
/>
|
||||
<StatisticsPanel v-else :problem="query.problem" :username="query.username" />
|
||||
</n-modal>
|
||||
<n-modal
|
||||
v-model:show="codePanel"
|
||||
|
||||
431
src/shared/components/FlowchartStatisticsPanel.vue
Normal file
431
src/shared/components/FlowchartStatisticsPanel.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<n-flex align="center">
|
||||
<n-input
|
||||
placeholder="用户(可选)"
|
||||
v-model:value="query.username"
|
||||
style="width: 150px"
|
||||
clearable
|
||||
/>
|
||||
<n-input
|
||||
placeholder="题号(可选)"
|
||||
v-model:value="query.problem"
|
||||
style="width: 120px"
|
||||
clearable
|
||||
/>
|
||||
<n-select
|
||||
style="width: 120px"
|
||||
v-model:value="query.duration"
|
||||
:options="durationOptions"
|
||||
/>
|
||||
<n-button type="primary" @click="handleStatistics">统计</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-empty
|
||||
v-if="data.total_count === 0"
|
||||
description="暂无数据"
|
||||
style="margin: 40px 0"
|
||||
/>
|
||||
|
||||
<template v-if="data.total_count > 0">
|
||||
<n-divider style="margin: 16px 0" />
|
||||
<n-flex justify="space-around">
|
||||
<div class="stat-item">
|
||||
<n-text>总提交</n-text>
|
||||
<n-gradient-text type="info" font-size="28">
|
||||
{{ data.total_count }}
|
||||
</n-gradient-text>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<n-text>平均分</n-text>
|
||||
<n-gradient-text type="primary" font-size="28">
|
||||
{{ data.avg_score }}
|
||||
</n-gradient-text>
|
||||
</div>
|
||||
<template v-if="data.person_count > 0">
|
||||
<div class="stat-item">
|
||||
<n-text>完成人数</n-text>
|
||||
<n-gradient-text type="error" font-size="28">
|
||||
{{ data.completed_count }}
|
||||
</n-gradient-text>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<n-text>班级人数</n-text>
|
||||
<n-gradient-text type="warning" font-size="28">
|
||||
{{ data.person_count }}
|
||||
</n-gradient-text>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<n-text>完成度</n-text>
|
||||
<n-gradient-text type="success" font-size="28">
|
||||
{{ completionRate }}
|
||||
</n-gradient-text>
|
||||
</div>
|
||||
</template>
|
||||
</n-flex>
|
||||
<n-divider style="margin: 16px 0" />
|
||||
|
||||
<n-tabs animated type="line">
|
||||
<n-tab-pane name="charts" tab="数据图表">
|
||||
<n-grid :cols="2" :x-gap="20" :y-gap="20" style="margin-top: 12px">
|
||||
<!-- 1. Grade pie chart -->
|
||||
<n-gi>
|
||||
<n-card title="等级分布">
|
||||
<Doughnut :data="gradeChartData" :options="doughnutOptions" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<!-- 3. Completion doughnut -->
|
||||
<n-gi v-if="data.person_count > 0">
|
||||
<n-card title="班级完成度">
|
||||
<Doughnut
|
||||
:data="completionChartData"
|
||||
:options="doughnutOptions"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<!-- 2. Radar chart -->
|
||||
<n-gi v-if="hasRadarData">
|
||||
<n-card title="四维评分雷达图">
|
||||
<Radar :data="radarChartData" :options="radarOptions" />
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<!-- 4. Word cloud -->
|
||||
<n-gi v-if="data.word_frequencies.length > 0">
|
||||
<n-card title="高频改进建议">
|
||||
<div class="wordcloud-container">
|
||||
<canvas ref="wordcloudCanvas"></canvas>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane
|
||||
v-if="data.data_unaccepted.length > 0"
|
||||
name="unaccepted"
|
||||
:tab="`未完成(${data.data_unaccepted.length})`"
|
||||
>
|
||||
<n-flex size="large" align="center" style="margin-top: 12px">
|
||||
<span
|
||||
v-for="item in data.data_unaccepted"
|
||||
:key="item.username"
|
||||
style="font-size: 24px"
|
||||
>
|
||||
{{ item.real_name }}
|
||||
</span>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatISO, sub, type Duration } from "date-fns"
|
||||
import { getFlowchartStatistics } from "oj/api"
|
||||
import { DURATION_OPTIONS } from "utils/constants"
|
||||
import { Doughnut, Radar } from "vue-chartjs"
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
ArcElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
RadialLinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
LinearScale,
|
||||
} from "chart.js"
|
||||
import {
|
||||
WordCloudController,
|
||||
WordElement,
|
||||
} from "chartjs-chart-wordcloud"
|
||||
|
||||
ChartJS.register(
|
||||
ArcElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
RadialLinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
LinearScale,
|
||||
WordCloudController,
|
||||
WordElement,
|
||||
)
|
||||
|
||||
interface Props {
|
||||
problem: string
|
||||
username: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const durationOptions: SelectOption[] = [
|
||||
{ label: "10分钟内", value: "minutes:10" },
|
||||
{ label: "20分钟内", value: "minutes:20" },
|
||||
{ label: "30分钟内", value: "minutes:30" },
|
||||
...DURATION_OPTIONS,
|
||||
{ label: "全部时段", value: "all" },
|
||||
]
|
||||
|
||||
const query = reactive({
|
||||
username: props.username,
|
||||
problem: props.problem,
|
||||
duration: durationOptions[0].value,
|
||||
})
|
||||
|
||||
interface StatisticsData {
|
||||
total_count: number
|
||||
avg_score: number
|
||||
grade_distribution: Record<string, number>
|
||||
criteria_averages: Record<string, { avg: number; max: number }>
|
||||
person_count: number
|
||||
completed_count: number
|
||||
word_frequencies: { word: string; count: number }[]
|
||||
data_unaccepted: { username: string; real_name: string }[]
|
||||
}
|
||||
|
||||
const data = reactive<StatisticsData>({
|
||||
total_count: 0,
|
||||
avg_score: 0,
|
||||
grade_distribution: {},
|
||||
criteria_averages: {},
|
||||
person_count: 0,
|
||||
completed_count: 0,
|
||||
word_frequencies: [],
|
||||
data_unaccepted: [],
|
||||
})
|
||||
|
||||
const wordcloudCanvas = useTemplateRef<HTMLCanvasElement>("wordcloudCanvas")
|
||||
let wordcloudChart: ChartJS | null = null
|
||||
|
||||
const completionRate = computed(() => {
|
||||
if (data.person_count <= 0) return "0%"
|
||||
const rate = Math.min(
|
||||
100,
|
||||
(data.completed_count / data.person_count) * 100,
|
||||
)
|
||||
return `${Math.round(rate * 100) / 100}%`
|
||||
})
|
||||
|
||||
const GRADE_COLORS: Record<string, { bg: string; border: string }> = {
|
||||
S: { bg: "rgba(24, 160, 88, 0.6)", border: "rgba(24, 160, 88, 1)" },
|
||||
A: { bg: "rgba(32, 128, 240, 0.6)", border: "rgba(32, 128, 240, 1)" },
|
||||
B: { bg: "rgba(240, 160, 32, 0.6)", border: "rgba(240, 160, 32, 1)" },
|
||||
C: { bg: "rgba(208, 48, 80, 0.6)", border: "rgba(208, 48, 80, 1)" },
|
||||
}
|
||||
|
||||
const gradeChartData = computed(() => {
|
||||
const grades = ["S", "A", "B", "C"]
|
||||
const counts = grades.map((g) => data.grade_distribution[g] || 0)
|
||||
const labels = grades.map(
|
||||
(g) => `${g}级 (${data.grade_distribution[g] || 0})`,
|
||||
)
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data: counts,
|
||||
backgroundColor: grades.map((g) => GRADE_COLORS[g].bg),
|
||||
borderColor: grades.map((g) => GRADE_COLORS[g].border),
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const completionChartData = computed(() => {
|
||||
const uncompleted = Math.max(0, data.person_count - data.completed_count)
|
||||
return {
|
||||
labels: ["已完成", "未完成"],
|
||||
datasets: [
|
||||
{
|
||||
data: [data.completed_count, uncompleted],
|
||||
backgroundColor: [
|
||||
"rgba(106, 176, 76, 0.6)",
|
||||
"rgba(255, 159, 64, 0.6)",
|
||||
],
|
||||
borderColor: ["rgba(106, 176, 76, 1)", "rgba(255, 159, 64, 1)"],
|
||||
borderWidth: 2,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const doughnutOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: "bottom" as const },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label(context: any) {
|
||||
const label = context.label || ""
|
||||
const value = context.parsed || 0
|
||||
const total = context.dataset.data.reduce(
|
||||
(a: number, b: number) => a + b,
|
||||
0,
|
||||
)
|
||||
const pct = ((value / total) * 100).toFixed(1)
|
||||
return `${label}: ${value} (${pct}%)`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const CRITERIA_ORDER = ["逻辑正确性", "完整性", "规范性", "清晰度"]
|
||||
|
||||
const hasRadarData = computed(() =>
|
||||
CRITERIA_ORDER.some((k) => k in data.criteria_averages),
|
||||
)
|
||||
|
||||
const radarChartData = computed(() => {
|
||||
const labels = CRITERIA_ORDER
|
||||
const values = CRITERIA_ORDER.map((k) => {
|
||||
const item = data.criteria_averages[k]
|
||||
if (!item) return 0
|
||||
return Math.round((item.avg / item.max) * 100)
|
||||
})
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "平均得分率 (%)",
|
||||
data: values,
|
||||
backgroundColor: "rgba(32, 128, 240, 0.2)",
|
||||
borderColor: "rgba(32, 128, 240, 1)",
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: "rgba(32, 128, 240, 1)",
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const radarOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: { stepSize: 20 },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label(context: any) {
|
||||
const key = CRITERIA_ORDER[context.dataIndex]
|
||||
const item = data.criteria_averages[key]
|
||||
if (!item) return ""
|
||||
return `${key}: ${item.avg}/${item.max} (${context.parsed.r}%)`
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const WORD_COLORS = [
|
||||
"#2080f0",
|
||||
"#18a058",
|
||||
"#f0a020",
|
||||
"#d03050",
|
||||
"#722ed1",
|
||||
"#13c2c2",
|
||||
"#1890ff",
|
||||
"#52c41a",
|
||||
"#faad14",
|
||||
"#f5222d",
|
||||
]
|
||||
|
||||
function renderWordCloud() {
|
||||
if (!wordcloudCanvas.value || data.word_frequencies.length === 0) return
|
||||
|
||||
if (wordcloudChart) {
|
||||
wordcloudChart.destroy()
|
||||
wordcloudChart = null
|
||||
}
|
||||
|
||||
const words = data.word_frequencies
|
||||
const maxCount = Math.max(...words.map((w) => w.count))
|
||||
|
||||
wordcloudChart = new ChartJS(wordcloudCanvas.value, {
|
||||
type: "wordCloud" as any,
|
||||
data: {
|
||||
labels: words.map((w) => w.word),
|
||||
datasets: [
|
||||
{
|
||||
label: "",
|
||||
data: words.map((w) => 10 + (w.count / maxCount) * 50),
|
||||
color: words.map(
|
||||
(_, i) => WORD_COLORS[i % WORD_COLORS.length],
|
||||
),
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label(context: any) {
|
||||
const word = words[context.dataIndex]
|
||||
return word ? `${word.word}: ${word.count}次` : ""
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const subOptions = computed<Duration>(() => {
|
||||
const dur =
|
||||
durationOptions.find((it) => it.value === query.duration) ??
|
||||
durationOptions[0]
|
||||
const x = dur.value!.toString().split(":")
|
||||
return { [x[0]]: parseInt(x[1]) }
|
||||
})
|
||||
|
||||
async function handleStatistics() {
|
||||
const current = Date.now()
|
||||
const end = formatISO(current)
|
||||
const duration =
|
||||
query.duration === "all"
|
||||
? { end }
|
||||
: { start: formatISO(sub(current, subOptions.value)), end }
|
||||
const res = await getFlowchartStatistics(
|
||||
duration,
|
||||
query.problem,
|
||||
query.username,
|
||||
)
|
||||
Object.assign(data, res.data)
|
||||
await nextTick()
|
||||
renderWordCloud()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (wordcloudChart) {
|
||||
wordcloudChart.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wordcloud-container {
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user