1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-08-31 20:29:23 +00:00

添加考试看板功能

This commit is contained in:
SunWuyuan 2025-08-30 14:55:55 +08:00
parent cd10d0f49a
commit f546621ab9
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
10 changed files with 2433 additions and 156 deletions

View File

@ -12,17 +12,17 @@
"dependencies": {
"@mdi/font": "7.4.47",
"@microsoft/clarity": "^1.0.0",
"axios": "^1.8.4",
"idb": "^8.0.2",
"js-base64": "^3.7.7",
"axios": "^1.11.0",
"idb": "^8.0.3",
"js-base64": "^3.7.8",
"js-yaml": "^4.1.0",
"pinyin-pro": "^3.26.0",
"pinyin-pro": "^3.27.0",
"ratelimit-header-parser": "^0.1.0",
"roboto-fontface": "*",
"typewriter-effect": "^2.21.0",
"uuid": "^9.0.1",
"vue": "^3.4.31",
"vuetify": "^3.8.0"
"vue": "^3.5.20",
"vuetify": "^3.9.6"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
@ -34,7 +34,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.4.0",
"eslint-plugin-vue": "^9.30.0",
"pinia": "^3.0.1",
"pinia": "^3.0.3",
"sass": "1.86.3",
"sass-embedded": "^1.86.3",
"unplugin-auto-import": "^19.1.2",
@ -42,9 +42,9 @@
"unplugin-vue-components": "^28.4.1",
"unplugin-vue-router": "^0.12.0",
"vite": "^5.4.17",
"vite-plugin-pwa": "^1.0.0",
"vite-plugin-pwa": "^1.0.3",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "^2.1.1",
"vue-router": "^4.5.0"
"vite-plugin-vuetify": "^2.1.2",
"vue-router": "^4.5.1"
}
}

354
pnpm-lock.yaml generated
View File

@ -15,20 +15,20 @@ importers:
specifier: ^1.0.0
version: 1.0.0
axios:
specifier: ^1.8.4
version: 1.8.4
specifier: ^1.11.0
version: 1.11.0
idb:
specifier: ^8.0.2
version: 8.0.2
specifier: ^8.0.3
version: 8.0.3
js-base64:
specifier: ^3.7.7
version: 3.7.7
specifier: ^3.7.8
version: 3.7.8
js-yaml:
specifier: ^4.1.0
version: 4.1.0
pinyin-pro:
specifier: ^3.26.0
version: 3.26.0
specifier: ^3.27.0
version: 3.27.0
ratelimit-header-parser:
specifier: ^0.1.0
version: 0.1.0
@ -42,11 +42,11 @@ importers:
specifier: ^9.0.1
version: 9.0.1
vue:
specifier: ^3.4.31
version: 3.5.13
specifier: ^3.5.20
version: 3.5.20
vuetify:
specifier: ^3.8.0
version: 3.8.0(vite-plugin-vuetify@2.1.1)(vue@3.5.13)
specifier: ^3.9.6
version: 3.9.6(vite-plugin-vuetify@2.1.2)(vue@3.5.20)
devDependencies:
'@eslint/js':
specifier: ^9.14.0
@ -56,7 +56,7 @@ importers:
version: 1.0.0
'@vitejs/plugin-vue':
specifier: ^5.2.3
version: 5.2.3(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.13)
version: 5.2.3(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.20)
eslint:
specifier: ^9.14.0
version: 9.15.0(jiti@2.4.2)
@ -76,8 +76,8 @@ importers:
specifier: ^9.30.0
version: 9.31.0(eslint@9.15.0(jiti@2.4.2))
pinia:
specifier: ^3.0.1
version: 3.0.1(vue@3.5.13)
specifier: ^3.0.3
version: 3.0.3(vue@3.5.20)
sass:
specifier: 1.86.3
version: 1.86.3
@ -92,25 +92,25 @@ importers:
version: 1.3.1(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))
unplugin-vue-components:
specifier: ^28.4.1
version: 28.4.1(@babel/parser@7.27.0)(vue@3.5.13)
version: 28.4.1(@babel/parser@7.28.3)(vue@3.5.20)
unplugin-vue-router:
specifier: ^0.12.0
version: 0.12.0(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13)
version: 0.12.0(vue-router@4.5.1(vue@3.5.20))(vue@3.5.20)
vite:
specifier: ^5.4.17
version: 5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0)
vite-plugin-pwa:
specifier: ^1.0.0
version: 1.0.0(@vite-pwa/assets-generator@1.0.0)(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(workbox-build@7.3.0)(workbox-window@7.3.0)
specifier: ^1.0.3
version: 1.0.3(@vite-pwa/assets-generator@1.0.0)(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(workbox-build@7.3.0)(workbox-window@7.3.0)
vite-plugin-vue-layouts:
specifier: ^0.11.0
version: 0.11.0(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13)
version: 0.11.0(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue-router@4.5.1(vue@3.5.20))(vue@3.5.20)
vite-plugin-vuetify:
specifier: ^2.1.1
version: 2.1.1(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.13)(vuetify@3.8.0)
specifier: ^2.1.2
version: 2.1.2(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.20)(vuetify@3.9.6)
vue-router:
specifier: ^4.5.0
version: 4.5.0(vue@3.5.13)
specifier: ^4.5.1
version: 4.5.1(vue@3.5.20)
packages:
@ -207,10 +207,18 @@ packages:
resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.25.9':
resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-identifier@7.27.1':
resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
engines: {node: '>=6.9.0'}
'@babel/helper-validator-option@7.25.9':
resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
engines: {node: '>=6.9.0'}
@ -233,6 +241,11 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.28.3':
resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9':
resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==}
engines: {node: '>=6.9.0'}
@ -618,6 +631,10 @@ packages:
resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==}
engines: {node: '>=6.9.0'}
'@babel/types@7.28.2':
resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==}
engines: {node: '>=6.9.0'}
'@bufbuild/protobuf@2.2.2':
resolution: {integrity: sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==}
@ -1249,15 +1266,27 @@ packages:
'@vue/compiler-core@3.5.13':
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
'@vue/compiler-core@3.5.20':
resolution: {integrity: sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==}
'@vue/compiler-dom@3.5.13':
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
'@vue/compiler-dom@3.5.20':
resolution: {integrity: sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==}
'@vue/compiler-sfc@3.5.13':
resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
'@vue/compiler-sfc@3.5.20':
resolution: {integrity: sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==}
'@vue/compiler-ssr@3.5.13':
resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
'@vue/compiler-ssr@3.5.20':
resolution: {integrity: sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==}
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
@ -1270,25 +1299,28 @@ packages:
'@vue/devtools-shared@7.7.2':
resolution: {integrity: sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==}
'@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
'@vue/reactivity@3.5.20':
resolution: {integrity: sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ==}
'@vue/runtime-core@3.5.13':
resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==}
'@vue/runtime-core@3.5.20':
resolution: {integrity: sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==}
'@vue/runtime-dom@3.5.13':
resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==}
'@vue/runtime-dom@3.5.20':
resolution: {integrity: sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw==}
'@vue/server-renderer@3.5.13':
resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==}
'@vue/server-renderer@3.5.20':
resolution: {integrity: sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg==}
peerDependencies:
vue: 3.5.13
vue: 3.5.20
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
'@vuetify/loader-shared@2.1.0':
resolution: {integrity: sha512-dNE6Ceym9ijFsmJKB7YGW0cxs7xbYV8+1LjU6jd4P14xOt/ji4Igtgzt0rJFbxu+ZhAzqz853lhB0z8V9Dy9cQ==}
'@vue/shared@3.5.20':
resolution: {integrity: sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==}
'@vuetify/loader-shared@2.1.1':
resolution: {integrity: sha512-jSZTzTYaoiv8iwonFCVZQ0YYX/M+Uyl4ng+C4egMJT0Hcmh9gIxJL89qfZICDeo3g0IhqrvipW2FFKKRDMtVcA==}
peerDependencies:
vue: ^3.0.0
vuetify: ^3.0.0
@ -1387,8 +1419,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
axios@1.8.4:
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
axios@1.11.0:
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
babel-plugin-polyfill-corejs2@0.4.13:
resolution: {integrity: sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==}
@ -1943,8 +1975,8 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
form-data@4.0.1:
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
form-data@4.0.4:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
fs-extra@9.1.0:
@ -2084,8 +2116,8 @@ packages:
idb@7.1.1:
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
idb@8.0.2:
resolution: {integrity: sha512-CX70rYhx7GDDQzwwQMDwF6kDRQi5vVs6khHUumDrMecBylKkwvZ8HWvKV08AGb7VbpoGCWUQ4aHzNDgoUiOIUg==}
idb@8.0.3:
resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
@ -2308,8 +2340,8 @@ packages:
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
hasBin: true
js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -2571,8 +2603,8 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pinia@3.0.1:
resolution: {integrity: sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg==}
pinia@3.0.3:
resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
@ -2580,8 +2612,8 @@ packages:
typescript:
optional: true
pinyin-pro@3.26.0:
resolution: {integrity: sha512-HcBZZb0pvm0/JkPhZHWA5Hqp2cWHXrrW/WrV+OtaYYM+kf35ffvZppIUuGmyuQ7gDr1JDJKMkbEE+GN0wfMoGg==}
pinyin-pro@3.27.0:
resolution: {integrity: sha512-Osdgjwe7Rm17N2paDMM47yW+jUIUH3+0RGo8QP39ZTLpTaJVDK0T58hOLaMQJbcMmAebVuK2ePunTEVEx1clNQ==}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
@ -2605,6 +2637,10 @@ packages:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@ -2977,6 +3013,7 @@ packages:
source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
engines: {node: '>= 8'}
deprecated: The work that was done in this beta branch won't be included in future versions
sourcemap-codec@1.4.8:
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
@ -3257,12 +3294,12 @@ packages:
varint@6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
vite-plugin-pwa@1.0.0:
resolution: {integrity: sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA==}
vite-plugin-pwa@1.0.3:
resolution: {integrity: sha512-/OpqIpUldALGxcsEnv/ekQiQ5xHkQ53wcoN5ewX4jiIDNGs3W+eNcI1WYZeyOLmzoEjg09D7aX0O89YGjen1aw==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@vite-pwa/assets-generator': ^1.0.0
vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
vite: ^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
workbox-build: ^7.3.0
workbox-window: ^7.3.0
peerDependenciesMeta:
@ -3276,8 +3313,8 @@ packages:
vue: ^3.2.4
vue-router: ^4.0.11
vite-plugin-vuetify@2.1.1:
resolution: {integrity: sha512-Pb7bKhQH8qPMzURmEGq2aIqCJkruFNsyf1NcrrtnjsOIkqJPMcBbiP0oJoO8/uAmyB5W/1JTbbUEsyXdMM0QHQ==}
vite-plugin-vuetify@2.1.2:
resolution: {integrity: sha512-I/wd6QS+DO6lHmuGoi1UTyvvBTQ2KDzQZ9oowJQEJ6OcjWfJnscYXx2ptm6S7fJSASuZT8jGRBL3LV4oS3LpaA==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
vite: '>=5'
@ -3321,21 +3358,21 @@ packages:
peerDependencies:
eslint: '>=6.0.0'
vue-router@4.5.0:
resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
vue-router@4.5.1:
resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
peerDependencies:
vue: ^3.2.0
vue@3.5.13:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
vue@3.5.20:
resolution: {integrity: sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
vuetify@3.8.0:
resolution: {integrity: sha512-ROC0Xq2G/25ZyUpQMhaynMyXZBJY1WbOGlqOB810yubp8hfY8RlrOw+mzXJonOq6jylCY32muQ9xiJF1JPTLVA==}
vuetify@3.9.6:
resolution: {integrity: sha512-jNs2yLYiM50kE16gBu58xmnh9t/MOvgnYcNvmLNps6TLq9rPvjTNFm2k2jWfe69hGg0gQf+MFXXDkf65fxi9gg==}
engines: {node: ^12.20 || >=14.13}
peerDependencies:
typescript: '>=4.7'
@ -3475,7 +3512,7 @@ snapshots:
'@babel/code-frame@7.26.2':
dependencies:
'@babel/helper-validator-identifier': 7.25.9
'@babel/helper-validator-identifier': 7.27.1
js-tokens: 4.0.0
picocolors: 1.1.1
@ -3489,10 +3526,10 @@ snapshots:
'@babel/helper-compilation-targets': 7.27.0
'@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10)
'@babel/helpers': 7.27.0
'@babel/parser': 7.27.0
'@babel/parser': 7.28.3
'@babel/template': 7.27.0
'@babel/traverse': 7.27.0
'@babel/types': 7.27.0
'@babel/types': 7.28.2
convert-source-map: 2.0.0
debug: 4.4.0
gensync: 1.0.0-beta.2
@ -3503,15 +3540,15 @@ snapshots:
'@babel/generator@7.27.0':
dependencies:
'@babel/parser': 7.27.0
'@babel/types': 7.27.0
'@babel/parser': 7.28.3
'@babel/types': 7.28.2
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
jsesc: 3.1.0
'@babel/helper-annotate-as-pure@7.25.9':
dependencies:
'@babel/types': 7.27.0
'@babel/types': 7.28.2
'@babel/helper-compilation-targets@7.27.0':
dependencies:
@ -3555,14 +3592,14 @@ snapshots:
'@babel/helper-member-expression-to-functions@7.25.9':
dependencies:
'@babel/traverse': 7.27.0
'@babel/types': 7.27.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
'@babel/helper-module-imports@7.25.9':
dependencies:
'@babel/traverse': 7.27.0
'@babel/types': 7.27.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
@ -3570,14 +3607,14 @@ snapshots:
dependencies:
'@babel/core': 7.26.10
'@babel/helper-module-imports': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.27.0
transitivePeerDependencies:
- supports-color
'@babel/helper-optimise-call-expression@7.25.9':
dependencies:
'@babel/types': 7.27.0
'@babel/types': 7.28.2
'@babel/helper-plugin-utils@7.26.5': {}
@ -3602,28 +3639,32 @@ snapshots:
'@babel/helper-skip-transparent-expression-wrappers@7.25.9':
dependencies:
'@babel/traverse': 7.27.0
'@babel/types': 7.27.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
'@babel/helper-string-parser@7.25.9': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.25.9': {}
'@babel/helper-validator-identifier@7.27.1': {}
'@babel/helper-validator-option@7.25.9': {}
'@babel/helper-wrap-function@7.25.9':
dependencies:
'@babel/template': 7.27.0
'@babel/traverse': 7.27.0
'@babel/types': 7.27.0
'@babel/types': 7.28.2
transitivePeerDependencies:
- supports-color
'@babel/helpers@7.27.0':
dependencies:
'@babel/template': 7.27.0
'@babel/types': 7.27.0
'@babel/types': 7.28.2
'@babel/parser@7.26.2':
dependencies:
@ -3633,6 +3674,10 @@ snapshots:
dependencies:
'@babel/types': 7.27.0
'@babel/parser@7.28.3':
dependencies:
'@babel/types': 7.28.2
'@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.10)':
dependencies:
'@babel/core': 7.26.10
@ -3850,7 +3895,7 @@ snapshots:
'@babel/core': 7.26.10
'@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10)
'@babel/helper-plugin-utils': 7.26.5
'@babel/helper-validator-identifier': 7.25.9
'@babel/helper-validator-identifier': 7.27.1
'@babel/traverse': 7.27.0
transitivePeerDependencies:
- supports-color
@ -4086,7 +4131,7 @@ snapshots:
dependencies:
'@babel/core': 7.26.10
'@babel/helper-plugin-utils': 7.26.5
'@babel/types': 7.27.0
'@babel/types': 7.28.2
esutils: 2.0.3
'@babel/runtime@7.27.0':
@ -4096,16 +4141,16 @@ snapshots:
'@babel/template@7.27.0':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/parser': 7.27.0
'@babel/types': 7.27.0
'@babel/parser': 7.28.3
'@babel/types': 7.28.2
'@babel/traverse@7.27.0':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/generator': 7.27.0
'@babel/parser': 7.27.0
'@babel/parser': 7.28.3
'@babel/template': 7.27.0
'@babel/types': 7.27.0
'@babel/types': 7.28.2
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
@ -4121,6 +4166,11 @@ snapshots:
'@babel/helper-string-parser': 7.25.9
'@babel/helper-validator-identifier': 7.25.9
'@babel/types@7.28.2':
dependencies:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@bufbuild/protobuf@2.2.2': {}
'@canvas/image-data@1.0.0': {}
@ -4569,12 +4619,12 @@ snapshots:
sharp-ico: 0.1.5
unconfig: 7.3.1
'@vitejs/plugin-vue@5.2.3(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.13)':
'@vitejs/plugin-vue@5.2.3(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.20)':
dependencies:
vite: 5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0)
vue: 3.5.13
vue: 3.5.20
'@vue-macros/common@1.16.1(vue@3.5.13)':
'@vue-macros/common@1.16.1(vue@3.5.20)':
dependencies:
'@vue/compiler-sfc': 3.5.13
ast-kit: 1.4.2
@ -4583,7 +4633,7 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.2
optionalDependencies:
vue: 3.5.13
vue: 3.5.20
'@vue/compiler-core@3.5.13':
dependencies:
@ -4593,11 +4643,24 @@ snapshots:
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-core@3.5.20':
dependencies:
'@babel/parser': 7.28.3
'@vue/shared': 3.5.20
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.13':
dependencies:
'@vue/compiler-core': 3.5.13
'@vue/shared': 3.5.13
'@vue/compiler-dom@3.5.20':
dependencies:
'@vue/compiler-core': 3.5.20
'@vue/shared': 3.5.20
'@vue/compiler-sfc@3.5.13':
dependencies:
'@babel/parser': 7.26.2
@ -4610,11 +4673,28 @@ snapshots:
postcss: 8.4.49
source-map-js: 1.2.1
'@vue/compiler-sfc@3.5.20':
dependencies:
'@babel/parser': 7.28.3
'@vue/compiler-core': 3.5.20
'@vue/compiler-dom': 3.5.20
'@vue/compiler-ssr': 3.5.20
'@vue/shared': 3.5.20
estree-walker: 2.0.2
magic-string: 0.30.17
postcss: 8.5.6
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.13':
dependencies:
'@vue/compiler-dom': 3.5.13
'@vue/shared': 3.5.13
'@vue/compiler-ssr@3.5.20':
dependencies:
'@vue/compiler-dom': 3.5.20
'@vue/shared': 3.5.20
'@vue/devtools-api@6.6.4': {}
'@vue/devtools-api@7.7.2':
@ -4635,35 +4715,37 @@ snapshots:
dependencies:
rfdc: 1.4.1
'@vue/reactivity@3.5.13':
'@vue/reactivity@3.5.20':
dependencies:
'@vue/shared': 3.5.13
'@vue/shared': 3.5.20
'@vue/runtime-core@3.5.13':
'@vue/runtime-core@3.5.20':
dependencies:
'@vue/reactivity': 3.5.13
'@vue/shared': 3.5.13
'@vue/reactivity': 3.5.20
'@vue/shared': 3.5.20
'@vue/runtime-dom@3.5.13':
'@vue/runtime-dom@3.5.20':
dependencies:
'@vue/reactivity': 3.5.13
'@vue/runtime-core': 3.5.13
'@vue/shared': 3.5.13
'@vue/reactivity': 3.5.20
'@vue/runtime-core': 3.5.20
'@vue/shared': 3.5.20
csstype: 3.1.3
'@vue/server-renderer@3.5.13(vue@3.5.13)':
'@vue/server-renderer@3.5.20(vue@3.5.20)':
dependencies:
'@vue/compiler-ssr': 3.5.13
'@vue/shared': 3.5.13
vue: 3.5.13
'@vue/compiler-ssr': 3.5.20
'@vue/shared': 3.5.20
vue: 3.5.20
'@vue/shared@3.5.13': {}
'@vuetify/loader-shared@2.1.0(vue@3.5.13)(vuetify@3.8.0)':
'@vue/shared@3.5.20': {}
'@vuetify/loader-shared@2.1.1(vue@3.5.20)(vuetify@3.9.6)':
dependencies:
upath: 2.0.1
vue: 3.5.13
vuetify: 3.8.0(vite-plugin-vuetify@2.1.1)(vue@3.5.13)
vue: 3.5.20
vuetify: 3.9.6(vite-plugin-vuetify@2.1.2)(vue@3.5.20)
acorn-jsx@5.3.2(acorn@8.14.0):
dependencies:
@ -4788,10 +4870,10 @@ snapshots:
dependencies:
possible-typed-array-names: 1.0.0
axios@1.8.4:
axios@1.11.0:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.1
form-data: 4.0.4
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
@ -5516,10 +5598,12 @@ snapshots:
dependencies:
is-callable: 1.2.7
form-data@4.0.1:
form-data@4.0.4:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
fs-extra@9.1.0:
@ -5670,7 +5754,7 @@ snapshots:
idb@7.1.1: {}
idb@8.0.2: {}
idb@8.0.3: {}
ignore@5.3.2: {}
@ -5893,7 +5977,7 @@ snapshots:
jiti@2.4.2: {}
js-base64@3.7.7: {}
js-base64@3.7.8: {}
js-tokens@4.0.0: {}
@ -6131,12 +6215,12 @@ snapshots:
picomatch@4.0.2: {}
pinia@3.0.1(vue@3.5.13):
pinia@3.0.3(vue@3.5.20):
dependencies:
'@vue/devtools-api': 7.7.2
vue: 3.5.13
vue: 3.5.20
pinyin-pro@3.26.0: {}
pinyin-pro@3.27.0: {}
pkg-types@1.3.1:
dependencies:
@ -6169,6 +6253,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
prelude-ls@1.2.1: {}
pretty-bytes@5.6.0: {}
@ -6871,7 +6961,7 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.2
unplugin-vue-components@28.4.1(@babel/parser@7.27.0)(vue@3.5.13):
unplugin-vue-components@28.4.1(@babel/parser@7.28.3)(vue@3.5.20):
dependencies:
chokidar: 3.6.0
debug: 4.4.0
@ -6881,16 +6971,16 @@ snapshots:
tinyglobby: 0.2.12
unplugin: 2.2.2
unplugin-utils: 0.2.4
vue: 3.5.13
vue: 3.5.20
optionalDependencies:
'@babel/parser': 7.27.0
'@babel/parser': 7.28.3
transitivePeerDependencies:
- supports-color
unplugin-vue-router@0.12.0(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13):
unplugin-vue-router@0.12.0(vue-router@4.5.1(vue@3.5.20))(vue@3.5.20):
dependencies:
'@babel/types': 7.27.0
'@vue-macros/common': 1.16.1(vue@3.5.13)
'@vue-macros/common': 1.16.1(vue@3.5.20)
ast-walker-scope: 0.6.2
chokidar: 4.0.3
fast-glob: 3.3.3
@ -6905,7 +6995,7 @@ snapshots:
unplugin-utils: 0.2.4
yaml: 2.7.1
optionalDependencies:
vue-router: 4.5.0(vue@3.5.13)
vue-router: 4.5.1(vue@3.5.20)
transitivePeerDependencies:
- vue
@ -6939,9 +7029,9 @@ snapshots:
varint@6.0.0: {}
vite-plugin-pwa@1.0.0(@vite-pwa/assets-generator@1.0.0)(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(workbox-build@7.3.0)(workbox-window@7.3.0):
vite-plugin-pwa@1.0.3(@vite-pwa/assets-generator@1.0.0)(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(workbox-build@7.3.0)(workbox-window@7.3.0):
dependencies:
debug: 4.3.7
debug: 4.4.0
pretty-bytes: 6.1.1
tinyglobby: 0.2.12
vite: 5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0)
@ -6952,24 +7042,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-vue-layouts@0.11.0(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13):
vite-plugin-vue-layouts@0.11.0(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue-router@4.5.1(vue@3.5.20))(vue@3.5.20):
dependencies:
debug: 4.3.7
fast-glob: 3.3.2
vite: 5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0)
vue: 3.5.13
vue-router: 4.5.0(vue@3.5.13)
vue: 3.5.20
vue-router: 4.5.1(vue@3.5.20)
transitivePeerDependencies:
- supports-color
vite-plugin-vuetify@2.1.1(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.13)(vuetify@3.8.0):
vite-plugin-vuetify@2.1.2(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.20)(vuetify@3.9.6):
dependencies:
'@vuetify/loader-shared': 2.1.0(vue@3.5.13)(vuetify@3.8.0)
debug: 4.3.7
'@vuetify/loader-shared': 2.1.1(vue@3.5.20)(vuetify@3.9.6)
debug: 4.4.0
upath: 2.0.1
vite: 5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0)
vue: 3.5.13
vuetify: 3.8.0(vite-plugin-vuetify@2.1.1)(vue@3.5.13)
vue: 3.5.20
vuetify: 3.9.6(vite-plugin-vuetify@2.1.2)(vue@3.5.20)
transitivePeerDependencies:
- supports-color
@ -6997,24 +7087,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-router@4.5.0(vue@3.5.13):
vue-router@4.5.1(vue@3.5.20):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.13
vue: 3.5.20
vue@3.5.13:
vue@3.5.20:
dependencies:
'@vue/compiler-dom': 3.5.13
'@vue/compiler-sfc': 3.5.13
'@vue/runtime-dom': 3.5.13
'@vue/server-renderer': 3.5.13(vue@3.5.13)
'@vue/shared': 3.5.13
'@vue/compiler-dom': 3.5.20
'@vue/compiler-sfc': 3.5.20
'@vue/runtime-dom': 3.5.20
'@vue/server-renderer': 3.5.20(vue@3.5.20)
'@vue/shared': 3.5.20
vuetify@3.8.0(vite-plugin-vuetify@2.1.1)(vue@3.5.13):
vuetify@3.9.6(vite-plugin-vuetify@2.1.2)(vue@3.5.20):
dependencies:
vue: 3.5.13
vue: 3.5.20
optionalDependencies:
vite-plugin-vuetify: 2.1.1(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.13)(vuetify@3.8.0)
vite-plugin-vuetify: 2.1.2(vite@5.4.17(sass-embedded@1.86.3)(sass@1.86.3)(terser@5.39.0))(vue@3.5.20)(vuetify@3.9.6)
webidl-conversions@4.0.2: {}

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,8 @@
<v-divider class="my-2" />
<setting-item :setting-key="'display.showAntiScreenBurnCard'" />
<v-divider class="my-2" />
<setting-item :setting-key="'display.showExamScheduleButton'" />
</v-list>
</settings-card>

View File

@ -82,6 +82,13 @@
@click="editItem(item)"
title="编辑"
/>
<v-btn
icon="mdi-cloud-download"
size="small"
color="primary"
@click="getCloudUrl(item)"
title="获取云端地址"
/>
<v-btn
icon="mdi-delete"
size="small"
@ -228,6 +235,100 @@
</v-card>
</v-dialog>
<!-- 云端地址对话框 -->
<v-dialog v-model="cloudUrlDialog" max-width="800px">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon icon="mdi-cloud-download" class="mr-2" />
获取云端访问地址
<v-spacer />
<v-btn icon="mdi-close" variant="text" @click="cloudUrlDialog = false" />
</v-card-title>
<v-card-subtitle v-if="selectedCloudItem">
键名: <code>{{ selectedCloudItem.key }}</code>
</v-card-subtitle>
<v-card-text>
<v-alert v-if="cloudUrlError" type="error" variant="tonal" class="mb-4">
{{ cloudUrlError }}
</v-alert>
<v-alert v-if="cloudUrlResult && cloudUrlResult.success" type="success" variant="tonal" class="mb-4">
<v-alert-title>云端地址获取成功</v-alert-title>
<div class="mt-2">
<div v-if="cloudUrlResult.migrated" class="mb-2">
<v-icon icon="mdi-database-arrow-up" class="mr-1" color="success" />
数据已从本地迁移到云端
</div>
<div v-if="cloudUrlResult.configured" class="mb-2">
<v-icon icon="mdi-cog" class="mr-1" color="info" />
云端配置已自动设置
</div>
</div>
</v-alert>
<v-text-field
v-if="cloudUrlResult && cloudUrlResult.url"
:model-value="cloudUrlResult.url"
label="云端访问地址"
variant="outlined"
readonly
class="font-monospace"
append-inner-icon="mdi-content-copy"
@click:append-inner="copyCloudUrl"
/>
<v-expansion-panels v-if="cloudUrlResult && cloudUrlResult.url" class="mt-4">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon icon="mdi-cog" class="mr-2" />
高级选项
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-checkbox
v-model="cloudUrlOptions.migrateFromLocal"
label="从本地迁移数据到云端"
density="compact"
/>
<v-checkbox
v-model="cloudUrlOptions.autoConfigureCloud"
label="自动配置云端默认设置"
density="compact"
/>
<v-btn
@click="refreshCloudUrl"
variant="tonal"
color="primary"
:loading="gettingCloudUrl"
class="mt-2"
>
<v-icon icon="mdi-refresh" class="mr-1" />
重新获取
</v-btn>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="cloudUrlDialog = false" variant="text">
关闭
</v-btn>
<v-btn
v-if="cloudUrlResult && cloudUrlResult.url"
@click="openCloudUrl"
variant="tonal"
color="primary"
>
<v-icon icon="mdi-open-in-new" class="mr-1" />
在新窗口打开
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除确认对话框 -->
<v-dialog v-model="deleteDialog" max-width="400px">
<v-card>
@ -289,11 +390,22 @@ export default {
editDialog: false,
deleteDialog: false,
createDialog: false,
cloudUrlDialog: false,
//
selectedItem: null,
editingItem: null,
itemToDelete: null,
selectedCloudItem: null,
//
gettingCloudUrl: false,
cloudUrlResult: null,
cloudUrlError: null,
cloudUrlOptions: {
migrateFromLocal: true,
autoConfigureCloud: true
},
//
editingData: '',
@ -597,6 +709,67 @@ export default {
} catch (error) {
this.$message.error('复制失败', error.message);
}
},
async getCloudUrl(item) {
this.selectedCloudItem = item;
this.cloudUrlResult = null;
this.cloudUrlError = null;
this.cloudUrlDialog = true;
await this.fetchCloudUrl();
},
async fetchCloudUrl() {
if (!this.selectedCloudItem) return;
this.gettingCloudUrl = true;
this.cloudUrlError = null;
try {
const result = await dataProvider.getKeyCloudUrl(
this.selectedCloudItem.key,
this.cloudUrlOptions
);
if (result.success) {
this.cloudUrlResult = result;
this.$message.success('云端地址获取成功');
} else {
this.cloudUrlError = result.error?.message || '获取云端地址失败';
this.$message.error('获取失败', this.cloudUrlError);
}
} catch (error) {
this.cloudUrlError = error.message || '获取云端地址时发生错误';
this.$message.error('获取失败', this.cloudUrlError);
} finally {
this.gettingCloudUrl = false;
}
},
async refreshCloudUrl() {
await this.fetchCloudUrl();
},
async copyCloudUrl() {
if (!this.cloudUrlResult?.url) return;
try {
await navigator.clipboard.writeText(this.cloudUrlResult.url);
this.$message.success('云端地址已复制到剪贴板');
} catch (error) {
this.$message.error('复制失败', error.message);
}
},
openCloudUrl() {
if (!this.cloudUrlResult?.url) return;
try {
window.open(this.cloudUrlResult.url, '_blank');
} catch (error) {
this.$message.error('打开链接失败', error.message);
}
}
}
};

611
src/pages/examschedule.vue Normal file
View File

@ -0,0 +1,611 @@
<template>
<v-container class="fill-height">
<v-row>
<v-col cols="12">
<v-card class="elevation-12" border>
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
<v-icon color="white" class="mr-2">mdi-calendar-check</v-icon>
考试看板
</v-card-title>
<v-card-subtitle>
不只是考试看板
</v-card-subtitle>
<v-card-text>
<!-- 错误提示 -->
<v-alert
v-if="error"
type="error"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start"
closable
@click:close="error = ''"
>
<div class="d-flex align-center">
<v-icon class="mr-2">mdi-alert-circle</v-icon>
{{ error }}
</div>
</v-alert>
<!-- 成功提示 -->
<v-alert
v-if="success"
type="success"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start"
closable
@click:close="success = ''"
>
<div class="d-flex align-center">
<v-icon class="mr-2">mdi-check-circle</v-icon>
{{ success }}
</div>
</v-alert>
<!-- 操作按钮 -->
<div class="d-flex justify-space-between align-center mb-4">
<div class="d-flex align-center">
<v-btn
color="primary"
prepend-icon="mdi-plus"
class="mr-2"
@click="createNewConfig"
>
新建配置
</v-btn>
<v-btn
color="info"
prepend-icon="mdi-refresh"
:loading="loading"
@click="loadConfigs"
variant="outlined"
>
刷新
</v-btn>
</div>
<v-chip
v-if="configs.length > 0"
color="primary"
prepend-icon="mdi-format-list-numbered"
>
{{ configs.length }} 个配置
</v-chip>
</div>
<!-- 加载状态 -->
<v-card v-if="loading" class="my-4" outlined>
<v-card-text>
<v-skeleton-loader
type="list-item-avatar-two-line@3"
class="mx-auto"
></v-skeleton-loader>
</v-card-text>
</v-card>
<!-- 配置列表 -->
<v-card v-if="!loading && configs.length > 0" class="my-4" elevation="1">
<v-card-title class="d-flex align-center pa-4 bg-primary-lighten-5">
<v-icon class="mr-2">mdi-format-list-bulleted</v-icon>
<span class="font-weight-bold">配置列表</span>
</v-card-title>
<v-list>
<v-list-item
v-for="(config) in configs"
:key="config.id"
class="border-b"
style="cursor: pointer;"
@click="showEditDialog(config)"
>
<template #prepend>
<v-avatar color="primary" class="mr-2">
<v-icon color="white">mdi-calendar-text</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-medium">
{{ config.examName || `配置 ${config.id}` }}
</v-list-item-title>
<v-list-item-subtitle class="text-caption mt-1">
<div class="d-flex align-center">
<v-icon size="small" class="mr-1">mdi-information-outline</v-icon>
{{ config.message || '无描述' }}
</div>
<div class="d-flex align-center mt-1">
<v-icon size="small" class="mr-1">mdi-book-multiple</v-icon>
{{ config.examInfos ? config.examInfos.length : 0 }} 堂考试
</div>
</v-list-item-subtitle>
<template #append>
<div class="d-flex align-center">
<v-btn
icon="mdi-pencil"
size="small"
color="primary"
variant="text"
class="mr-1"
@click="showEditDialog(config)"
>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn
icon="mdi-eye"
size="small"
color="info"
variant="text"
class="mr-1"
@click="showEditDialog(config)"
>
<v-icon>mdi-eye</v-icon>
</v-btn>
</div>
</template>
</v-list-item>
</v-list>
</v-card>
<!-- 空状态 -->
<v-card v-if="!loading && configs.length === 0" class="my-4" elevation="1">
<v-card-text class="text-center py-8">
<v-icon size="64" color="grey-lighten-1" class="mb-4">
mdi-calendar-blank
</v-icon>
<h3 class="text-h6 mb-2 text-grey-darken-1">暂无配置</h3>
<p class="text-body-2 text-grey-darken-1 mb-4">
点击"新建配置"按钮创建您的第一个考试配置
</p>
<v-btn
color="primary"
prepend-icon="mdi-plus"
@click="createNewConfig"
>
新建配置
</v-btn>
</v-card-text>
</v-card>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 重命名对话框 -->
<v-dialog v-model="renameDialog" max-width="500">
<v-card>
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-rename-box</v-icon>
重命名配置
</v-card-title>
<v-card-text>
<v-text-field
v-model="newConfigName"
label="配置名称"
prepend-inner-icon="mdi-calendar-text"
variant="outlined"
:rules="[v => !!v || '配置名称不能为空']"
@keyup.enter="renameConfig"
></v-text-field>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="grey"
variant="text"
@click="renameDialog = false"
>
取消
</v-btn>
<v-btn
color="primary"
variant="outlined"
:loading="renaming"
@click="renameConfig"
:disabled="!newConfigName"
>
确认
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 编辑配置弹框 -->
<v-dialog v-model="editDialog" max-width="1200" persistent>
<v-card>
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
<v-icon color="white" class="mr-2">mdi-pencil</v-icon>
编辑考试配置
<v-spacer></v-spacer>
<v-chip
v-if="editingConfig"
color="white"
text-color="primary"
size="small"
class="mr-2"
>
ID: {{ editingConfig.id }}
</v-chip>
<v-btn
icon="mdi-close"
color="white"
variant="text"
@click="closeEditDialog"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-4"
style="max-height: 70vh; overflow-y: auto;">
<ExamConfigEditor
v-if="editingConfig"
:config-id="editingConfig.id"
ref="configEditor"
:dialog-mode="true"
@saved="onConfigSaved"
@error="onConfigError"
@opened="onConfigOpened"
@deleted="onConfigDeleted"
/>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn
color="grey"
variant="outlined"
prepend-icon="mdi-close"
@click="closeEditDialog"
>
关闭
</v-btn>
<v-spacer></v-spacer>
<v-btn
color="success"
variant="outlined"
prepend-icon="mdi-content-save"
:loading="saving"
@click="saveConfigInDialog"
>
保存配置
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import dataProvider from '@/utils/dataProvider'
import { getSetting } from '@/utils/settings'
import ExamConfigEditor from '@/components/ExamConfigEditor.vue'
export default {
name: 'ExamScheduleManager',
components: {
ExamConfigEditor
},
data() {
return {
configs: [],
loading: false,
error: '',
success: '',
renameDialog: false,
editDialog: false,
configToRename: null,
editingConfig: null,
newConfigName: '',
renaming: false,
saving: false
}
},
async mounted() {
await this.loadConfigs()
},
methods: {
/**
* 初始化示例数据仅在首次访问时
*/
async initializeExampleData() {
const exampleConfigs = [
{
id: 'exam_example_001',
examName: '期末考试安排',
message: '请按时参加考试,携带学生证和身份证',
examInfos: [
{
name: '数学',
start: '2025/01/15 09:00',
end: '2025/01/15 11:00'
},
{
name: '英语',
start: '2025/01/16 14:00',
end: '2025/01/16 16:00'
}
]
},
{
id: 'exam_example_002',
examName: '期中考试安排',
message: '考试期间请保持安静',
examInfos: [
{
name: '物理',
start: '2025/01/20 10:00',
end: '2025/01/20 12:00'
},
{
name: '化学',
start: '2025/01/21 14:00',
end: '2025/01/21 16:00'
}
]
},
{
id: 'exam_example_003',
examName: '模拟考试安排',
message: '模拟考试,请认真对待',
examInfos: [
{
name: '语文',
start: '2025/01/25 09:00',
end: '2025/01/25 11:30'
}
]
}
]
//
const configList = exampleConfigs.map(c => ({ id: c.id }))
await dataProvider.saveData('es_list', configList)
//
for (let config of exampleConfigs) {
const configData = { ...config }
delete configData.id
await dataProvider.saveData(`es_${config.id}`, configData)
}
return exampleConfigs
},
/**
* 加载配置列表
*/
async loadConfigs() {
this.loading = true
this.error = ''
try {
//
const response = await dataProvider.loadData('es_list')
if (response && response && response.length > 0) {
//
this.configs = []
for (let configItem of response) {
try {
const detailResponse = await dataProvider.loadData(`es_${configItem.id}`)
if (detailResponse) {
this.configs.push({
id: configItem.id,
...detailResponse
})
}
} catch (err) {
console.warn(`加载配置 es_${configItem.id} 失败:`, err)
}
}
} else {
//
this.configs = await this.initializeExampleData()
}
} catch (err) {
this.error = '加载配置列表失败: ' + err.message
this.configs = []
} finally {
this.loading = false
}
},
/**
* 创建新配置
*/
async createNewConfig() {
const newId = Date.now().toString()
const defaultConfig = {
examName: '新考试配置',
message: '请编辑此配置',
room: getSetting('server.classNumber') || '待定',
examInfos: [
{
name: '科目1',
start: '2025/08/29 16:27',
end: '2025/08/29 17:27'
}
]
}
try {
//
const saveResponse = await dataProvider.saveData(`es_${newId}`, defaultConfig)
if (!saveResponse) {
throw new Error(saveResponse.error?.message || '保存失败')
}
//
this.configs.push({
id: newId,
...defaultConfig
})
//
const currentList = this.configs.map(c => ({ id: c.id }))
const listResponse = await dataProvider.saveData('es_list', currentList)
if (!listResponse) {
throw new Error(listResponse.error?.message || '更新列表失败')
}
this.success = '新配置创建成功'
//
const newConfig = this.configs.find(c => c.id === newId)
if (newConfig) {
this.editingConfig = newConfig
this.editDialog = true
}
} catch (err) {
this.error = '创建配置失败: ' + err.message
}
},
/**
* 显示重命名对话框
*/
showRenameDialog(config) {
this.configToRename = config
this.newConfigName = config.examName || `配置 ${config.id}`
this.renameDialog = true
},
/**
* 重命名配置
*/
async renameConfig() {
if (!this.configToRename || !this.newConfigName) return
this.renaming = true
try {
// id
const configData = {
examName: this.newConfigName,
message: this.configToRename.message,
examInfos: this.configToRename.examInfos
}
const saveResponse = await dataProvider.saveData(`es_${this.configToRename.id}`, configData)
if (!saveResponse) {
throw new Error(saveResponse.error?.message || '保存失败')
}
//
const configIndex = this.configs.findIndex(c => c.id === this.configToRename.id)
if (configIndex !== -1) {
this.configs[configIndex].examName = this.newConfigName
}
this.success = '配置重命名成功'
this.renameDialog = false
this.configToRename = null
this.newConfigName = ''
} catch (err) {
this.error = '重命名配置失败: ' + err.message
} finally {
this.renaming = false
}
},
/**
* 显示编辑弹框
*/
showEditDialog(config) {
this.editingConfig = config
this.editDialog = true
},
/**
* 关闭编辑弹框
*/
closeEditDialog() {
this.editDialog = false
this.editingConfig = null
this.saving = false
},
/**
* 在弹框中保存配置
*/
async saveConfigInDialog() {
if (this.$refs.configEditor) {
this.saving = true
try {
await this.$refs.configEditor.saveConfig()
} catch (error) {
console.error('保存配置失败:', error)
} finally {
this.saving = false
}
}
},
/**
* 配置保存成功回调
*/
onConfigSaved() {
this.success = '配置保存成功!'
this.loadConfigs() //
setTimeout(() => {
this.success = ''
}, 3000)
},
/**
* 配置保存错误回调
*/
onConfigError(error) {
this.error = error || '保存配置时发生错误'
setTimeout(() => {
this.error = ''
}, 5000)
},
/**
* 配置打开成功回调
*/
onConfigOpened(data) {
this.success = '配置已在新窗口中打开'
setTimeout(() => {
this.success = ''
}, 3000)
},
/**
* 处理配置删除事件
*/
onConfigDeleted(result) {
if (result.success) {
this.success = result.message || "配置删除成功"
//
this.editDialog = false
//
this.loadConfigs()
} else {
this.error = result.message || "删除失败"
}
}
}
}
</script>
<style scoped>
.border-b {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.border-b:last-child {
border-bottom: none;
}
</style>

View File

@ -116,6 +116,16 @@
>
随机点名
</v-btn>
<v-btn
v-if="showExamScheduleButton"
color="green"
prepend-icon="mdi-calendar-check"
size="large"
class="ml-2"
@click="$router.push('/examschedule')"
>
考试看板
</v-btn>
<v-btn
v-if="showListCardButton"
color="primary-darken-1"
@ -839,6 +849,9 @@ export default {
showFullscreenButton() {
return getSetting("display.showFullscreenButton");
},
showExamScheduleButton() {
return getSetting("display.showExamScheduleButton");
},
showAntiScreenBurnCard() {
return getSetting("display.showAntiScreenBurnCard");
},

View File

@ -1,6 +1,6 @@
import { kvLocalProvider } from "./providers/kvLocalProvider";
import { kvServerProvider } from "./providers/kvServerProvider";
import { getSetting } from "./settings";
import { getSetting, setSetting } from "./settings";
export const formatResponse = (data) => data;
@ -86,8 +86,189 @@ export default {
return kvLocalProvider.loadKeys(options);
}
},
/**
* 获取键的云端访问地址并处理本地到云端的数据迁移
*
* 功能说明:
* 1. 如果用户选择本地存储则将本地键数据读取并存储到云端
* 2. 如果云端配置为空或错误则自动改成classworksCloudDefaults的配置
* 3. 根据网站验证情况私有则添加token公开或受保护则不需要拼接键的get路径并返回
*
* @param {string} key - 要获取地址的键名
* @param {Object} options - 选项配置
* @param {boolean} options.migrateFromLocal - 是否从本地迁移数据到云端默认为true
* @param {boolean} options.autoConfigureCloud - 是否自动配置云端默认设置默认为true
* @returns {Promise<Object>} 包含键访问地址和操作结果的响应对象
*
* 使用示例:
* ```javascript
* import dataProvider from '@/utils/dataProvider';
*
* // 基本用法:获取键的云端地址并自动迁移本地数据
* const result = await dataProvider.getKeyCloudUrl('exam_configs');
* if (result.success) {
* console.log('云端访问地址:', result.url);
* console.log('是否已迁移数据:', result.migrated);
* console.log('是否自动配置:', result.configured);
* } else {
* console.error('获取失败:', result.error.message);
* }
*
* // 仅获取地址,不迁移数据
* const urlOnly = await dataProvider.getKeyCloudUrl('my_data', {
* migrateFromLocal: false
* });
*
* // 不自动配置云端设置
* const noAutoConfig = await dataProvider.getKeyCloudUrl('my_data', {
* autoConfigureCloud: false
* });
* ```
*
* 传入参数示例:
* ```javascript
* // 参数1: key (必需)
* 'exam_configs' // 字符串类型的键名
*
* // 参数2: options (可选)
* {
* migrateFromLocal: true, // 是否迁移本地数据
* autoConfigureCloud: true // 是否自动配置云端
* }
* ```
*
* 返回值格式:
* ```javascript
* // 成功时返回:
* {
* success: true,
* url: "https://kv.wuyuan.dev/device-uuid-123/exam_configs?token=abc123", // 私有访问时包含token
* migrated: true, // 是否成功迁移了本地数据
* configured: false // 是否自动配置了云端设置
* }
*
* // 公开访问时返回:
* {
* success: true,
* url: "https://kv.wuyuan.dev/device-uuid-123/exam_configs", // 公开访问不包含token
* migrated: false,
* configured: true
* }
*
* // 失败时返回:
* {
* success: false,
* error: {
* code: "CLOUD_URL_ERROR",
* message: "获取键云端地址失败"
* }
* }
* ```
*/
async getKeyCloudUrl(key, options = {}) {
const {
migrateFromLocal = true,
autoConfigureCloud = true
} = options;
try {
let serverUrl = getSetting("server.domain");
let siteKey = getSetting("server.siteKey");
const machineId = getSetting("device.uuid");
let configured = false;
// 检查云端配置是否为空或错误,如果是则使用默认配置
if (!serverUrl || !machineId) {
if (autoConfigureCloud) {
// 使用classworksCloudDefaults配置
const classworksCloudDefaults = {
"server.domain": "https://kv.wuyuan.dev",
"server.siteKey": "",
};
if (!serverUrl) {
setSetting("server.domain", classworksCloudDefaults["server.domain"]);
serverUrl = classworksCloudDefaults["server.domain"];
configured = true;
}
if (!siteKey) {
setSetting("server.siteKey", classworksCloudDefaults["server.siteKey"]);
siteKey = classworksCloudDefaults["server.siteKey"];
}
// 设置provider为classworkscloud
setSetting("server.provider", "classworkscloud");
} else {
return formatError("云端配置无效请检查服务器域名和设备UUID", "CONFIG_ERROR");
}
}
let migrated = false;
// 如果需要迁移本地数据到云端
if (migrateFromLocal) {
try {
// 尝试从本地读取数据
const localData = await kvLocalProvider.loadData(key);
// 如果本地有数据且不是错误响应
if (localData && localData.success !== false) {
// 检查云端是否已有数据
const cloudData = await kvServerProvider.loadData(key);
// 如果云端没有数据,则迁移本地数据
if (cloudData && cloudData.success === false && cloudData.error?.code === "NOT_FOUND") {
const saveResult = await kvServerProvider.saveData(key, localData);
if (saveResult && saveResult.success !== false) {
migrated = true;
console.log(`已成功将键 ${key} 的数据从本地迁移到云端`);
}
}
}
} catch (error) {
console.warn(`迁移键 ${key} 的数据时出错:`, error);
// 迁移失败不影响URL生成继续执行
}
}
// 构建云端访问URL
let url = `${serverUrl}/${machineId}/${key}`;
// 根据网站验证情况添加token参数
const namespaceInfo = await kvServerProvider.loadNamespaceInfo();
if (namespaceInfo && namespaceInfo.success !== false) {
const { accessType } = namespaceInfo;
// 如果是私有访问添加token参数
if (accessType === 'private' && siteKey) {
const urlObj = new URL(url);
urlObj.searchParams.set('token', siteKey);
url = urlObj.toString();
}
// 公开或受保护访问不需要token参数
}
return {
success: true,
url,
migrated,
configured
};
} catch (error) {
console.error('获取键云端地址时出错:', error);
return formatError(
error.message || "获取键云端地址失败",
"CLOUD_URL_ERROR"
);
}
},
};
export const ErrorCodes = {
NOT_FOUND: "数据不存在",
NETWORK_ERROR: "网络连接失败",
@ -96,5 +277,6 @@ export const ErrorCodes = {
CONFIG_ERROR: "配置错误",
PERMISSION_DENIED: "无权限访问",
UNAUTHORIZED: "认证失败",
CLOUD_URL_ERROR: "云端地址获取失败",
UNKNOWN_ERROR: "未知错误",
};

View File

@ -79,12 +79,10 @@ export const kvLocalProvider = {
// 设置默认参数
const {
sortBy = "key",
sortDir = "asc",
limit = 100,
skip = 0
} = options;
// 排序键名(本地存储只支持按键名排序)
const sortedKeys = allKeys.sort((a, b) => {
if (sortDir === "desc") {

View File

@ -170,6 +170,13 @@ const settingsDefinitions = {
description: "是否显示列表卡片",
icon: "mdi-list-box",
},
"display.showExamScheduleButton": {
type: "boolean",
default: true,
description: "是否显示考试看板",
icon: "mdi-calendar-check",
// 控制是否在主页显示考试看板按钮,指向考试安排页面
},
// 服务器设置(合并了数据提供者设置)
"server.domain": {
type: "string",