add raw data for radar
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2026-01-04 09:19:40 +08:00
parent a4ebf245c4
commit fab689ffdf
8 changed files with 551 additions and 399 deletions

188
package-lock.json generated
View File

@@ -40,11 +40,11 @@
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"y-codemirror.next": "^0.3.5", "y-codemirror.next": "^0.3.5",
"y-webrtc": "^10.3.0", "y-webrtc": "^10.3.0",
"yjs": "^13.6.28" "yjs": "^13.6.29"
}, },
"devDependencies": { "devDependencies": {
"@iconify/vue": "^5.0.0", "@iconify/vue": "^5.0.0",
"@rsbuild/core": "^1.6.15", "@rsbuild/core": "^1.7.1",
"@rsbuild/plugin-vue": "^1.2.2", "@rsbuild/plugin-vue": "^1.2.2",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",
@@ -581,9 +581,9 @@
} }
}, },
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.7.1", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.0.tgz",
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "integrity": "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -593,9 +593,9 @@
} }
}, },
"node_modules/@emnapi/runtime": { "node_modules/@emnapi/runtime": {
"version": "1.7.1", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.0.tgz",
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "integrity": "sha512-Z82FDl1ByxqPEPrAYYeTQVlx2FSHPe1qwX465c+96IRS3fTdSYRoJcRxg3g2fEG5I69z1dSEWQlNRRr0/677mg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -923,62 +923,62 @@
} }
}, },
"node_modules/@module-federation/error-codes": { "node_modules/@module-federation/error-codes": {
"version": "0.21.6", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.21.6.tgz", "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz",
"integrity": "sha512-MLJUCQ05KnoVl8xd6xs9a5g2/8U+eWmVxg7xiBMeR0+7OjdWUbHwcwgVFatRIwSZvFgKHfWEiI7wsU1q1XbTRQ==", "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@module-federation/runtime": { "node_modules/@module-federation/runtime": {
"version": "0.21.6", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.21.6.tgz", "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz",
"integrity": "sha512-+caXwaQqwTNh+CQqyb4mZmXq7iEemRDrTZQGD+zyeH454JAYnJ3s/3oDFizdH6245pk+NiqDyOOkHzzFQorKhQ==", "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@module-federation/error-codes": "0.21.6", "@module-federation/error-codes": "0.22.0",
"@module-federation/runtime-core": "0.21.6", "@module-federation/runtime-core": "0.22.0",
"@module-federation/sdk": "0.21.6" "@module-federation/sdk": "0.22.0"
} }
}, },
"node_modules/@module-federation/runtime-core": { "node_modules/@module-federation/runtime-core": {
"version": "0.21.6", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.21.6.tgz", "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz",
"integrity": "sha512-5Hd1Y5qp5lU/aTiK66lidMlM/4ji2gr3EXAtJdreJzkY+bKcI5+21GRcliZ4RAkICmvdxQU5PHPL71XmNc7Lsw==", "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@module-federation/error-codes": "0.21.6", "@module-federation/error-codes": "0.22.0",
"@module-federation/sdk": "0.21.6" "@module-federation/sdk": "0.22.0"
} }
}, },
"node_modules/@module-federation/runtime-tools": { "node_modules/@module-federation/runtime-tools": {
"version": "0.21.6", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.21.6.tgz", "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz",
"integrity": "sha512-fnP+ZOZTFeBGiTAnxve+axGmiYn2D60h86nUISXjXClK3LUY1krUfPgf6MaD4YDJ4i51OGXZWPekeMe16pkd8Q==", "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@module-federation/runtime": "0.21.6", "@module-federation/runtime": "0.22.0",
"@module-federation/webpack-bundler-runtime": "0.21.6" "@module-federation/webpack-bundler-runtime": "0.22.0"
} }
}, },
"node_modules/@module-federation/sdk": { "node_modules/@module-federation/sdk": {
"version": "0.21.6", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.21.6.tgz", "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz",
"integrity": "sha512-x6hARETb8iqHVhEsQBysuWpznNZViUh84qV2yE7AD+g7uIzHKiYdoWqj10posbo5XKf/147qgWDzKZoKoEP2dw==", "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@module-federation/webpack-bundler-runtime": { "node_modules/@module-federation/webpack-bundler-runtime": {
"version": "0.21.6", "version": "0.22.0",
"resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.21.6.tgz", "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz",
"integrity": "sha512-7zIp3LrcWbhGuFDTUMLJ2FJvcwjlddqhWGxi/MW3ur1a+HaO8v5tF2nl+vElKmbG1DFLU/52l3PElVcWf/YcsQ==", "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@module-federation/runtime": "0.21.6", "@module-federation/runtime": "0.22.0",
"@module-federation/sdk": "0.21.6" "@module-federation/sdk": "0.22.0"
} }
}, },
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
@@ -995,16 +995,16 @@
} }
}, },
"node_modules/@rsbuild/core": { "node_modules/@rsbuild/core": {
"version": "1.6.15", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.6.15.tgz", "resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.7.1.tgz",
"integrity": "sha512-LvoOF53PL6zXgdzEhgnnP51S4FseDFH1bHrobK4EK6zZX/tN8qgf5tdlmN7h4OkMv/Qs1oUfvj0QcLWSstnnvA==", "integrity": "sha512-ULIE/Qh+Ne80Pm/aUPbRHUvwvIzpap07jYNFB47azI8w5Q3sDEC4Gn574jsluT/42iNDsZTFADRBog9FEvtN9Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@rspack/core": "1.6.8", "@rspack/core": "1.7.0",
"@rspack/lite-tapable": "~1.1.0", "@rspack/lite-tapable": "~1.1.0",
"@swc/helpers": "^0.5.17", "@swc/helpers": "^0.5.18",
"core-js": "~3.47.0", "core-js": "~3.47.0",
"jiti": "^2.6.1" "jiti": "^2.6.1"
}, },
@@ -1030,28 +1030,28 @@
} }
}, },
"node_modules/@rspack/binding": { "node_modules/@rspack/binding": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.0.tgz",
"integrity": "sha512-lUeL4mbwGo+nqRKqFDCm9vH2jv9FNMVt1X8jqayWRcOCPlj/2UVMEFgqjR7Pp2vlvnTKq//31KbDBJmDZq31RQ==", "integrity": "sha512-xO+pZKG2dvU9CuRTTi+DcCc4p+CZhBJlvuYikBja/0a62cTntQV2PWV+/xU1a6Vbo89yNz158LR05nvjtKVwTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optionalDependencies": { "optionalDependencies": {
"@rspack/binding-darwin-arm64": "1.6.8", "@rspack/binding-darwin-arm64": "1.7.0",
"@rspack/binding-darwin-x64": "1.6.8", "@rspack/binding-darwin-x64": "1.7.0",
"@rspack/binding-linux-arm64-gnu": "1.6.8", "@rspack/binding-linux-arm64-gnu": "1.7.0",
"@rspack/binding-linux-arm64-musl": "1.6.8", "@rspack/binding-linux-arm64-musl": "1.7.0",
"@rspack/binding-linux-x64-gnu": "1.6.8", "@rspack/binding-linux-x64-gnu": "1.7.0",
"@rspack/binding-linux-x64-musl": "1.6.8", "@rspack/binding-linux-x64-musl": "1.7.0",
"@rspack/binding-wasm32-wasi": "1.6.8", "@rspack/binding-wasm32-wasi": "1.7.0",
"@rspack/binding-win32-arm64-msvc": "1.6.8", "@rspack/binding-win32-arm64-msvc": "1.7.0",
"@rspack/binding-win32-ia32-msvc": "1.6.8", "@rspack/binding-win32-ia32-msvc": "1.7.0",
"@rspack/binding-win32-x64-msvc": "1.6.8" "@rspack/binding-win32-x64-msvc": "1.7.0"
} }
}, },
"node_modules/@rspack/binding-darwin-arm64": { "node_modules/@rspack/binding-darwin-arm64": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.0.tgz",
"integrity": "sha512-e8CTQtzaeGnf+BIzR7wRMUwKfIg0jd/sxMRc1Vd0bCMHBhSN9EsGoMuJJaKeRrSmy2nwMCNWHIG+TvT1CEKg+A==", "integrity": "sha512-HMYrhvVh3sMRBXl6cSI2JqsvlHJKQ42qX+Sw4qbj7LeZBN6Gv4GjfL3cXRLUTdO37FOC0uLEUYgxVXetx/Y4sA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1063,9 +1063,9 @@
] ]
}, },
"node_modules/@rspack/binding-darwin-x64": { "node_modules/@rspack/binding-darwin-x64": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.0.tgz",
"integrity": "sha512-ku1XpTEPt6Za11zhpFWhfwrTQogcgi9RJrOUVC4FESiPO9aKyd4hJ+JiPgLY0MZOqsptK6vEAgOip+uDVXrCpg==", "integrity": "sha512-R/SoR04ySmHPqoIBGC+SjP9zRGjL1fS908mdwBvQ1RfFinKu7a/o/5rxH/vxUUsVQrHCyX+o7YXpfWq9xpvyQA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1077,9 +1077,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-arm64-gnu": { "node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.0.tgz",
"integrity": "sha512-fvZX6xZPvBT8qipSpvkKMX5M7yd2BSpZNCZXcefw6gA3uC7LI3gu+er0LrDXY1PtPzVuHTyDx+abwWpagV3PiQ==", "integrity": "sha512-jDCcso++qshu58+Iuo6oiL0XKuX04lDugL0qwrWHW8SS/EjZ2rc1J3yQx+XDW0PCQsfI2c9ji0IOW56PzW1hXQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1091,9 +1091,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-arm64-musl": { "node_modules/@rspack/binding-linux-arm64-musl": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.0.tgz",
"integrity": "sha512-++XMKcMNrt59HcFBLnRaJcn70k3X0GwkAegZBVpel8xYIAgvoXT5+L8P1ExId/yTFxqedaz8DbcxQnNmMozviw==", "integrity": "sha512-0W49s0SQQhr3hZ8Zd7Auyf2pv4OTBr6wQhgWUQ6XeeMEjB16KpAVypSK5Jpn1ON0v9jAPLdod+a255rz8/f3kg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1105,9 +1105,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-x64-gnu": { "node_modules/@rspack/binding-linux-x64-gnu": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.0.tgz",
"integrity": "sha512-tv3BWkTE1TndfX+DsE1rSTg8fBevCxujNZ3MlfZ22Wfy9x1FMXTJlWG8VIOXmaaJ1wUHzv8S7cE2YUUJ2LuiCg==", "integrity": "sha512-oFjzjTD1MmG0ucAaP0Wyg9eobrsnFwZjEHa7LwyzWDRBeC3GWAF9T04Bqd6Ba6DgASGzU0BjEJcUpjvtXxO95Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1119,9 +1119,9 @@
] ]
}, },
"node_modules/@rspack/binding-linux-x64-musl": { "node_modules/@rspack/binding-linux-x64-musl": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.0.tgz",
"integrity": "sha512-DCGgZ5/in1O3FjHWqXnDsncRy+48cMhfuUAAUyl0yDj1NpsZu9pP+xfGLvGcQTiYrVl7IH9Aojf1eShP/77WGA==", "integrity": "sha512-MNGslPLOsurdwOcoo6r0u8mLpw1ADar3hkx67WzwwMqYnem/Ky0aANJC2JvQHPC22mu01gCOukHYyEaUFTxcuw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1133,9 +1133,9 @@
] ]
}, },
"node_modules/@rspack/binding-wasm32-wasi": { "node_modules/@rspack/binding-wasm32-wasi": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.0.tgz",
"integrity": "sha512-VUwdhl/lI4m6o1OGCZ9JwtMjTV/yLY5VZTQdEPKb40JMTlmZ5MBlr5xk7ByaXXYHr6I+qnqEm73iMKQvg6iknw==", "integrity": "sha512-eaZzkGpxzVESmaX/UALMiQO+eNppe/i1VWQksGRfdoUu0rILqr/YDjsWFTcpbI9Dt3fg2kshHawBHxfwtxHcZQ==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@@ -1147,9 +1147,9 @@
} }
}, },
"node_modules/@rspack/binding-win32-arm64-msvc": { "node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.0.tgz",
"integrity": "sha512-23YX7zlOZlub+nPGDBUzktb4D5D6ETUAluKjXEeHIZ9m7fSlEYBnGL66YE+3t1DHXGd0OqsdwlvrNGcyo6EXDQ==", "integrity": "sha512-XFg4l7sOhupnpG0soOfzYLeF2cgpSJMenmjmdzd9y06CotTyVId0hNoS7y+A7hEP8XGf3YPbdiUL5UDp6+DRBA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1161,9 +1161,9 @@
] ]
}, },
"node_modules/@rspack/binding-win32-ia32-msvc": { "node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.0.tgz",
"integrity": "sha512-cFgRE3APxrY4AEdooVk2LtipwNNT/9mrnjdC5lVbsIsz+SxvGbZR231bxDJEqP15+RJOaD07FO1sIjINFqXMEg==", "integrity": "sha512-eWt2XV6la/c0IlU/18RlhQsqwHGShSypwA3kt4s/dpfOK0YB1h4f0fYeUZuvj2X0MIoJQGhMofMrgA35/IcAcw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1175,9 +1175,9 @@
] ]
}, },
"node_modules/@rspack/binding-win32-x64-msvc": { "node_modules/@rspack/binding-win32-x64-msvc": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.0.tgz",
"integrity": "sha512-cIuhVsZYd3o3Neo1JSAhJYw6BDvlxaBoqvgwRkG1rs0ExFmEmgYyG7ip9pFKnKNWph/tmW3rDYypmEfjs1is7g==", "integrity": "sha512-LOL5G8rfbAwlmusx+t98r9QzuGRz+L9Bg+8s5s6K/Qe64iemcNIuxGr5QLVq1jLa0SGNTeog4N21pAzlkWh4jw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1189,14 +1189,14 @@
] ]
}, },
"node_modules/@rspack/core": { "node_modules/@rspack/core": {
"version": "1.6.8", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.6.8.tgz", "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.0.tgz",
"integrity": "sha512-FolcIAH5FW4J2FET+qwjd1kNeFbCkd0VLuIHO0thyolEjaPSxw5qxG67DA7BZGm6PVcoiSgPLks1DL6eZ8c+fA==", "integrity": "sha512-uDxPQsPh/+2DnOISuKnUiXZ9M0y2G1BOsI0IesxPJGp42ME2QW7axbJfUqD3bwp4bi3RN2zqh56NgxU/XETQvA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@module-federation/runtime-tools": "0.21.6", "@module-federation/runtime-tools": "0.22.0",
"@rspack/binding": "1.6.8", "@rspack/binding": "1.7.0",
"@rspack/lite-tapable": "1.1.0" "@rspack/lite-tapable": "1.1.0"
}, },
"engines": { "engines": {
@@ -1219,9 +1219,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.17", "version": "0.5.18",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
@@ -6022,9 +6022,9 @@
} }
}, },
"node_modules/yjs": { "node_modules/yjs": {
"version": "13.6.28", "version": "13.6.29",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.28.tgz", "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.29.tgz",
"integrity": "sha512-EgnDOXs8+hBVm6mq3/S89Kiwzh5JRbn7w2wXwbrMRyKy/8dOFsLvuIfC+x19ZdtaDc0tA9rQmdZzbqqNHG44wA==", "integrity": "sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {

View File

@@ -42,11 +42,11 @@
"vue-router": "^4.6.4", "vue-router": "^4.6.4",
"y-codemirror.next": "^0.3.5", "y-codemirror.next": "^0.3.5",
"y-webrtc": "^10.3.0", "y-webrtc": "^10.3.0",
"yjs": "^13.6.28" "yjs": "^13.6.29"
}, },
"devDependencies": { "devDependencies": {
"@iconify/vue": "^5.0.0", "@iconify/vue": "^5.0.0",
"@rsbuild/core": "^1.6.15", "@rsbuild/core": "^1.7.1",
"@rsbuild/plugin-vue": "^1.2.2", "@rsbuild/plugin-vue": "^1.2.2",
"@types/canvas-confetti": "^1.9.0", "@types/canvas-confetti": "^1.9.0",
"@types/node": "^25.0.3", "@types/node": "^25.0.3",

View File

@@ -8,4 +8,4 @@ body {
.md-editor-dark div.vuepress-theme { .md-editor-dark div.vuepress-theme {
--md-theme-color: var(--n-text-color) !important; --md-theme-color: var(--n-text-color) !important;
} }

View File

@@ -143,7 +143,11 @@ export function getActivityRank(start: string) {
}) })
} }
export function getClassRank(offset: number, limit: number, grade?: number | null) { export function getClassRank(
offset: number,
limit: number,
grade?: number | null,
) {
return http.get("class_rank", { return http.get("class_rank", {
params: { offset, limit, grade }, params: { offset, limit, grade },
}) })
@@ -155,7 +159,11 @@ export function getUserClassRank(offset: number, limit: number) {
}) })
} }
export function getClassPK(classNames: string[], startTime?: string, endTime?: string) { export function getClassPK(
classNames: string[],
startTime?: string,
endTime?: string,
) {
const payload: any = { const payload: any = {
class_name: classNames, class_name: classNames,
} }

View File

@@ -1,121 +1,120 @@
<script setup lang="ts"> <script setup lang="ts">
import { getUserClassRank } from "oj/api" import { getUserClassRank } from "oj/api"
import { useUserStore } from "shared/store/user" import { useUserStore } from "shared/store/user"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
import { renderTableTitle } from "utils/renders" import { renderTableTitle } from "utils/renders"
import { NButton } from "naive-ui" import { NButton } from "naive-ui"
const userStore = useUserStore() const userStore = useUserStore()
const router = useRouter() const router = useRouter()
const message = useMessage() const message = useMessage()
interface UserRank { interface UserRank {
rank: number rank: number
username: string username: string
accepted_number: number accepted_number: number
submission_number: number submission_number: number
} }
const myRank = ref(-1) const myRank = ref(-1)
const totalUsers = ref(0) const totalUsers = ref(0)
const className = ref("") const className = ref("")
const data = ref<UserRank[]>([]) const data = ref<UserRank[]>([])
const total = ref(0) const total = ref(0)
const query = reactive({ const query = reactive({
limit: 20, limit: 20,
page: 1, page: 1,
}) })
const columns: DataTableColumn<UserRank>[] = [ const columns: DataTableColumn<UserRank>[] = [
{ {
title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"), title: renderTableTitle("排名", "streamline-emojis:flexed-biceps-1"),
key: "rank", key: "rank",
width: 100, width: 100,
align: "center", align: "center",
render: (row) => { render: (row) => {
if (row.rank === 1) return "🥇" if (row.rank === 1) return "🥇"
if (row.rank === 2) return "🥈" if (row.rank === 2) return "🥈"
if (row.rank === 3) return "🥉" if (row.rank === 3) return "🥉"
return row.rank return row.rank
}, },
}, },
{ {
title: renderTableTitle( title: renderTableTitle(
"用户名", "用户名",
"streamline-emojis:smiling-face-with-sunglasses", "streamline-emojis:smiling-face-with-sunglasses",
), ),
key: "username", key: "username",
width: 200, width: 200,
render: (row) => render: (row) =>
h( h(
NButton, NButton,
{ {
text: true, text: true,
type: "info", type: "info",
onClick: () => router.push("/user?name=" + row.username), onClick: () => router.push("/user?name=" + row.username),
}, },
() => row.username, () => row.username,
), ),
}, },
{ {
title: renderTableTitle("AC数", "streamline-emojis:raised-fist-1"), title: renderTableTitle("AC数", "streamline-emojis:raised-fist-1"),
key: "accepted_number", key: "accepted_number",
width: 120, width: 120,
align: "center", align: "center",
}, },
{ {
title: renderTableTitle("提交数", "streamline-emojis:rocket"), title: renderTableTitle("提交数", "streamline-emojis:rocket"),
key: "submission_number", key: "submission_number",
width: 120, width: 120,
align: "center", align: "center",
}, },
] ]
async function init() { async function init() {
const user = userStore.user const user = userStore.user
if (!user || !user.class_name) { if (!user || !user.class_name) {
message.warning("您没有班级信息") message.warning("您没有班级信息")
return return
} }
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getUserClassRank(offset, query.limit) const res = await getUserClassRank(offset, query.limit)
myRank.value = res.data.my_rank myRank.value = res.data.my_rank
totalUsers.value = res.data.total_users totalUsers.value = res.data.total_users
className.value = res.data.class_name className.value = res.data.class_name
data.value = res.data.ranks.results data.value = res.data.ranks.results
total.value = res.data.ranks.total total.value = res.data.ranks.total
} }
watch(() => query.page, init) watch(() => query.page, init)
watch( watch(
() => query.limit, () => query.limit,
() => { () => {
query.page = 1 query.page = 1
init() init()
}, },
) )
onMounted(init) onMounted(init)
</script> </script>
<template> <template>
<n-flex vertical size="large"> <n-flex vertical size="large">
<n-h2>我的班级排名</n-h2> <n-h2>我的班级排名</n-h2>
<n-alert v-if="className" type="info"> <n-alert v-if="className" type="info">
班级{{ className }} | 我的排名{{ 班级{{ className }} | 我的排名{{
myRank > 0 ? `${myRank}` : "暂无排名" myRank > 0 ? `${myRank}` : "暂无排名"
}} }}
| 班级总人数{{ totalUsers }} | 班级总人数{{ totalUsers }}
</n-alert> </n-alert>
<n-alert v-else type="warning"> 您还没有加入班级 </n-alert> <n-alert v-else type="warning"> 您还没有加入班级 </n-alert>
<n-data-table :data="data" :columns="columns" /> <n-data-table :data="data" :columns="columns" />
<Pagination <Pagination
:total="total" :total="total"
v-model:page="query.page" v-model:page="query.page"
v-model:limit="query.limit" v-model:limit="query.limit"
/> />
</n-flex> </n-flex>
</template> </template>

View File

@@ -384,6 +384,14 @@ const radarChartData = computed(() => {
const datasets = comparisons.value.map((c, index) => { const datasets = comparisons.value.map((c, index) => {
const color = getClassColor(index) const color = getClassColor(index)
const rawData = [
c.total_ac,
c.avg_ac,
c.median_ac,
c.excellent_rate,
c.pass_rate,
c.active_rate,
]
return { return {
label: c.class_name, label: c.class_name,
data: [ data: [
@@ -394,6 +402,7 @@ const radarChartData = computed(() => {
c.pass_rate, c.pass_rate,
c.active_rate, c.active_rate,
], ],
rawData,
backgroundColor: color.bg, backgroundColor: color.bg,
borderColor: color.border, borderColor: color.border,
borderWidth: 2, borderWidth: 2,
@@ -466,6 +475,28 @@ const radarChartOptions = {
legend: { legend: {
position: "bottom" as const, position: "bottom" as const,
}, },
tooltip: {
callbacks: {
label: function (context: any) {
const dataset = context.dataset as any
const rawValue = dataset?.rawData?.[context.dataIndex]
const metric = context.label || ""
const isRate = context.dataIndex >= 3
if (rawValue === undefined || rawValue === null) {
return `${dataset.label || ""}: ${context.parsed.r?.toFixed(2) ?? ""}`
}
const formatted = Number.isFinite(rawValue)
? isRate
? rawValue.toFixed(1)
: Number.isInteger(rawValue)
? rawValue.toString()
: rawValue.toFixed(2)
: String(rawValue)
const suffix = isRate ? "%" : ""
return `${dataset.label || ""} - ${metric}: ${formatted}${suffix}`
},
},
},
}, },
scales: { scales: {
r: { r: {
@@ -485,7 +516,10 @@ const radarChartOptions = {
<n-h2 style="margin-bottom: 0">班级PK</n-h2> <n-h2 style="margin-bottom: 0">班级PK</n-h2>
<n-flex :wrap="false" align="flex-start" :size="16"> <n-flex :wrap="false" align="flex-start" :size="16">
<n-form-item label="选择班级至少2个" style="width: 300px; margin-bottom: 0"> <n-form-item
label="选择班级至少2个"
style="width: 300px; margin-bottom: 0"
>
<n-select <n-select
v-model:value="selectedClasses" v-model:value="selectedClasses"
:options="classOptions" :options="classOptions"
@@ -494,7 +528,10 @@ const radarChartOptions = {
/> />
</n-form-item> </n-form-item>
<n-form-item label="时间段(可选)" style="width: 200px; margin-bottom: 0"> <n-form-item
label="时间段(可选)"
style="width: 200px; margin-bottom: 0"
>
<n-select <n-select
v-model:value="duration" v-model:value="duration"
:options="timeRangeOptions" :options="timeRangeOptions"
@@ -504,7 +541,12 @@ const radarChartOptions = {
/> />
</n-form-item> </n-form-item>
<n-button type="primary" @click="compare" :loading="loading" style="margin-top: 26px"> <n-button
type="primary"
@click="compare"
:loading="loading"
style="margin-top: 26px"
>
开始PK 开始PK
</n-button> </n-button>
</n-flex> </n-flex>
@@ -516,7 +558,10 @@ const radarChartOptions = {
:x-gap="16" :x-gap="16"
:y-gap="16" :y-gap="16"
> >
<n-gi v-for="(classData, index) in comparisons" :key="classData.class_name"> <n-gi
v-for="(classData, index) in comparisons"
:key="classData.class_name"
>
<n-card <n-card
:title="classData.class_name" :title="classData.class_name"
:bordered="true" :bordered="true"
@@ -544,7 +589,12 @@ const radarChartOptions = {
<!-- AC核心指标 - 突出显示便于横向对比 --> <!-- AC核心指标 - 突出显示便于横向对比 -->
<n-grid :cols="5" :x-gap="8" responsive="screen"> <n-grid :cols="5" :x-gap="8" responsive="screen">
<n-gi> <n-gi>
<n-statistic label="总AC数" :value="classData.total_ac" size="large" class="stat-total-ac"> <n-statistic
label="总AC数"
:value="classData.total_ac"
size="large"
class="stat-total-ac"
>
<template #suffix> <template #suffix>
<Icon icon="streamline-emojis:raised-fist-1" width="20" /> <Icon icon="streamline-emojis:raised-fist-1" width="20" />
</template> </template>
@@ -603,43 +653,71 @@ const radarChartOptions = {
<n-divider style="margin: 12px 0" /> <n-divider style="margin: 12px 0" />
<!-- 详细统计 - 紧凑布局统一格式 --> <!-- 详细统计 - 紧凑布局统一格式 -->
<n-descriptions bordered :column="2" size="small" label-placement="left"> <n-descriptions
bordered
:column="2"
size="small"
label-placement="left"
>
<!-- 分位数统计 --> <!-- 分位数统计 -->
<n-descriptions-item label="第一四分位数(Q1)"> <n-descriptions-item label="第一四分位数(Q1)">
<span style="color: #9254de; font-weight: 500">{{ classData.q1_ac.toFixed(2) }}</span> <span style="color: #9254de; font-weight: 500">{{
classData.q1_ac.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="第三四分位数(Q3)"> <n-descriptions-item label="第三四分位数(Q3)">
<span style="color: #f759ab; font-weight: 500">{{ classData.q3_ac.toFixed(2) }}</span> <span style="color: #f759ab; font-weight: 500">{{
classData.q3_ac.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="四分位距(IQR)"> <n-descriptions-item label="四分位距(IQR)">
<span style="color: #13c2c2; font-weight: 500">{{ classData.iqr.toFixed(2) }}</span> <span style="color: #13c2c2; font-weight: 500">{{
classData.iqr.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="标准差"> <n-descriptions-item label="标准差">
<span style="color: #fa8c16; font-weight: 500">{{ classData.std_dev.toFixed(2) }}</span> <span style="color: #fa8c16; font-weight: 500">{{
classData.std_dev.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<!-- 分层统计 --> <!-- 分层统计 -->
<n-descriptions-item label="前10名平均"> <n-descriptions-item label="前10名平均">
<span style="color: #cf1322; font-weight: 600">{{ classData.top_10_avg.toFixed(2) }}</span> <span style="color: #cf1322; font-weight: 600">{{
classData.top_10_avg.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="后10名平均"> <n-descriptions-item label="后10名平均">
<span style="color: #096dd9; font-weight: 500">{{ classData.bottom_10_avg.toFixed(2) }}</span> <span style="color: #096dd9; font-weight: 500">{{
classData.bottom_10_avg.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="前25%平均"> <n-descriptions-item label="前25%平均">
<span style="color: #f5222d; font-weight: 600">{{ classData.top_25_avg.toFixed(2) }}</span> <span style="color: #f5222d; font-weight: 600">{{
classData.top_25_avg.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="后25%平均"> <n-descriptions-item label="后25%平均">
<span style="color: #531dab; font-weight: 500">{{ classData.bottom_25_avg.toFixed(2) }}</span> <span style="color: #531dab; font-weight: 500">{{
classData.bottom_25_avg.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<!-- 人数 --> <!-- 人数 -->
<n-descriptions-item label="人数"> <n-descriptions-item label="人数">
<span style="color: #1890ff; font-weight: 600">{{ classData.user_count }}</span> <span style="color: #1890ff; font-weight: 600">{{
classData.user_count
}}</span>
</n-descriptions-item> </n-descriptions-item>
</n-descriptions> </n-descriptions>
<!-- 比率统计 - 使用进度条图表 --> <!-- 比率统计 - 使用进度条图表 -->
<n-card size="small" title="比率统计" embedded style="margin-top: 12px"> <n-card
size="small"
title="比率统计"
embedded
style="margin-top: 12px"
>
<n-space vertical :size="10"> <n-space vertical :size="10">
<n-progress <n-progress
type="line" type="line"
@@ -680,22 +758,37 @@ const radarChartOptions = {
<template <template
v-if="hasTimeRange && classData.recent_total_ac !== undefined" v-if="hasTimeRange && classData.recent_total_ac !== undefined"
> >
<n-descriptions bordered :column="2" size="small" label-placement="left" style="margin-top: 12px"> <n-descriptions
bordered
:column="2"
size="small"
label-placement="left"
style="margin-top: 12px"
>
<n-descriptions-item label="时间段总AC"> <n-descriptions-item label="时间段总AC">
<span style="color: #ff7875; font-weight: 600">{{ classData.recent_total_ac }}</span> <span style="color: #ff7875; font-weight: 600">{{
classData.recent_total_ac
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="时间段平均AC"> <n-descriptions-item label="时间段平均AC">
<span style="color: #73d13d; font-weight: 600">{{ classData.recent_avg_ac?.toFixed(2) }}</span> <span style="color: #73d13d; font-weight: 600">{{
classData.recent_avg_ac?.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="时间段中位数AC"> <n-descriptions-item label="时间段中位数AC">
<span style="color: #ffc53d; font-weight: 600">{{ classData.recent_median_ac?.toFixed(2) }}</span> <span style="color: #ffc53d; font-weight: 600">{{
classData.recent_median_ac?.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="时间段前10名平均"> <n-descriptions-item label="时间段前10名平均">
<span style="color: #ff4d4f; font-weight: 600">{{ classData.recent_top_10_avg?.toFixed(2) }}</span> <span style="color: #ff4d4f; font-weight: 600">{{
classData.recent_top_10_avg?.toFixed(2)
}}</span>
</n-descriptions-item> </n-descriptions-item>
<n-descriptions-item label="活跃学生数" :span="2"> <n-descriptions-item label="活跃学生数" :span="2">
<span style="color: #1890ff; font-weight: 600">{{ classData.recent_active_count }}</span> <span style="color: #1890ff; font-weight: 600">{{
classData.recent_active_count
}}</span>
</n-descriptions-item> </n-descriptions-item>
</n-descriptions> </n-descriptions>
</template> </template>
@@ -827,7 +920,11 @@ const radarChartOptions = {
</template> </template>
<!-- 对比表格 --> <!-- 对比表格 -->
<n-card v-if="comparisons.length > 0" title="对比表格" style="margin-top: 20px"> <n-card
v-if="comparisons.length > 0"
title="对比表格"
style="margin-top: 20px"
>
<n-data-table <n-data-table
:data="comparisons" :data="comparisons"
:columns="[ :columns="[
@@ -842,48 +939,93 @@ const radarChartOptions = {
title: '人数', title: '人数',
key: 'user_count', key: 'user_count',
width: 80, width: 80,
render: (row) => h('span', { style: { color: '#1890ff', fontWeight: '600' } }, row.user_count), render: (row) =>
h(
'span',
{ style: { color: '#1890ff', fontWeight: '600' } },
row.user_count,
),
}, },
{ {
title: '总AC数', title: '总AC数',
key: 'total_ac', key: 'total_ac',
width: 100, width: 100,
render: (row) => h('span', { style: { color: '#ff4d4f', fontWeight: '600' } }, row.total_ac), render: (row) =>
h(
'span',
{ style: { color: '#ff4d4f', fontWeight: '600' } },
row.total_ac,
),
}, },
{ {
title: '平均AC', title: '平均AC',
key: 'avg_ac', key: 'avg_ac',
render: (row) => h('span', { style: { color: '#52c41a', fontWeight: '600' } }, row.avg_ac.toFixed(2)), render: (row) =>
h(
'span',
{ style: { color: '#52c41a', fontWeight: '600' } },
row.avg_ac.toFixed(2),
),
}, },
{ {
title: '中位数AC', title: '中位数AC',
key: 'median_ac', key: 'median_ac',
render: (row) => h('span', { style: { color: '#fa8c16', fontWeight: '600' } }, row.median_ac.toFixed(2)), render: (row) =>
h(
'span',
{ style: { color: '#fa8c16', fontWeight: '600' } },
row.median_ac.toFixed(2),
),
}, },
{ {
title: '前10名平均', title: '前10名平均',
key: 'top_10_avg', key: 'top_10_avg',
render: (row) => h('span', { style: { color: '#cf1322', fontWeight: '600' } }, row.top_10_avg.toFixed(2)), render: (row) =>
h(
'span',
{ style: { color: '#cf1322', fontWeight: '600' } },
row.top_10_avg.toFixed(2),
),
}, },
{ {
title: '后10名平均', title: '后10名平均',
key: 'bottom_10_avg', key: 'bottom_10_avg',
render: (row) => h('span', { style: { color: '#096dd9', fontWeight: '500' } }, row.bottom_10_avg.toFixed(2)), render: (row) =>
h(
'span',
{ style: { color: '#096dd9', fontWeight: '500' } },
row.bottom_10_avg.toFixed(2),
),
}, },
{ {
title: '优秀率', title: '优秀率',
key: 'excellent_rate', key: 'excellent_rate',
render: (row) => h('span', { style: { color: '#faad14', fontWeight: '600' } }, row.excellent_rate.toFixed(1) + '%'), render: (row) =>
h(
'span',
{ style: { color: '#faad14', fontWeight: '600' } },
row.excellent_rate.toFixed(1) + '%',
),
}, },
{ {
title: '及格率', title: '及格率',
key: 'pass_rate', key: 'pass_rate',
render: (row) => h('span', { style: { color: '#52c41a', fontWeight: '600' } }, row.pass_rate.toFixed(1) + '%'), render: (row) =>
h(
'span',
{ style: { color: '#52c41a', fontWeight: '600' } },
row.pass_rate.toFixed(1) + '%',
),
}, },
{ {
title: '参与度', title: '参与度',
key: 'active_rate', key: 'active_rate',
render: (row) => h('span', { style: { color: '#1890ff', fontWeight: '600' } }, row.active_rate.toFixed(1) + '%'), render: (row) =>
h(
'span',
{ style: { color: '#1890ff', fontWeight: '600' } },
row.active_rate.toFixed(1) + '%',
),
}, },
]" ]"
/> />
@@ -946,4 +1088,3 @@ const radarChartOptions = {
font-weight: 600; font-weight: 600;
} }
</style> </style>

View File

@@ -1,144 +1,144 @@
<script setup lang="ts"> <script setup lang="ts">
import { getClassRank } from "oj/api" import { getClassRank } from "oj/api"
import Pagination from "shared/components/Pagination.vue" import Pagination from "shared/components/Pagination.vue"
interface ClassRank { interface ClassRank {
rank: number rank: number
class_name: string class_name: string
user_count: number user_count: number
total_ac: number total_ac: number
total_submission: number total_submission: number
avg_ac: number avg_ac: number
ac_rate: number ac_rate: number
} }
const data = ref<ClassRank[]>([]) const data = ref<ClassRank[]>([])
const total = ref(0) const total = ref(0)
const query = reactive({ const query = reactive({
limit: 10, limit: 10,
page: 1, page: 1,
grade: null as number | null, grade: null as number | null,
}) })
const gradeOptions = [ const gradeOptions = [
{ label: "24年级", value: 24 }, { label: "24年级", value: 24 },
{ label: "23年级", value: 23 }, { label: "23年级", value: 23 },
{ label: "22年级", value: 22 }, { label: "22年级", value: 22 },
{ label: "21年级", value: 21 }, { label: "21年级", value: 21 },
{ label: "20年级", value: 20 }, { label: "20年级", value: 20 },
] ]
const columns: DataTableColumn<ClassRank>[] = [ const columns: DataTableColumn<ClassRank>[] = [
{ {
title: "排名", title: "排名",
key: "rank", key: "rank",
width: 100, width: 100,
titleAlign: "center", titleAlign: "center",
align: "center", align: "center",
render: (row) => { render: (row) => {
if (row.rank === 1) return "🥇" if (row.rank === 1) return "🥇"
if (row.rank === 2) return "🥈" if (row.rank === 2) return "🥈"
if (row.rank === 3) return "🥉" if (row.rank === 3) return "🥉"
return row.rank return row.rank
}, },
}, },
{ {
title: "班级", title: "班级",
key: "class_name", key: "class_name",
width: 200, width: 200,
titleAlign: "center", titleAlign: "center",
align: "center", align: "center",
}, },
{ {
title: "人数", title: "人数",
key: "user_count", key: "user_count",
width: 100, width: 100,
titleAlign: "center", titleAlign: "center",
align: "center", align: "center",
}, },
{ {
title: "总AC数", title: "总AC数",
key: "total_ac", key: "total_ac",
width: 120, width: 120,
titleAlign: "center", titleAlign: "center",
align: "center", align: "center",
}, },
{ {
title: "总提交数", title: "总提交数",
key: "total_submission", key: "total_submission",
width: 120, width: 120,
titleAlign: "center", titleAlign: "center",
align: "center", align: "center",
}, },
{ {
title: "平均AC数", title: "平均AC数",
key: "avg_ac", key: "avg_ac",
width: 120, width: 120,
titleAlign: "center", titleAlign: "center",
align: "center", align: "center",
}, },
{ {
title: "正确率", title: "正确率",
key: "ac_rate", key: "ac_rate",
width: 100, width: 100,
titleAlign: "center", titleAlign: "center",
align: "center", align: "center",
render: (row) => `${row.ac_rate}%`, render: (row) => `${row.ac_rate}%`,
}, },
] ]
async function init() { async function init() {
if (query.grade === null) { if (query.grade === null) {
data.value = [] data.value = []
total.value = 0 total.value = 0
return return
} }
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getClassRank(offset, query.limit, query.grade) const res = await getClassRank(offset, query.limit, query.grade)
data.value = res.data.results data.value = res.data.results
total.value = res.data.total total.value = res.data.total
} }
watch(() => query.page, init) watch(() => query.page, init)
watch( watch(
() => query.limit, () => query.limit,
() => { () => {
query.page = 1 query.page = 1
init() init()
}, },
) )
watch( watch(
() => query.grade, () => query.grade,
() => { () => {
query.page = 1 query.page = 1
init() init()
}, },
) )
onMounted(() => { onMounted(() => {
if (query.grade !== null) { if (query.grade !== null) {
init() init()
} }
}) })
</script> </script>
<template> <template>
<n-flex justify="center"> <n-flex justify="center">
<n-h2>班级排名</n-h2> <n-h2>班级排名</n-h2>
</n-flex> </n-flex>
<n-flex justify="center" style="margin-bottom: 16px"> <n-flex justify="center" style="margin-bottom: 16px">
<n-select <n-select
v-model:value="query.grade" v-model:value="query.grade"
placeholder="选择年级" placeholder="选择年级"
clearable clearable
style="width: 200px" style="width: 200px"
:options="gradeOptions" :options="gradeOptions"
/> />
</n-flex> </n-flex>
<n-data-table :data="data" :columns="columns" /> <n-data-table :data="data" :columns="columns" />
<Pagination <Pagination
:total="total" :total="total"
v-model:page="query.page" v-model:page="query.page"
v-model:limit="query.limit" v-model:limit="query.limit"
/> />
</template> </template>

View File

@@ -136,7 +136,11 @@ const menus = computed<MenuOption[]>(() => [
}, },
{ {
label: () => label: () =>
h(RouterLink, { to: "/class/my-rank" }, { default: () => "我的排名" }), h(
RouterLink,
{ to: "/class/my-rank" },
{ default: () => "我的排名" },
),
key: "my-rank", key: "my-rank",
show: userStore.isAuthed, show: userStore.isAuthed,
}, },