pwa,设置项,自动刷新
1
dev-dist/registerSW.js
Normal file
@ -0,0 +1 @@
|
||||
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
0
dev-dist/suppress-warnings.js
Normal file
92
dev-dist/sw.js
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// If the loader is already loaded, just stop.
|
||||
if (!self.define) {
|
||||
let registry = {};
|
||||
|
||||
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||
let nextDefineUri;
|
||||
|
||||
const singleRequire = (uri, parentUri) => {
|
||||
uri = new URL(uri + ".js", parentUri).href;
|
||||
return registry[uri] || (
|
||||
|
||||
new Promise(resolve => {
|
||||
if ("document" in self) {
|
||||
const script = document.createElement("script");
|
||||
script.src = uri;
|
||||
script.onload = resolve;
|
||||
document.head.appendChild(script);
|
||||
} else {
|
||||
nextDefineUri = uri;
|
||||
importScripts(uri);
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
|
||||
.then(() => {
|
||||
let promise = registry[uri];
|
||||
if (!promise) {
|
||||
throw new Error(`Module ${uri} didn’t register its module`);
|
||||
}
|
||||
return promise;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
self.define = (depsNames, factory) => {
|
||||
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||
if (registry[uri]) {
|
||||
// Module is already loading or loaded.
|
||||
return;
|
||||
}
|
||||
let exports = {};
|
||||
const require = depUri => singleRequire(depUri, uri);
|
||||
const specialDeps = {
|
||||
module: { uri },
|
||||
exports,
|
||||
require
|
||||
};
|
||||
registry[uri] = Promise.all(depsNames.map(
|
||||
depName => specialDeps[depName] || require(depName)
|
||||
)).then(deps => {
|
||||
factory(...deps);
|
||||
return exports;
|
||||
});
|
||||
};
|
||||
}
|
||||
define(['./workbox-86c9b217'], (function (workbox) { 'use strict';
|
||||
|
||||
self.skipWaiting();
|
||||
workbox.clientsClaim();
|
||||
|
||||
/**
|
||||
* The precacheAndRoute() method efficiently caches and responds to
|
||||
* requests for URLs in the manifest.
|
||||
* See https://goo.gl/S9QRab
|
||||
*/
|
||||
workbox.precacheAndRoute([{
|
||||
"url": "suppress-warnings.js",
|
||||
"revision": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.6m682f8mvag"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
allowlist: [/^\/$/]
|
||||
}));
|
||||
|
||||
}));
|
3391
dev-dist/workbox-86c9b217.js
Normal file
@ -5,6 +5,10 @@
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Classworks 作业板</title>
|
||||
<meta name="description" content="记录,查看并同步作业" />
|
||||
<link rel="apple-touch-icon" href="/image/apple-touch-icon.png" sizes="180x180" />
|
||||
<link rel="mask-icon" href="/image/mask-icon.svg" color="#212121" />
|
||||
<meta name="theme-color" content="#212121" />
|
||||
<script defer src="https://umami.wuyuan.dev/script.js" data-website-id="e3f8ed7a-4db4-4081-aaf4-45396b1f479c"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -23,6 +23,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@vite-pwa/assets-generator": "^1.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
@ -38,6 +39,7 @@
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"unplugin-vue-router": "^0.10.0",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-router": "^4.4.0"
|
||||
|
3358
pnpm-lock.yaml
generated
BIN
public/image/apple-touch-icon-180x180.png
Normal file
After Width: | Height: | Size: 576 B |
BIN
public/image/favicon.ico
Normal file
After Width: | Height: | Size: 367 B |
23
public/image/logo.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" viewBox="0 0 256 256" fill="none">
|
||||
<g clip-path="url(#clip-path-74_1)">
|
||||
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
|
||||
</path>
|
||||
<rect x="0" y="0" width="256" height="128" fill="#D8C4A0" >
|
||||
</rect>
|
||||
<rect x="0" y="128" width="256" height="128" fill="#F5E0BB" >
|
||||
</rect>
|
||||
<path d="M28 228L128 128L228 128L128 228L28 228Z" fill-rule="evenodd" fill="#241A04" >
|
||||
</path>
|
||||
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A" >
|
||||
</path>
|
||||
<g >
|
||||
<path fill="#000000" d="M-3049.01 2467.94L-3043.48 2467.94L-3043.48 2466.99L-3045.92 2466.99C-3046.36 2466.99 -3046.9 2467.04 -3047.36 2467.08C-3045.29 2465.12 -3043.9 2463.33 -3043.9 2461.57C-3043.9 2460.01 -3044.9 2458.99 -3046.47 2458.99C-3047.58 2458.99 -3048.35 2459.49 -3049.06 2460.27L-3048.43 2460.9C-3047.93 2460.31 -3047.32 2459.88 -3046.6 2459.88C-3045.51 2459.88 -3044.98 2460.61 -3044.98 2461.62C-3044.98 2463.13 -3046.25 2464.88 -3049.01 2467.29L-3049.01 2467.94ZM-3039.27 2468.1C-3037.9 2468.1 -3036.74 2466.95 -3036.74 2465.24C-3036.74 2463.39 -3037.7 2462.48 -3039.19 2462.48C-3039.87 2462.48 -3040.64 2462.88 -3041.18 2463.54C-3041.13 2460.81 -3040.13 2459.89 -3038.91 2459.89C-3038.38 2459.89 -3037.85 2460.15 -3037.52 2460.56L-3036.89 2459.89C-3037.39 2459.36 -3038.04 2458.99 -3038.96 2458.99C-3040.66 2458.99 -3042.21 2460.3 -3042.21 2463.74C-3042.21 2466.65 -3040.95 2468.1 -3039.27 2468.1ZM-3041.15 2464.41C-3040.58 2463.6 -3039.91 2463.3 -3039.36 2463.3C-3038.3 2463.3 -3037.78 2464.05 -3037.78 2465.24C-3037.78 2466.44 -3038.43 2467.23 -3039.27 2467.23C-3040.37 2467.23 -3041.03 2466.24 -3041.15 2464.41ZM-3035.17 2467.94L-3030.34 2467.94L-3030.34 2467.03L-3032.1 2467.03L-3032.1 2459.15L-3032.95 2459.15C-3033.43 2459.42 -3033.99 2459.62 -3034.77 2459.77L-3034.77 2460.47L-3033.2 2460.47L-3033.2 2467.03L-3035.17 2467.03L-3035.17 2467.94ZM-3029.51 2467.94L-3028.4 2467.94L-3027.54 2465.25L-3024.33 2465.25L-3023.49 2467.94L-3022.31 2467.94L-3025.3 2459.15L-3026.54 2459.15L-3029.51 2467.94ZM-3027.27 2464.38L-3026.84 2463.02C-3026.52 2462.02 -3026.24 2461.08 -3025.96 2460.04L-3025.91 2460.04C-3025.62 2461.06 -3025.35 2462.02 -3025.02 2463.02L-3024.6 2464.38L-3027.27 2464.38ZM-3018.93 2468.1C-3017.26 2468.1 -3016.19 2466.58 -3016.19 2463.51C-3016.19 2460.47 -3017.26 2458.99 -3018.93 2458.99C-3020.61 2458.99 -3021.67 2460.47 -3021.67 2463.51C-3021.67 2466.58 -3020.61 2468.1 -3018.93 2468.1ZM-3018.93 2467.21C-3019.93 2467.21 -3020.61 2466.09 -3020.61 2463.51C-3020.61 2460.95 -3019.93 2459.85 -3018.93 2459.85C-3017.93 2459.85 -3017.25 2460.95 -3017.25 2463.51C-3017.25 2466.09 -3017.93 2467.21 -3018.93 2467.21ZM-3012.27 2468.1C-3010.6 2468.1 -3009.53 2466.58 -3009.53 2463.51C-3009.53 2460.47 -3010.6 2458.99 -3012.27 2458.99C-3013.95 2458.99 -3015.01 2460.47 -3015.01 2463.51C-3015.01 2466.58 -3013.95 2468.1 -3012.27 2468.1ZM-3012.27 2467.21C-3013.27 2467.21 -3013.95 2466.09 -3013.95 2463.51C-3013.95 2460.95 -3013.27 2459.85 -3012.27 2459.85C-3011.27 2459.85 -3010.59 2460.95 -3010.59 2463.51C-3010.59 2466.09 -3011.27 2467.21 -3012.27 2467.21Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip-path-74_1">
|
||||
<path d="M0 256L256 256L256 0L0 0L0 256Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/image/maskable-icon-512x512.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
public/image/pwa-192x192.png
Normal file
After Width: | Height: | Size: 746 B |
BIN
public/image/pwa-512x512.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
public/image/pwa-64x64.png
Normal file
After Width: | Height: | Size: 339 B |
BIN
src/assets/favicon.ico
Normal file
After Width: | Height: | Size: 367 B |
@ -1,6 +1,23 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
|
||||
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
|
||||
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
|
||||
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256" viewBox="0 0 256 256" fill="none">
|
||||
<g clip-path="url(#clip-path-74_1)">
|
||||
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
|
||||
</path>
|
||||
<rect x="0" y="0" width="256" height="128" fill="#D8C4A0" >
|
||||
</rect>
|
||||
<rect x="0" y="128" width="256" height="128" fill="#F5E0BB" >
|
||||
</rect>
|
||||
<path d="M28 228L128 128L228 128L128 228L28 228Z" fill-rule="evenodd" fill="#241A04" >
|
||||
</path>
|
||||
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A" >
|
||||
</path>
|
||||
<g >
|
||||
<path fill="#000000" d="M-3049.01 2467.94L-3043.48 2467.94L-3043.48 2466.99L-3045.92 2466.99C-3046.36 2466.99 -3046.9 2467.04 -3047.36 2467.08C-3045.29 2465.12 -3043.9 2463.33 -3043.9 2461.57C-3043.9 2460.01 -3044.9 2458.99 -3046.47 2458.99C-3047.58 2458.99 -3048.35 2459.49 -3049.06 2460.27L-3048.43 2460.9C-3047.93 2460.31 -3047.32 2459.88 -3046.6 2459.88C-3045.51 2459.88 -3044.98 2460.61 -3044.98 2461.62C-3044.98 2463.13 -3046.25 2464.88 -3049.01 2467.29L-3049.01 2467.94ZM-3039.27 2468.1C-3037.9 2468.1 -3036.74 2466.95 -3036.74 2465.24C-3036.74 2463.39 -3037.7 2462.48 -3039.19 2462.48C-3039.87 2462.48 -3040.64 2462.88 -3041.18 2463.54C-3041.13 2460.81 -3040.13 2459.89 -3038.91 2459.89C-3038.38 2459.89 -3037.85 2460.15 -3037.52 2460.56L-3036.89 2459.89C-3037.39 2459.36 -3038.04 2458.99 -3038.96 2458.99C-3040.66 2458.99 -3042.21 2460.3 -3042.21 2463.74C-3042.21 2466.65 -3040.95 2468.1 -3039.27 2468.1ZM-3041.15 2464.41C-3040.58 2463.6 -3039.91 2463.3 -3039.36 2463.3C-3038.3 2463.3 -3037.78 2464.05 -3037.78 2465.24C-3037.78 2466.44 -3038.43 2467.23 -3039.27 2467.23C-3040.37 2467.23 -3041.03 2466.24 -3041.15 2464.41ZM-3035.17 2467.94L-3030.34 2467.94L-3030.34 2467.03L-3032.1 2467.03L-3032.1 2459.15L-3032.95 2459.15C-3033.43 2459.42 -3033.99 2459.62 -3034.77 2459.77L-3034.77 2460.47L-3033.2 2460.47L-3033.2 2467.03L-3035.17 2467.03L-3035.17 2467.94ZM-3029.51 2467.94L-3028.4 2467.94L-3027.54 2465.25L-3024.33 2465.25L-3023.49 2467.94L-3022.31 2467.94L-3025.3 2459.15L-3026.54 2459.15L-3029.51 2467.94ZM-3027.27 2464.38L-3026.84 2463.02C-3026.52 2462.02 -3026.24 2461.08 -3025.96 2460.04L-3025.91 2460.04C-3025.62 2461.06 -3025.35 2462.02 -3025.02 2463.02L-3024.6 2464.38L-3027.27 2464.38ZM-3018.93 2468.1C-3017.26 2468.1 -3016.19 2466.58 -3016.19 2463.51C-3016.19 2460.47 -3017.26 2458.99 -3018.93 2458.99C-3020.61 2458.99 -3021.67 2460.47 -3021.67 2463.51C-3021.67 2466.58 -3020.61 2468.1 -3018.93 2468.1ZM-3018.93 2467.21C-3019.93 2467.21 -3020.61 2466.09 -3020.61 2463.51C-3020.61 2460.95 -3019.93 2459.85 -3018.93 2459.85C-3017.93 2459.85 -3017.25 2460.95 -3017.25 2463.51C-3017.25 2466.09 -3017.93 2467.21 -3018.93 2467.21ZM-3012.27 2468.1C-3010.6 2468.1 -3009.53 2466.58 -3009.53 2463.51C-3009.53 2460.47 -3010.6 2458.99 -3012.27 2458.99C-3013.95 2458.99 -3015.01 2460.47 -3015.01 2463.51C-3015.01 2466.58 -3013.95 2468.1 -3012.27 2468.1ZM-3012.27 2467.21C-3013.27 2467.21 -3013.95 2466.09 -3013.95 2463.51C-3013.95 2460.95 -3013.27 2459.85 -3012.27 2459.85C-3011.27 2459.85 -3010.59 2460.95 -3010.59 2463.51C-3010.59 2466.09 -3011.27 2467.21 -3012.27 2467.21Z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip-path-74_1">
|
||||
<path d="M0 256L256 256L256 0L0 0L0 256Z" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 3.2 KiB |
523
src/components/RandomPicker.vue
Normal file
@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="600" fullscreen-breakpoint="sm">
|
||||
<v-card class="random-picker-card">
|
||||
<v-card-title class="text-h5 d-flex align-center">
|
||||
<v-icon icon="mdi-account-question" class="mr-2" />
|
||||
随机点名
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="dialog = false" />
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text v-if="!isPickingStarted" class="text-center py-6">
|
||||
<div class="text-h6 mb-4">请选择抽取人数</div>
|
||||
|
||||
<div class="d-flex justify-center align-center counter-container">
|
||||
<v-btn size="x-large" icon="mdi-minus" variant="tonal" color="primary" :disabled="count <= 1"
|
||||
@click="decrementCount" class="counter-btn" />
|
||||
|
||||
<div class="count-display mx-8">
|
||||
<span class="text-h2 font-weight-bold">{{ count }}</span>
|
||||
<span class="text-subtitle-1 ml-2">人</span>
|
||||
</div>
|
||||
|
||||
<v-btn size="x-large" icon="mdi-plus" variant="tonal" color="primary" :disabled="count >= maxAllowedCount"
|
||||
@click="incrementCount" class="counter-btn" />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="mt-4">
|
||||
<v-btn size="x-large" color="primary" prepend-icon="mdi-dice-multiple" @click="startPicking"
|
||||
:disabled="filteredStudents.length === 0" class="start-btn">
|
||||
开始抽取
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredStudents.length === 0" class="mt-4 text-error">
|
||||
没有可抽取的学生,请调整过滤选项
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-caption">
|
||||
当前可抽取学生: {{ filteredStudents.length }}人
|
||||
<v-tooltip location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" icon="mdi-information-outline" size="small" class="ml-1" />
|
||||
</template>
|
||||
<div class="pa-2">
|
||||
<div v-if="tempFilters.excludeAbsent">• 已排除请假学生 ({{ absentCount }}人)</div>
|
||||
<div v-if="tempFilters.excludeLate">• 已排除迟到学生 ({{ lateCount }}人)</div>
|
||||
<div v-if="tempFilters.excludeExcluded">• 已排除不参与学生 ({{ excludedCount }}人)</div>
|
||||
</div>
|
||||
</v-tooltip><!-- 添加临时过滤选项 -->
|
||||
|
||||
<div class="d-flex flex-wrap justify-center gap-2 mt-4">
|
||||
<v-chip :color="tempFilters.excludeLate ? 'warning' : 'default'"
|
||||
:variant="tempFilters.excludeLate ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeLate = !tempFilters.excludeLate" prepend-icon="mdi-clock-alert"
|
||||
class="filter-chip">
|
||||
{{ tempFilters.excludeLate ? '排除' : '包含' }}迟到学生
|
||||
</v-chip>
|
||||
<v-chip :color="tempFilters.excludeAbsent ? 'error' : 'default'"
|
||||
:variant="tempFilters.excludeAbsent ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent" prepend-icon="mdi-account-off"
|
||||
class="filter-chip">
|
||||
{{ tempFilters.excludeAbsent ? '排除' : '包含' }}请假学生
|
||||
</v-chip>
|
||||
|
||||
|
||||
|
||||
<v-chip :color="tempFilters.excludeExcluded ? 'grey' : 'default'"
|
||||
:variant="tempFilters.excludeExcluded ? 'elevated' : 'text'"
|
||||
@click="tempFilters.excludeExcluded = !tempFilters.excludeExcluded" prepend-icon="mdi-account-cancel"
|
||||
class="filter-chip">
|
||||
{{ tempFilters.excludeExcluded ? '排除' : '包含' }}不参与学生
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text v-else class="text-center py-6">
|
||||
<div v-if="isAnimating" class="animation-container">
|
||||
<div class="animation-wrapper">
|
||||
<transition-group name="shuffle" tag="div" class="shuffle-container">
|
||||
<div v-for="(student, index) in animationStudents" :key="student.id" class="student-item"
|
||||
:class="{ 'highlighted': highlightedIndices.includes(index) }">
|
||||
{{ student.name }}
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="result-container">
|
||||
<div class="text-h6 mb-4">抽取结果</div>
|
||||
<v-card v-for="(student, index) in pickedStudents" :key="index" variant="outlined" color="primary"
|
||||
class="mb-2 result-card">
|
||||
<v-card-text class="text-h4 text-center py-4 d-flex align-center justify-center">
|
||||
{{ student }}
|
||||
<v-btn icon="mdi-refresh" variant="text" size="small" class="ml-2 refresh-btn"
|
||||
@click="refreshSingleStudent(index)" :disabled="remainingStudents.length === 0"
|
||||
:title="remainingStudents.length === 0 ? '没有更多可用学生' : '重新抽取此学生'" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="mt-8 d-flex justify-center">
|
||||
<v-btn color="primary" prepend-icon="mdi-refresh" @click="resetPicker" size="large" class="mx-2">
|
||||
重新抽取
|
||||
</v-btn>
|
||||
<v-btn color="grey" variant="outlined" @click="dialog = false" size="large" class="mx-2">
|
||||
关闭
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'RandomPicker',
|
||||
props: {
|
||||
studentList: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
attendance: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({ absent: [], late: [], exclude: [] })
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
count: getSetting('randomPicker.defaultCount'),
|
||||
isPickingStarted: false,
|
||||
isAnimating: false,
|
||||
pickedStudents: [],
|
||||
animationStudents: [],
|
||||
highlightedIndices: [],
|
||||
animationTimer: null,
|
||||
getSetting,
|
||||
// 添加临时过滤选项
|
||||
tempFilters: {
|
||||
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
|
||||
excludeLate: getSetting('randomPicker.excludeLate'),
|
||||
excludeExcluded: getSetting('randomPicker.excludeExcluded')
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 计算请假、迟到、不参与的学生数量
|
||||
absentCount() {
|
||||
return this.attendance.absent ? this.attendance.absent.length : 0;
|
||||
},
|
||||
lateCount() {
|
||||
return this.attendance.late ? this.attendance.late.length : 0;
|
||||
},
|
||||
excludedCount() {
|
||||
return this.attendance.exclude ? this.attendance.exclude.length : 0;
|
||||
},
|
||||
|
||||
// 使用临时过滤选项过滤学生
|
||||
filteredStudents() {
|
||||
if (!this.studentList || !this.studentList.length) return [];
|
||||
|
||||
return this.studentList.filter(student => {
|
||||
// 根据临时过滤选项过滤学生
|
||||
if (this.tempFilters.excludeAbsent && this.attendance.absent.includes(student)) {
|
||||
return false;
|
||||
}
|
||||
if (this.tempFilters.excludeLate && this.attendance.late.includes(student)) {
|
||||
return false;
|
||||
}
|
||||
if (this.tempFilters.excludeExcluded && this.attendance.exclude.includes(student)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// 兼容性:保留原有的 availableStudents 计算属性,但使用新的过滤逻辑
|
||||
availableStudents() {
|
||||
return this.filteredStudents;
|
||||
},
|
||||
|
||||
maxAllowedCount() {
|
||||
return Math.min(10, this.filteredStudents.length);
|
||||
},
|
||||
|
||||
// 计算剩余可用学生(排除已抽取的学生)
|
||||
remainingStudents() {
|
||||
return this.filteredStudents.filter(student => !this.pickedStudents.includes(student));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dialog(newVal) {
|
||||
if (newVal) {
|
||||
// 打开对话框时重置状态
|
||||
this.count = getSetting('randomPicker.defaultCount');
|
||||
this.isPickingStarted = false;
|
||||
this.isAnimating = false;
|
||||
this.pickedStudents = [];
|
||||
|
||||
// 重置临时过滤选项为设置中的值
|
||||
this.tempFilters = {
|
||||
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
|
||||
excludeLate: getSetting('randomPicker.excludeLate'),
|
||||
excludeExcluded: getSetting('randomPicker.excludeExcluded')
|
||||
};
|
||||
} else {
|
||||
// 关闭对话框时清除计时器
|
||||
if (this.animationTimer) {
|
||||
clearTimeout(this.animationTimer);
|
||||
this.animationTimer = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 监听过滤选项变化,确保count不超过可用学生数
|
||||
'tempFilters': {
|
||||
handler() {
|
||||
if (this.count > this.maxAllowedCount) {
|
||||
this.count = Math.max(1, this.maxAllowedCount);
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.dialog = true;
|
||||
},
|
||||
incrementCount() {
|
||||
if (this.count < this.maxAllowedCount) {
|
||||
this.count++;
|
||||
}
|
||||
},
|
||||
decrementCount() {
|
||||
if (this.count > 1) {
|
||||
this.count--;
|
||||
}
|
||||
},
|
||||
startPicking() {
|
||||
if (this.filteredStudents.length === 0) return;
|
||||
|
||||
this.isPickingStarted = true;
|
||||
|
||||
if (getSetting('randomPicker.animation')) {
|
||||
this.startAnimation();
|
||||
} else {
|
||||
this.finishPicking();
|
||||
}
|
||||
},
|
||||
startAnimation() {
|
||||
this.isAnimating = true;
|
||||
|
||||
// 创建动画用的学生列表(添加ID以便于动画)
|
||||
this.animationStudents = this.filteredStudents.map((name, index) => ({
|
||||
id: `student-${index}`,
|
||||
name
|
||||
}));
|
||||
|
||||
// 随机高亮显示
|
||||
this.animateHighlight();
|
||||
},
|
||||
animateHighlight() {
|
||||
const totalSteps = 5; // 动画总步数
|
||||
let currentStep = 0;
|
||||
const intervalTime = 50; // 初始间隔时间
|
||||
|
||||
const animate = () => {
|
||||
// 清除之前的高亮
|
||||
this.highlightedIndices = [];
|
||||
|
||||
// 随机选择要高亮的索引
|
||||
const indices = [];
|
||||
for (let i = 0; i < this.count; i++) {
|
||||
let randomIndex;
|
||||
do {
|
||||
randomIndex = Math.floor(Math.random() * this.animationStudents.length);
|
||||
} while (indices.includes(randomIndex));
|
||||
indices.push(randomIndex);
|
||||
}
|
||||
|
||||
this.highlightedIndices = indices;
|
||||
|
||||
currentStep++;
|
||||
|
||||
// 逐渐增加间隔时间,使动画变慢
|
||||
const nextInterval = intervalTime + (currentStep * 20);
|
||||
|
||||
if (currentStep < totalSteps) {
|
||||
this.animationTimer = setTimeout(animate, nextInterval);
|
||||
} else {
|
||||
// 动画结束,显示最终结果
|
||||
setTimeout(() => {
|
||||
this.finishPicking();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始动画
|
||||
animate();
|
||||
},
|
||||
finishPicking() {
|
||||
this.isAnimating = false;
|
||||
|
||||
// 随机选择学生
|
||||
const shuffled = [...this.filteredStudents].sort(() => 0.5 - Math.random());
|
||||
this.pickedStudents = shuffled.slice(0, this.count);
|
||||
},
|
||||
resetPicker() {
|
||||
this.isPickingStarted = false;
|
||||
this.isAnimating = false;
|
||||
this.pickedStudents = [];
|
||||
if (this.animationTimer) {
|
||||
clearTimeout(this.animationTimer);
|
||||
this.animationTimer = null;
|
||||
}
|
||||
},
|
||||
// 刷新单个学生
|
||||
refreshSingleStudent(index) {
|
||||
if (this.remainingStudents.length === 0) return;
|
||||
|
||||
// 从剩余学生中随机选择一个
|
||||
const randomIndex = Math.floor(Math.random() * this.remainingStudents.length);
|
||||
const newStudent = this.remainingStudents[randomIndex];
|
||||
|
||||
// 替换指定位置的学生
|
||||
this.pickedStudents[index] = newStudent;
|
||||
|
||||
// 添加动画效果
|
||||
const resultCards = document.querySelectorAll('.result-card');
|
||||
if (resultCards[index]) {
|
||||
resultCards[index].classList.add('refresh-animation');
|
||||
setTimeout(() => {
|
||||
resultCards[index].classList.remove('refresh-animation');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.random-picker-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.counter-container {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.counter-btn {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.count-display {
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
min-width: 200px;
|
||||
height: 64px;
|
||||
border-radius: 32px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
// 过滤选项卡片样式
|
||||
.filter-options-card {
|
||||
max-width: 450px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 学生列表提示框样式
|
||||
.student-list-tooltip {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 5px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.animation-container {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.animation-wrapper {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.shuffle-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.student-item {
|
||||
padding: 10px 15px;
|
||||
background-color: rgba(var(--v-theme-surface-variant), 0.7);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1.2rem;
|
||||
|
||||
&.highlighted {
|
||||
background-color: rgb(var(--v-theme-primary));
|
||||
color: rgb(var(--v-theme-on-primary));
|
||||
transform: scale(1.1);
|
||||
font-weight: bold;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.result-container {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
|
||||
.refresh-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新动画
|
||||
@keyframes refresh-pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 rgba(var(--v-theme-primary), 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 15px rgba(var(--v-theme-primary), 0.7);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 rgba(var(--v-theme-primary), 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-animation {
|
||||
animation: refresh-pulse 0.5s ease;
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.shuffle-enter-active,
|
||||
.shuffle-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.shuffle-enter-from,
|
||||
.shuffle-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.shuffle-move {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
// 触摸屏优化
|
||||
@media (hover: none) {
|
||||
|
||||
.counter-btn,
|
||||
.start-btn {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.student-item {
|
||||
padding: 12px 20px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
opacity: 1;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
min-height: 40px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
80
src/components/settings/SettingGroup.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<v-card :border="border" class="setting-group">
|
||||
<v-card-title v-if="title" class="d-flex align-center">
|
||||
<v-icon v-if="icon" :icon="icon" class="mr-2" />
|
||||
{{ title }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="description">
|
||||
{{ description }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<slot>
|
||||
<!-- 默认插槽用于放置 SettingItem 组件 -->
|
||||
</slot>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="$slots.actions">
|
||||
<slot name="actions"></slot>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingGroup',
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 设置组的标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置组的描述
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置组的图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否显示边框
|
||||
*/
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSettingUpdate(key, value) {
|
||||
this.$emit('update', key, value);
|
||||
},
|
||||
|
||||
onSettingError(key) {
|
||||
this.$emit('error', key);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
457
src/components/settings/SettingItem.vue
Normal file
@ -0,0 +1,457 @@
|
||||
<template>
|
||||
<v-list-item class="setting-item" :disabled="disabled">
|
||||
<template #prepend>
|
||||
<v-icon :icon="settingIcon" />
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="text-wrap">
|
||||
{{ displayTitle }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="d-flex align-center text-wrap">
|
||||
<span class="text-caption text-grey-darken-1">{{ settingKey }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template #append >
|
||||
<div class="d-flex flex-column flex-sm-row align-center">
|
||||
<div v-if="type !== 'string' || hasOptions" class="me-2">
|
||||
<!-- 根据设置类型渲染不同的控件 -->
|
||||
<v-switch v-if="type === 'boolean'" v-model="localValue" density="comfortable" hide-details :disabled="disabled"
|
||||
@update:model-value="updateSetting" />
|
||||
|
||||
<v-select v-else-if="type === 'string' && hasOptions" v-model="localValue" :items="selectOptions"
|
||||
density="compact" hide-details :disabled="disabled" class="setting-select" variant="outlined" bg-color="surface"
|
||||
@update:model-value="updateSetting" item-title="title" item-value="value" />
|
||||
|
||||
<div v-else-if="type === 'number'" class="d-flex align-center">
|
||||
<v-btn icon="mdi-minus" size="small" variant="text" :disabled="disabled || localValue <= minValue"
|
||||
@click="adjustValue(-stepValue)" />
|
||||
|
||||
<v-text-field v-model.number="localValue" type="number" density="compact" hide-details :min="minValue"
|
||||
:max="maxValue" :step="stepValue" :disabled="disabled" class="mx-2 setting-number-field" style="width: 80px"
|
||||
variant="outlined" bg-color="surface" @update:model-value="updateSetting" />
|
||||
|
||||
<v-btn icon="mdi-plus" size="small" variant="text" :disabled="disabled || localValue >= maxValue"
|
||||
@click="adjustValue(stepValue)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon="mdi-dots-vertical" size="small" variant="text" v-bind="props" class="ml-2"
|
||||
:disabled="disabled" />
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="copySettingId">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-key" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>复制设置ID</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item @click="copySettingValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-content-copy" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>复制设置值</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-list-item @click="resetToDefault" :disabled="isDefaultValue">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-restore" size="small" />
|
||||
</template>
|
||||
<v-list-item-title>重置为默认值</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<!-- 文本框显示在下方 -->
|
||||
<div v-if="type === 'string' && !hasOptions" class="px-4 pb-2 pt-0">
|
||||
<v-text-field v-model="localValue" density="compact" hide-details :disabled="disabled"
|
||||
class="setting-text-field mt-1" variant="outlined" bg-color="surface" @update:model-value="updateSetting" />
|
||||
</div>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="showSnackbar" :timeout="2000" color="success" location="top">
|
||||
{{ snackbarText }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting, getSettingDefinition, resetSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'SettingItem',
|
||||
|
||||
props: {
|
||||
/**
|
||||
* 设置项的键名,例如 'display.cardHoverEffect'
|
||||
*/
|
||||
settingKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义图标,如果不提供则使用默认图标
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否禁用此设置项
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义标题,如果不提供则使用定义中的描述或键名
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/**
|
||||
* 可选的自定义描述,如果不提供则不显示描述
|
||||
*/
|
||||
description: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localValue: null,
|
||||
definition: null,
|
||||
type: null,
|
||||
selectOptions: [],
|
||||
hasOptions: false,
|
||||
minValue: 0,
|
||||
maxValue: 100,
|
||||
stepValue: 1,
|
||||
showSnackbar: false,
|
||||
snackbarText: '',
|
||||
fontFamilies: [
|
||||
{ title: 'Arial', value: 'Arial, sans-serif' },
|
||||
{ title: 'Calibri', value: 'Calibri, sans-serif' },
|
||||
{ title: 'Cambria', value: 'Cambria, serif' },
|
||||
{ title: 'Consolas', value: 'Consolas, monospace' },
|
||||
{ title: 'Courier New', value: 'Courier New, monospace' },
|
||||
{ title: 'Georgia', value: 'Georgia, serif' },
|
||||
{ title: 'Helvetica', value: 'Helvetica, sans-serif' },
|
||||
{ title: 'Segoe UI', value: 'Segoe UI, sans-serif' },
|
||||
{ title: 'Times New Roman', value: 'Times New Roman, serif' },
|
||||
{ title: 'Trebuchet MS', value: 'Trebuchet MS, sans-serif' },
|
||||
{ title: 'Verdana', value: 'Verdana, sans-serif' },
|
||||
{ title: 'Monospace', value: 'monospace' },
|
||||
{ title: 'Sans-serif', value: 'sans-serif' },
|
||||
{ title: 'Serif', value: 'serif' }
|
||||
],
|
||||
// 设置项的显示名称映射
|
||||
displayValueMappings: {
|
||||
'display.emptySubjectDisplay': {
|
||||
'card': '卡片',
|
||||
'button': '按钮'
|
||||
},
|
||||
'theme.mode': {
|
||||
'light': '浅色',
|
||||
'dark': '深色'
|
||||
},
|
||||
'server.provider': {
|
||||
'server': '远程服务器',
|
||||
'indexedDB': '本地存储'
|
||||
}
|
||||
},
|
||||
// 默认图标映射,按设置类型
|
||||
defaultIcons: {
|
||||
'boolean': 'mdi-toggle-switch-outline',
|
||||
'number': 'mdi-numeric',
|
||||
'string': 'mdi-form-textbox'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayTitle() {
|
||||
// 优先使用自定义标题
|
||||
if (this.title) {
|
||||
return this.title;
|
||||
}
|
||||
|
||||
// 其次使用定义中的描述
|
||||
if (this.definition && this.definition.description) {
|
||||
return this.definition.description;
|
||||
}
|
||||
|
||||
// 最后使用键名的最后一部分
|
||||
const parts = this.settingKey.split('.');
|
||||
return parts[parts.length - 1];
|
||||
},
|
||||
|
||||
displayDescription() {
|
||||
// 优先使用自定义描述
|
||||
if (this.description) {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
// 否则使用设置键名
|
||||
return this.settingKey;
|
||||
},
|
||||
|
||||
// 判断是否为字体系列设置
|
||||
isFontFamily() {
|
||||
return this.settingKey.toLowerCase().includes('fontfamily') ||
|
||||
this.settingKey.toLowerCase().includes('font.family');
|
||||
},
|
||||
|
||||
// 判断当前值是否为默认值
|
||||
isDefaultValue() {
|
||||
if (!this.definition) return true;
|
||||
|
||||
// 对于对象或数组,需要进行深度比较
|
||||
if (typeof this.localValue === 'object' && this.localValue !== null) {
|
||||
return JSON.stringify(this.localValue) === JSON.stringify(this.definition.default);
|
||||
}
|
||||
|
||||
return this.localValue === this.definition.default;
|
||||
},
|
||||
|
||||
// 获取设置项的图标
|
||||
settingIcon() {
|
||||
// 优先使用props中传入的图标
|
||||
if (this.icon) {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
// 其次使用定义中的图标
|
||||
if (this.definition && this.definition.icon) {
|
||||
return this.definition.icon;
|
||||
}
|
||||
|
||||
// 最后使用基于类型的默认图标
|
||||
return this.defaultIcons[this.type] || 'mdi-cog-outline';
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadSetting();
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadSetting() {
|
||||
// 获取设置定义
|
||||
this.definition = getSettingDefinition(this.settingKey);
|
||||
|
||||
if (!this.definition) {
|
||||
console.error(`未找到设置项定义: ${this.settingKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置类型
|
||||
this.type = this.definition.type;
|
||||
|
||||
// 加载设置值
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
|
||||
// 处理选项(对于字符串类型的设置)
|
||||
if (this.type === 'string') {
|
||||
// 检查是否有字体设置
|
||||
if (this.isFontFamily) {
|
||||
this.selectOptions = this.fontFamilies;
|
||||
this.hasOptions = true;
|
||||
}
|
||||
// 检查是否有显示值映射
|
||||
else if (this.settingKey in this.displayValueMappings) {
|
||||
const mapping = this.displayValueMappings[this.settingKey];
|
||||
this.selectOptions = Object.entries(mapping).map(([value, title]) => ({
|
||||
title,
|
||||
value
|
||||
}));
|
||||
this.hasOptions = true;
|
||||
}
|
||||
// 检查是否有验证函数中的选项
|
||||
else if (this.definition.validate) {
|
||||
const validateStr = this.definition.validate.toString();
|
||||
const match = validateStr.match(/\[(.*?)\]/);
|
||||
|
||||
if (match) {
|
||||
const optionsStr = match[1];
|
||||
const options = optionsStr.split(',').map(opt => {
|
||||
const cleaned = opt.trim().replace(/['"]/g, '');
|
||||
// 检查是否有显示值映射
|
||||
const displayValue = this.getDisplayValue(cleaned);
|
||||
return {
|
||||
title: displayValue || cleaned,
|
||||
value: cleaned
|
||||
};
|
||||
});
|
||||
|
||||
if (options.length > 0) {
|
||||
this.selectOptions = options;
|
||||
this.hasOptions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理数字类型的设置
|
||||
if (this.type === 'number' && this.definition.validate) {
|
||||
const validateStr = this.definition.validate.toString();
|
||||
|
||||
// 尝试提取最小值
|
||||
const minMatch = validateStr.match(/value\s*>=\s*(\d+)/);
|
||||
if (minMatch) {
|
||||
this.minValue = Number(minMatch[1]);
|
||||
}
|
||||
|
||||
// 尝试提取最大值
|
||||
const maxMatch = validateStr.match(/value\s*<=\s*(\d+)/);
|
||||
if (maxMatch) {
|
||||
this.maxValue = Number(maxMatch[1]);
|
||||
}
|
||||
|
||||
// 设置合理的步长
|
||||
const range = this.maxValue - this.minValue;
|
||||
if (range > 100) {
|
||||
this.stepValue = 10;
|
||||
} else if (range > 20) {
|
||||
this.stepValue = 5;
|
||||
} else if (range > 10) {
|
||||
this.stepValue = 2;
|
||||
} else {
|
||||
this.stepValue = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取设置值的显示名称
|
||||
getDisplayValue(value) {
|
||||
if (this.settingKey in this.displayValueMappings) {
|
||||
const mapping = this.displayValueMappings[this.settingKey];
|
||||
return mapping[value] || value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
updateSetting(value) {
|
||||
// 确保值的类型正确
|
||||
let typedValue = value;
|
||||
|
||||
if (this.type === 'boolean') {
|
||||
typedValue = Boolean(value);
|
||||
} else if (this.type === 'number') {
|
||||
typedValue = Number(value);
|
||||
|
||||
// 确保值在范围内
|
||||
if (typedValue < this.minValue) typedValue = this.minValue;
|
||||
if (typedValue > this.maxValue) typedValue = this.maxValue;
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const success = setSetting(this.settingKey, typedValue);
|
||||
|
||||
if (success) {
|
||||
this.$emit('update', this.settingKey, typedValue);
|
||||
} else {
|
||||
// 如果保存失败,恢复原值
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
this.$emit('error', this.settingKey);
|
||||
}
|
||||
},
|
||||
|
||||
adjustValue(amount) {
|
||||
if (this.type !== 'number') return;
|
||||
|
||||
const newValue = this.localValue + amount;
|
||||
|
||||
if (newValue >= this.minValue && newValue <= this.maxValue) {
|
||||
this.localValue = newValue;
|
||||
this.updateSetting(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
// 复制设置ID到剪贴板
|
||||
copySettingId() {
|
||||
navigator.clipboard.writeText(this.settingKey)
|
||||
.then(() => {
|
||||
this.showSnackbarMessage('设置ID已复制到剪贴板');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 复制设置值到剪贴板
|
||||
copySettingValue() {
|
||||
let valueText = '';
|
||||
|
||||
if (typeof this.localValue === 'object' && this.localValue !== null) {
|
||||
valueText = JSON.stringify(this.localValue);
|
||||
} else {
|
||||
valueText = String(this.localValue);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(valueText)
|
||||
.then(() => {
|
||||
this.showSnackbarMessage('设置值已复制到剪贴板');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 重置为默认值
|
||||
resetToDefault() {
|
||||
if (!this.definition) return;
|
||||
|
||||
resetSetting(this.settingKey);
|
||||
this.localValue = getSetting(this.settingKey);
|
||||
this.showSnackbarMessage('已重置为默认值');
|
||||
this.$emit('update', this.settingKey, this.localValue);
|
||||
},
|
||||
|
||||
// 显示消息提示
|
||||
showSnackbarMessage(message) {
|
||||
this.snackbarText = message;
|
||||
this.showSnackbar = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.setting-item {
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.setting-text-field,
|
||||
.setting-select,
|
||||
.setting-number-field {
|
||||
min-width: 180px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.setting-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
148
src/components/settings/SettingsExplorer.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="settings-explorer">
|
||||
|
||||
|
||||
<div >
|
||||
<v-text-field v-model="searchQuery" label="搜索设置" prepend-inner-icon="mdi-magnify" clearable variant="outlined"
|
||||
density="comfortable" class="mb-4" />
|
||||
|
||||
|
||||
|
||||
<v-list>
|
||||
<div v-for="setting in allSettings" :key="setting.key">
|
||||
<setting-item :key="setting.key" :setting-key="setting.key"
|
||||
:disabled="setting.requireDeveloper && !isDeveloperMode" @update="onSettingUpdate" @error="onSettingError" />
|
||||
<v-divider class="my-2" />
|
||||
</div>
|
||||
</v-list><v-card border>
|
||||
<v-card-title class="text-subtitle-1">当前配置</v-card-title>
|
||||
<v-card-text>
|
||||
<pre class="settings-json">{{ formattedSettings }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="copySettingsToClipboard">
|
||||
复制到剪贴板
|
||||
<v-icon right>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, settingsDefinitions, exportSettingsAsKeyValue, watchSettings } from '@/utils/settings';
|
||||
import SettingItem from './SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'SettingsExplorer',
|
||||
|
||||
components: {
|
||||
SettingItem
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
currentSettings: {},
|
||||
unwatchFunction: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isDeveloperMode() {
|
||||
return getSetting('developer.enabled');
|
||||
},
|
||||
|
||||
allSettings() {
|
||||
const settings = [];
|
||||
|
||||
for (const [key, definition] of Object.entries(settingsDefinitions)) {
|
||||
// 如果有搜索查询,过滤结果
|
||||
if (this.searchQuery && !key.toLowerCase().includes(this.searchQuery.toLowerCase()) &&
|
||||
!definition.description?.toLowerCase().includes(this.searchQuery.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
settings.push({
|
||||
key,
|
||||
...definition
|
||||
});
|
||||
}
|
||||
|
||||
return settings;
|
||||
},
|
||||
|
||||
formattedSettings() {
|
||||
return JSON.stringify(this.currentSettings, null, 2);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
// 初始化当前设置
|
||||
this.updateCurrentSettings();
|
||||
|
||||
// 监听设置变化
|
||||
this.unwatchFunction = watchSettings(() => {
|
||||
this.updateCurrentSettings();
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// 组件销毁前取消监听
|
||||
if (this.unwatchFunction) {
|
||||
this.unwatchFunction();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateCurrentSettings() {
|
||||
this.currentSettings = exportSettingsAsKeyValue();
|
||||
},
|
||||
|
||||
onSettingUpdate(key, value) {
|
||||
this.$emit('update', key, value);
|
||||
// 设置更新后立即更新当前设置显示
|
||||
this.updateCurrentSettings();
|
||||
},
|
||||
|
||||
onSettingError(key) {
|
||||
this.$emit('error', key);
|
||||
},
|
||||
|
||||
copySettingsToClipboard() {
|
||||
navigator.clipboard.writeText(JSON.stringify(this.currentSettings))
|
||||
.then(() => {
|
||||
// 可以添加一个提示,表示复制成功
|
||||
this.$emit('message', { type: 'success', text: '设置已复制到剪贴板' });
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('复制到剪贴板失败:', err);
|
||||
this.$emit('message', { type: 'error', text: '复制到剪贴板失败' });
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-explorer {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.settings-json {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.v-theme--dark .settings-json {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
</style>
|
@ -144,7 +144,7 @@ export default {
|
||||
|
||||
async clearIndexedDB() {
|
||||
try {
|
||||
const DBName = "HomeworkDB";
|
||||
const DBName = "ClassworksDB";
|
||||
// 删除整个数据库
|
||||
await window.indexedDB.deleteDatabase(DBName);
|
||||
this.$message.success("清除成功", "数据库缓存已清除");
|
||||
@ -156,7 +156,7 @@ export default {
|
||||
|
||||
async exportData() {
|
||||
try {
|
||||
const DBName = "HomeworkDB";
|
||||
const DBName = "ClassworksDB";
|
||||
const data = { indexedDB: {} };
|
||||
|
||||
// 打开数据库
|
||||
|
@ -1,260 +1,46 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="显示设置"
|
||||
icon="mdi-monitor"
|
||||
border
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<settings-card title="显示设置" icon="mdi-monitor" border>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-card-outline" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>空科目显示方式</v-list-item-title>
|
||||
<v-list-item-subtitle>选择空科目的显示方式</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-select
|
||||
v-model="emptySubjectDisplay"
|
||||
:items="displayOptions"
|
||||
density="compact"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
style="max-width: 150px"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.emptySubjectDisplay'" />
|
||||
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-sort-variant" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>动态排序</v-list-item-title>
|
||||
<v-list-item-subtitle>优化卡片布局以提高显示效果</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="dynamicSort"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.dynamicSort'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-dice-multiple" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示随机按钮</v-list-item-title>
|
||||
<v-list-item-subtitle>在主页显示随机点名按钮</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showRandomButton"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.showRandomButton'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-fullscreen" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示全屏按钮</v-list-item-title>
|
||||
<v-list-item-subtitle>在主页显示全屏切换按钮</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showFullscreenButton"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.showFullscreenButton'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-cards-outline" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>卡片悬浮效果</v-list-item-title>
|
||||
<v-list-item-subtitle>启用卡片悬停时的动画效果</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="cardHoverEffect"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.cardHoverEffect'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-gesture-tap" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>增强触摸模式</v-list-item-title>
|
||||
<v-list-item-subtitle>优化触摸屏操作体验</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="enhancedTouchMode"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item :setting-key="'display.enhancedTouchMode'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'display.showAntiScreenBurnCard'" />
|
||||
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-shield-check" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>显示防烧屏提示</v-list-item-title>
|
||||
<v-list-item-subtitle>显示防烧屏技术提示卡片</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="showAntiScreenBurnCard"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
export default {
|
||||
name: 'DisplaySettingsCard',
|
||||
components: { SettingsCard },
|
||||
components: { SettingsCard, SettingItem },
|
||||
data() {
|
||||
const settings = {
|
||||
emptySubjectDisplay: getSetting('display.emptySubjectDisplay'),
|
||||
dynamicSort: getSetting('display.dynamicSort'),
|
||||
showRandomButton: getSetting('display.showRandomButton'),
|
||||
showFullscreenButton: getSetting('display.showFullscreenButton'),
|
||||
cardHoverEffect: getSetting('display.cardHoverEffect'),
|
||||
enhancedTouchMode: getSetting('display.enhancedTouchMode'),
|
||||
showAntiScreenBurnCard: getSetting('display.showAntiScreenBurnCard')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true,
|
||||
displayOptions: [
|
||||
{ title: '卡片', value: 'card' },
|
||||
{ title: '按钮', value: 'button' }
|
||||
]
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
},
|
||||
emptySubjectDisplay: {
|
||||
get() {
|
||||
return this.localSettings.emptySubjectDisplay;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.emptySubjectDisplay = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
dynamicSort: {
|
||||
get() {
|
||||
return this.localSettings.dynamicSort;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.dynamicSort = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showRandomButton: {
|
||||
get() {
|
||||
return this.localSettings.showRandomButton;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showRandomButton = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showFullscreenButton: {
|
||||
get() {
|
||||
return this.localSettings.showFullscreenButton;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showFullscreenButton = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
cardHoverEffect: {
|
||||
get() {
|
||||
return this.localSettings.cardHoverEffect;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.cardHoverEffect = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
enhancedTouchMode: {
|
||||
get() {
|
||||
return this.localSettings.enhancedTouchMode;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.enhancedTouchMode = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
},
|
||||
showAntiScreenBurnCard: {
|
||||
get() {
|
||||
return this.localSettings.showAntiScreenBurnCard;
|
||||
},
|
||||
set(value) {
|
||||
this.localSettings.showAntiScreenBurnCard = value;
|
||||
this.$emit('saved');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`display.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
</script>
|
||||
|
@ -1,135 +1,27 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="编辑设置"
|
||||
icon="mdi-pencil-cog"
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-content-save" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>自动保存</v-list-item-title>
|
||||
<v-list-item-subtitle>在编辑完成后自动保存到服务器</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.autoSave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<settings-card title="编辑设置" icon="mdi-cog">
|
||||
<v-list>
|
||||
<setting-item :setting-key="'edit.autoSave'" />
|
||||
|
||||
<v-divider v-if="localSettings.autoSave" class="my-2" />
|
||||
|
||||
<v-list-item v-if="localSettings.autoSave">
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-calendar-lock" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>禁止自动保存非当天数据</v-list-item-title>
|
||||
<v-list-item-subtitle>仅允许自动保存当天的数据,避免误修改历史记录</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.blockNonTodayAutoSave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.blockNonTodayAutoSave'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-calendar-alert" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>确认保存历史数据</v-list-item-title>
|
||||
<v-list-item-subtitle>保存非当天数据时显示确认对话框</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.confirmNonTodaySave"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.confirmNonTodaySave'" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-refresh" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>编辑前刷新</v-list-item-title>
|
||||
<v-list-item-subtitle>在打开编辑框前从服务器获取最新数据</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.refreshBeforeEdit"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-form>
|
||||
<v-divider class="my-2" />
|
||||
<setting-item :setting-key="'edit.refreshBeforeEdit'" />
|
||||
|
||||
</v-list>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
import SettingItem from '../SettingItem.vue';
|
||||
|
||||
export default {
|
||||
name: 'EditSettingsCard',
|
||||
components: { SettingsCard },
|
||||
data() {
|
||||
const settings = {
|
||||
autoSave: getSetting('edit.autoSave'),
|
||||
blockNonTodayAutoSave: getSetting('edit.blockNonTodayAutoSave'),
|
||||
confirmNonTodaySave: getSetting('edit.confirmNonTodaySave'),
|
||||
refreshBeforeEdit: getSetting('edit.refreshBeforeEdit')
|
||||
};
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`edit.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
117
src/components/settings/cards/RandomPickerSettingsCard.vue
Normal file
@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<v-card border class="mb-4">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-account-question" class="mr-2" />
|
||||
随机点名设置
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-switch
|
||||
v-model="randomPickerEnabled"
|
||||
label="启用随机点名功能"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="randomPickerAnimation"
|
||||
label="启用随机点名动画效果"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-4"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-slider
|
||||
v-model="defaultCount"
|
||||
label="默认抽取人数"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
thumb-label
|
||||
:disabled="!randomPickerEnabled"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="text-subtitle-1 mb-2">学生过滤设置</div>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeAbsent"
|
||||
label="排除请假学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeLate"
|
||||
label="排除迟到学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="excludeExcluded"
|
||||
label="排除不参与学生"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
:disabled="!randomPickerEnabled"
|
||||
/>
|
||||
|
||||
<v-alert
|
||||
v-if="randomPickerEnabled"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-4"
|
||||
density="compact"
|
||||
>
|
||||
随机点名功能将在主页显示一个按钮,点击后可以随机抽取学生。
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
export default {
|
||||
name: 'RandomPickerSettingsCard',
|
||||
data() {
|
||||
return {
|
||||
randomPickerEnabled: getSetting('randomPicker.enabled'),
|
||||
randomPickerAnimation: getSetting('randomPicker.animation'),
|
||||
defaultCount: getSetting('randomPicker.defaultCount'),
|
||||
excludeAbsent: getSetting('randomPicker.excludeAbsent'),
|
||||
excludeLate: getSetting('randomPicker.excludeLate'),
|
||||
excludeExcluded: getSetting('randomPicker.excludeExcluded')
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
randomPickerEnabled(newValue) {
|
||||
setSetting('randomPicker.enabled', newValue);
|
||||
},
|
||||
randomPickerAnimation(newValue) {
|
||||
setSetting('randomPicker.animation', newValue);
|
||||
},
|
||||
defaultCount(newValue) {
|
||||
setSetting('randomPicker.defaultCount', newValue);
|
||||
},
|
||||
excludeAbsent(newValue) {
|
||||
setSetting('randomPicker.excludeAbsent', newValue);
|
||||
},
|
||||
excludeLate(newValue) {
|
||||
setSetting('randomPicker.excludeLate', newValue);
|
||||
},
|
||||
excludeExcluded(newValue) {
|
||||
setSetting('randomPicker.excludeExcluded', newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,102 +1,28 @@
|
||||
<template>
|
||||
<settings-card
|
||||
title="刷新设置"
|
||||
icon="mdi-refresh-circle"
|
||||
>
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<settings-card title="刷新设置" icon="mdi-refresh-circle">
|
||||
<v-form>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-refresh" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>自动刷新</v-list-item-title>
|
||||
<v-list-item-subtitle>在后台自动刷新数据</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-switch
|
||||
v-model="localSettings.auto"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<setting-item setting-key="refresh.auto" title="自动刷新" /> <v-divider class="my-2" />
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon icon="mdi-timer" class="mr-3" />
|
||||
</template>
|
||||
<v-list-item-title>刷新间隔</v-list-item-title>
|
||||
<v-list-item-subtitle>设置自动刷新的时间间隔(分钟)</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-text-field
|
||||
v-model="localSettings.interval"
|
||||
type="number"
|
||||
min="1"
|
||||
max="60"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
@click="reset"
|
||||
:disabled="!hasChanges"
|
||||
>
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
<setting-item setting-key="refresh.interval" title="刷新间隔" />
|
||||
</v-list>
|
||||
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from '@/components/SettingsCard.vue';
|
||||
import { getSetting, setSetting } from '@/utils/settings';
|
||||
|
||||
import SettingItem from '@/components/settings/SettingItem.vue';
|
||||
export default {
|
||||
name: 'RefreshSettingsCard',
|
||||
components: { SettingsCard },
|
||||
data() {
|
||||
const settings = {
|
||||
auto: getSetting('refresh.auto'),
|
||||
interval: getSetting('refresh.interval')
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return JSON.stringify(this.localSettings) !== JSON.stringify(this.originalSettings);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
setSetting(`refresh.${key}`, value);
|
||||
});
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -1,52 +1,19 @@
|
||||
<template>
|
||||
<settings-card title="数据源设置" icon="mdi-database" :loading="loading">
|
||||
<v-form v-model="isValid" @submit.prevent="save">
|
||||
<v-select
|
||||
v-model="localSettings.provider"
|
||||
:items="dataProviders"
|
||||
label="数据提供者"
|
||||
class="mb-4"
|
||||
/>
|
||||
<v-expand-transition>
|
||||
<div v-if="localSettings.provider === 'server'">
|
||||
<v-text-field
|
||||
v-model="localSettings.domain"
|
||||
label="服务器域名"
|
||||
placeholder="例如: http://example.com"
|
||||
prepend-inner-icon="mdi-web"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
<v-form>
|
||||
<setting-item setting-key="server.provider" title="数据提供者" />
|
||||
<v-divider class="my-2" />
|
||||
<setting-item setting-key="server.domain" title="服务器域名" /> <v-divider class="my-2" />
|
||||
|
||||
<v-text-field
|
||||
v-model="localSettings.classNumber"
|
||||
label="班号"
|
||||
placeholder="例如: 1 或 A"
|
||||
prepend-inner-icon="mdi-account-group"
|
||||
persistent-hint
|
||||
/>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!hasChanges || !isValid"
|
||||
prepend-icon="mdi-content-save"
|
||||
>
|
||||
保存更改
|
||||
</v-btn>
|
||||
<v-btn variant="outlined" @click="reset" :disabled="!hasChanges">
|
||||
重置
|
||||
</v-btn>
|
||||
</div>
|
||||
<setting-item setting-key="server.classNumber" title="班号" />
|
||||
|
||||
</v-form>
|
||||
</settings-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingsCard from "@/components/SettingsCard.vue";
|
||||
import { getSetting, setSetting } from "@/utils/settings";
|
||||
|
||||
import SettingItem from "@/components/settings/SettingItem.vue";
|
||||
export default {
|
||||
name: "ServerSettingsCard",
|
||||
components: { SettingsCard },
|
||||
@ -54,68 +21,12 @@ export default {
|
||||
loading: Boolean,
|
||||
},
|
||||
data() {
|
||||
const settings = {
|
||||
provider: getSetting("server.provider"),
|
||||
domain: getSetting("server.domain"),
|
||||
classNumber: getSetting("server.classNumber"),
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
localSettings: { ...settings },
|
||||
originalSettings: settings,
|
||||
isValid: true,
|
||||
dataProviders: [
|
||||
{ title: "服务器", value: "server" },
|
||||
{ title: "本地数据库", value: "indexedDB" },
|
||||
],
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChanges() {
|
||||
return (
|
||||
JSON.stringify(this.localSettings) !==
|
||||
JSON.stringify(this.originalSettings)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
try {
|
||||
// 先保存设置
|
||||
Object.entries(this.localSettings).forEach(([key, value]) => {
|
||||
const success = setSetting(`server.${key}`, value);
|
||||
if (!success) {
|
||||
throw new Error(`保存设置 ${key} 失败`);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果切换到服务器模式,验证服务器连接
|
||||
if (this.localSettings.provider === 'server' && this.localSettings.domain) {
|
||||
const testUrl = `${this.localSettings.domain.replace(/\/$/, '')}/api/test`;
|
||||
try {
|
||||
const response = await fetch(testUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('服务器连接测试失败');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('无法连接到服务器,请检查域名设置');
|
||||
}
|
||||
}
|
||||
|
||||
this.originalSettings = { ...this.localSettings };
|
||||
this.$emit('saved');
|
||||
|
||||
// 延迟刷新页面,让用户看到保存成功的提示
|
||||
setTimeout(() => {
|
||||
//window.location.reload();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
this.$message?.error('保存失败', error.message);
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.localSettings = { ...this.originalSettings };
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
@ -85,14 +85,10 @@
|
||||
上传
|
||||
</v-btn>
|
||||
<v-btn v-else color="success" size="large" @click="showSyncMessage">
|
||||
同步完成 </v-btn><v-btn v-if="showRandomButton" color="yellow" prepend-icon="mdi-account-question"
|
||||
append-icon="mdi-dice-multiple" size="large" class="ml-2" href="classisland://plugins/IslandCaller/Run">
|
||||
同步完成 </v-btn>
|
||||
<v-btn v-if="showRandomPickerButton" color="amber" prepend-icon="mdi-account-question"
|
||||
append-icon="mdi-dice-multiple" size="large" class="ml-2" @click="openRandomPicker">
|
||||
随机点名
|
||||
</v-btn>
|
||||
<v-btn v-if="showFullscreenButton" :color="state.isFullscreen ? 'blue-grey' : 'blue'"
|
||||
:prepend-icon="state.isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'" size="large" class="ml-2"
|
||||
@click="toggleFullscreen">
|
||||
{{ state.isFullscreen ? '退出全屏' : '全屏显示' }}
|
||||
</v-btn><!-- 修改防烧屏提示卡片,使用 tonal 样式减少信息密度 -->
|
||||
<v-card v-if="showAntiScreenBurnCard" border class="mt-4 anti-burn-card" color="primary" variant="tonal">
|
||||
<v-card-title class="text-subtitle-1">
|
||||
@ -142,7 +138,7 @@
|
||||
</h2>
|
||||
<h3 class="gray-text" v-for="(name, index) in state.boardData.attendance.absent" :key="'absent-' + index">
|
||||
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span><span style="white-space: nowrap">{{ name
|
||||
}}</span>
|
||||
}}</span>
|
||||
</h3>
|
||||
<h2>
|
||||
<snap style="white-space: nowrap">迟到</snap>:
|
||||
@ -152,7 +148,7 @@
|
||||
</h2>
|
||||
<h3 class="gray-text" v-for="(name, index) in state.boardData.attendance.late" :key="'late-' + index">
|
||||
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span><span style="white-space: nowrap">{{ name
|
||||
}}</span>
|
||||
}}</span>
|
||||
</h3>
|
||||
<h2>
|
||||
<snap style="white-space: nowrap">不参与</snap>:
|
||||
@ -162,7 +158,7 @@
|
||||
</h2>
|
||||
<h3 class="gray-text" v-for="(name, index) in state.boardData.attendance.exclude" :key="'exclude-' + index">
|
||||
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span><span style="white-space: nowrap">{{ name
|
||||
}}</span>
|
||||
}}</span>
|
||||
</h3>
|
||||
</v-col>
|
||||
</div>
|
||||
@ -183,7 +179,8 @@
|
||||
{{ state.snackbarText }}
|
||||
</v-snackbar>
|
||||
|
||||
<v-dialog v-model="state.attendanceDialog" max-width="900" fullscreen-breakpoint="sm" @update:model-value="handleAttendanceDialogClose">
|
||||
<v-dialog v-model="state.attendanceDialog" max-width="900" fullscreen-breakpoint="sm"
|
||||
@update:model-value="handleAttendanceDialogClose">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-account-group" class="mr-2" />
|
||||
@ -195,107 +192,55 @@
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
|
||||
|
||||
|
||||
<!-- 批量操作和搜索 -->
|
||||
<v-row class="mb-4">
|
||||
|
||||
|
||||
<v-col cols="12" md="12">
|
||||
<v-text-field
|
||||
v-model="attendanceSearch"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
label="搜索学生"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
@update:model-value="handleSearchChange"
|
||||
/>
|
||||
<div class="text-caption mt-1">支持筛选姓氏,如输入"张"可筛选所有姓张的学生</div>
|
||||
|
||||
<!-- 智能姓氏筛选 -->
|
||||
<v-text-field v-model="attendanceSearch" prepend-inner-icon="mdi-magnify" label="搜索学生"
|
||||
hint="支持筛选姓氏,如输入'孙'可筛选所有姓孙的学生" variant="outlined" clearable @update:model-value="handleSearchChange" />
|
||||
|
||||
<!-- 姓氏筛选 -->
|
||||
<div class="d-flex flex-wrap mt-2 gap-1">
|
||||
<v-btn
|
||||
v-for="surname in extractedSurnames"
|
||||
:key="surname.name"
|
||||
size="small"
|
||||
:variant="attendanceSearch === surname.name ? 'elevated' : 'text'"
|
||||
:color="attendanceSearch === surname.name ? 'primary' : ''"
|
||||
@click="attendanceSearch = attendanceSearch === surname.name ? '' : surname.name"
|
||||
class="surname-btn"
|
||||
>
|
||||
<v-btn v-for="surname in extractedSurnames" :key="surname.name"
|
||||
:variant="attendanceSearch === surname.name ? 'elevated' : 'text'"
|
||||
:color="attendanceSearch === surname.name ? 'primary' : ''"
|
||||
@click="attendanceSearch = attendanceSearch === surname.name ? '' : surname.name">
|
||||
{{ surname.name }}
|
||||
<span class="text-caption ml-1">({{ surname.count }})</span>
|
||||
({{ surname.count }})
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 搜索建议 -->
|
||||
<div v-if="searchSuggestions.length > 0" class="mt-2">
|
||||
<div class="text-caption text-grey">搜索建议:</div>
|
||||
<div class="d-flex flex-wrap gap-1 mt-1">
|
||||
<v-btn
|
||||
v-for="suggestion in searchSuggestions"
|
||||
:key="suggestion"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
color="secondary"
|
||||
@click="attendanceSearch = suggestion"
|
||||
class="suggestion-btn"
|
||||
density="compact"
|
||||
>
|
||||
{{ suggestion }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 过滤器 -->
|
||||
<div class="d-flex flex-wrap mb-4 gap-2">
|
||||
<div>
|
||||
<v-chip
|
||||
value="present"
|
||||
:color="attendanceFilter.includes('present') ? 'success' : ''"
|
||||
:variant="attendanceFilter.includes('present') ? 'elevated' : 'tonal'"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('present')"
|
||||
prepend-icon="mdi-account-check"
|
||||
:append-icon="attendanceFilter.includes('present') ? 'mdi-check' : ''"
|
||||
>
|
||||
<v-chip value="present" :color="attendanceFilter.includes('present') ? 'success' : ''"
|
||||
:variant="attendanceFilter.includes('present') ? 'elevated' : 'tonal'" class="px-2 filter-chip"
|
||||
@click="toggleFilter('present')" prepend-icon="mdi-account-check"
|
||||
:append-icon="attendanceFilter.includes('present') ? 'mdi-check' : ''">
|
||||
到课
|
||||
</v-chip>
|
||||
|
||||
<v-chip
|
||||
value="absent"
|
||||
:color="attendanceFilter.includes('absent') ? 'error' : ''"
|
||||
:variant="attendanceFilter.includes('absent') ? 'elevated' : 'tonal'"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('absent')"
|
||||
prepend-icon="mdi-account-off"
|
||||
:append-icon="attendanceFilter.includes('absent') ? 'mdi-check' : ''"
|
||||
>
|
||||
|
||||
<v-chip value="absent" :color="attendanceFilter.includes('absent') ? 'error' : ''"
|
||||
:variant="attendanceFilter.includes('absent') ? 'elevated' : 'tonal'" class="px-2 filter-chip"
|
||||
@click="toggleFilter('absent')" prepend-icon="mdi-account-off"
|
||||
:append-icon="attendanceFilter.includes('absent') ? 'mdi-check' : ''">
|
||||
请假
|
||||
</v-chip>
|
||||
<v-chip
|
||||
value="late"
|
||||
:color="attendanceFilter.includes('late') ? 'warning' : ''"
|
||||
:variant="attendanceFilter.includes('late') ? 'elevated' : 'tonal'"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('late')"
|
||||
prepend-icon="mdi-clock-alert"
|
||||
:append-icon="attendanceFilter.includes('late') ? 'mdi-check' : ''"
|
||||
>
|
||||
<v-chip value="late" :color="attendanceFilter.includes('late') ? 'warning' : ''"
|
||||
:variant="attendanceFilter.includes('late') ? 'elevated' : 'tonal'" class="px-2 filter-chip"
|
||||
@click="toggleFilter('late')" prepend-icon="mdi-clock-alert"
|
||||
:append-icon="attendanceFilter.includes('late') ? 'mdi-check' : ''">
|
||||
迟到
|
||||
</v-chip>
|
||||
<v-chip
|
||||
value="exclude"
|
||||
:color="attendanceFilter.includes('exclude') ? 'grey' : ''"
|
||||
:variant="attendanceFilter.includes('exclude') ? 'elevated' : 'tonal'"
|
||||
class="px-2 filter-chip"
|
||||
@click="toggleFilter('exclude')"
|
||||
prepend-icon="mdi-account-cancel"
|
||||
:append-icon="attendanceFilter.includes('exclude') ? 'mdi-check' : ''"
|
||||
>
|
||||
<v-chip value="exclude" :color="attendanceFilter.includes('exclude') ? 'grey' : ''"
|
||||
:variant="attendanceFilter.includes('exclude') ? 'elevated' : 'tonal'" class="px-2 filter-chip"
|
||||
@click="toggleFilter('exclude')" prepend-icon="mdi-account-cancel"
|
||||
:append-icon="attendanceFilter.includes('exclude') ? 'mdi-check' : ''">
|
||||
不参与
|
||||
</v-chip>
|
||||
</div>
|
||||
@ -303,15 +248,8 @@
|
||||
|
||||
<!-- 学生列表 -->
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="student in filteredStudents"
|
||||
:key="student"
|
||||
cols="12" sm="6" md="6" lg="4"
|
||||
>
|
||||
<v-card
|
||||
class="student-card"
|
||||
border
|
||||
>
|
||||
<v-col v-for="student in filteredStudents" :key="student" cols="12" sm="6" md="6" lg="4">
|
||||
<v-card class="student-card" border>
|
||||
<v-card-text class="d-flex align-center pa-2">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-center">
|
||||
@ -322,38 +260,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="attendance-actions">
|
||||
<v-btn
|
||||
:color="isPresent(state.studentList.indexOf(student)) ? 'success' : ''"
|
||||
icon="mdi-account-check"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setPresent(state.studentList.indexOf(student))"
|
||||
:title="'设为到课'"
|
||||
/>
|
||||
<v-btn
|
||||
:color="isAbsent(state.studentList.indexOf(student)) ? 'error' : ''"
|
||||
icon="mdi-account-off"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setAbsent(state.studentList.indexOf(student))"
|
||||
:title="'设为请假'"
|
||||
/>
|
||||
<v-btn
|
||||
:color="isLate(state.studentList.indexOf(student)) ? 'warning' : ''"
|
||||
icon="mdi-clock-alert"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setLate(state.studentList.indexOf(student))"
|
||||
:title="'设为迟到'"
|
||||
/>
|
||||
<v-btn
|
||||
:color="isExclude(state.studentList.indexOf(student)) ? 'grey' : ''"
|
||||
icon="mdi-account-cancel"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="setExclude(state.studentList.indexOf(student))"
|
||||
:title="'设为不参与'"
|
||||
/>
|
||||
<v-btn :color="isPresent(state.studentList.indexOf(student)) ? 'success' : ''"
|
||||
icon="mdi-account-check" size="small" variant="text"
|
||||
@click="setPresent(state.studentList.indexOf(student))" :title="'设为到课'" />
|
||||
<v-btn :color="isAbsent(state.studentList.indexOf(student)) ? 'error' : ''" icon="mdi-account-off"
|
||||
size="small" variant="text" @click="setAbsent(state.studentList.indexOf(student))"
|
||||
:title="'设为请假'" />
|
||||
<v-btn :color="isLate(state.studentList.indexOf(student)) ? 'warning' : ''" icon="mdi-clock-alert"
|
||||
size="small" variant="text" @click="setLate(state.studentList.indexOf(student))" :title="'设为迟到'" />
|
||||
<v-btn :color="isExclude(state.studentList.indexOf(student)) ? 'grey' : ''" icon="mdi-account-cancel"
|
||||
size="small" variant="text" @click="setExclude(state.studentList.indexOf(student))"
|
||||
:title="'设为不参与'" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@ -369,7 +286,7 @@
|
||||
</v-btn>
|
||||
<v-btn color="error" prepend-icon="mdi-account-off" @click="setAllAbsent">
|
||||
全部请假
|
||||
</v-btn> </v-btn-group> <v-btn-group>
|
||||
</v-btn> </v-btn-group> <v-btn-group>
|
||||
|
||||
<v-btn color="warning" prepend-icon="mdi-clock-alert" @click="setAllLate">
|
||||
全部迟到
|
||||
@ -387,9 +304,7 @@
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="state.attendanceDialog = false">
|
||||
取消
|
||||
</v-btn>
|
||||
|
||||
<v-btn color="primary" @click="saveAttendance">
|
||||
<v-icon start>mdi-content-save</v-icon>
|
||||
保存
|
||||
@ -417,11 +332,14 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 添加随机点名组件 -->
|
||||
<random-picker ref="randomPicker" :student-list="state.studentList" :attendance="state.boardData.attendance" />
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageLog from "@/components/MessageLog.vue";
|
||||
import RandomPicker from "@/components/RandomPicker.vue"; // 导入随机点名组件
|
||||
import dataProvider from "@/utils/dataProvider";
|
||||
import { getSetting, watchSettings, setSetting } from "@/utils/settings";
|
||||
import { useDisplay } from "vuetify";
|
||||
@ -429,11 +347,13 @@ import "../styles/index.scss";
|
||||
import "../styles/transitions.scss"; // 添加新的样式导入
|
||||
import { debounce, throttle } from "@/utils/debounce";
|
||||
import '../styles/global.scss';
|
||||
import { pinyin } from "pinyin-pro";
|
||||
|
||||
export default {
|
||||
name: "Classworks 作业板",
|
||||
components: {
|
||||
MessageLog,
|
||||
RandomPicker, // 注册随机点名组件
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -515,7 +435,6 @@ export default {
|
||||
},
|
||||
attendanceSearch: "",
|
||||
attendanceFilter: [],
|
||||
searchSuggestions: [],
|
||||
};
|
||||
},
|
||||
|
||||
@ -621,8 +540,8 @@ export default {
|
||||
unreadCount() {
|
||||
return this.$refs.messageLog?.unreadCount || 0;
|
||||
},
|
||||
showRandomButton() {
|
||||
return getSetting("display.showRandomButton");
|
||||
showRandomPickerButton() {
|
||||
return getSetting("randomPicker.enabled");
|
||||
},
|
||||
confirmNonTodaySave() {
|
||||
return getSetting("edit.confirmNonTodaySave");
|
||||
@ -641,15 +560,15 @@ export default {
|
||||
},
|
||||
filteredStudents() {
|
||||
let students = [...this.state.studentList];
|
||||
|
||||
|
||||
// 应用搜索过滤
|
||||
if (this.attendanceSearch) {
|
||||
const searchTerm = this.attendanceSearch.toLowerCase();
|
||||
students = students.filter(student =>
|
||||
students = students.filter(student =>
|
||||
student.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 应用状态过滤
|
||||
if (this.attendanceFilter && this.attendanceFilter.length > 0) {
|
||||
students = students.filter(student => {
|
||||
@ -661,7 +580,7 @@ export default {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return students;
|
||||
},
|
||||
extractedSurnames() {
|
||||
@ -669,9 +588,9 @@ export default {
|
||||
if (!this.state.studentList || this.state.studentList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const surnameMap = new Map();
|
||||
|
||||
|
||||
this.state.studentList.forEach(student => {
|
||||
if (student && student.length > 0) {
|
||||
// 中文姓名通常姓在前,取第一个字作为姓氏
|
||||
@ -683,11 +602,15 @@ export default {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 转换为数组并按数量排序
|
||||
|
||||
// 转换为数组并按拼音排序
|
||||
return Array.from(surnameMap.entries())
|
||||
.map(([name, count]) => ({ name, count }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
.sort((a, b) => {
|
||||
const pinyinA = pinyin(a.name, { toneType: "none", mode: 'surname' });
|
||||
const pinyinB = pinyin(b.name, { toneType: "none", mode: 'surname' });
|
||||
return pinyinA.localeCompare(pinyinB);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@ -735,6 +658,12 @@ export default {
|
||||
document.addEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
|
||||
document.addEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
|
||||
document.addEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
|
||||
|
||||
// 检查URL哈希值,如果包含#pick则自动打开随机点名
|
||||
this.checkHashForRandomPicker();
|
||||
|
||||
// 添加哈希变化监听器
|
||||
window.addEventListener('hashchange', this.checkHashForRandomPicker);
|
||||
} catch (err) {
|
||||
console.error("初始化失败:", err);
|
||||
this.showError("初始化失败,请刷新页面重试");
|
||||
@ -755,6 +684,9 @@ export default {
|
||||
document.removeEventListener('webkitfullscreenchange', this.fullscreenChangeHandler);
|
||||
document.removeEventListener('mozfullscreenchange', this.fullscreenChangeHandler);
|
||||
document.removeEventListener('MSFullscreenChange', this.fullscreenChangeHandler);
|
||||
|
||||
// 移除哈希变化监听器
|
||||
window.removeEventListener('hashchange', this.checkHashForRandomPicker);
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -1065,11 +997,38 @@ export default {
|
||||
}
|
||||
if (autoRefresh) {
|
||||
this.state.refreshInterval = setInterval(() => {
|
||||
this.downloadData();
|
||||
// 检查是否应该跳过刷新
|
||||
if (!this.shouldSkipRefresh()) {
|
||||
this.downloadData();
|
||||
}
|
||||
}, interval * 1000);
|
||||
}
|
||||
},
|
||||
|
||||
// 新增方法:检查是否应该跳过自动刷新
|
||||
shouldSkipRefresh() {
|
||||
// 如果对话框打开,跳过刷新
|
||||
if (this.state.dialogVisible) return true;
|
||||
|
||||
// 如果出勤对话框打开,跳过刷新
|
||||
if (this.state.attendanceDialog) return true;
|
||||
|
||||
// 如果确认对话框打开,跳过刷新
|
||||
if (this.confirmDialog.show) return true;
|
||||
|
||||
// 如果日期选择器打开,跳过刷新
|
||||
if (this.state.datePickerDialog) return true;
|
||||
|
||||
// 如果正在上传或下载数据,跳过刷新
|
||||
if (this.loading.upload || this.loading.download) return true;
|
||||
|
||||
// 如果数据未同步(有未保存的更改),跳过刷新
|
||||
if (!this.state.synced) return true;
|
||||
|
||||
// 没有特殊状态,可以刷新
|
||||
return false;
|
||||
},
|
||||
|
||||
updateSettings() {
|
||||
this.state.fontSize = getSetting("font.size");
|
||||
this.state.contentStyle = { "font-size": `${this.state.fontSize}px` };
|
||||
@ -1200,7 +1159,7 @@ export default {
|
||||
this.state.boardData.attendance.late = [];
|
||||
this.state.boardData.attendance.exclude = [...this.state.studentList];
|
||||
this.state.synced = false;
|
||||
},
|
||||
},
|
||||
|
||||
isPresent(index) {
|
||||
const student = this.state.studentList[index];
|
||||
@ -1407,17 +1366,17 @@ export default {
|
||||
if (this.state.boardData.attendance.exclude.includes(studentName)) return 'grey';
|
||||
return 'success';
|
||||
},
|
||||
|
||||
|
||||
getStudentStatusVariant(index) {
|
||||
const studentName = this.state.studentList[index];
|
||||
if (this.state.boardData.attendance.absent.includes(studentName) ||
|
||||
this.state.boardData.attendance.late.includes(studentName) ||
|
||||
this.state.boardData.attendance.exclude.includes(studentName)) {
|
||||
this.state.boardData.attendance.late.includes(studentName) ||
|
||||
this.state.boardData.attendance.exclude.includes(studentName)) {
|
||||
return 'tonal';
|
||||
}
|
||||
return 'outlined';
|
||||
},
|
||||
|
||||
|
||||
getStudentStatusIcon(index) {
|
||||
const studentName = this.state.studentList[index];
|
||||
if (this.state.boardData.attendance.absent.includes(studentName)) return 'mdi-account-off';
|
||||
@ -1425,7 +1384,7 @@ export default {
|
||||
if (this.state.boardData.attendance.exclude.includes(studentName)) return 'mdi-account-cancel';
|
||||
return 'mdi-account-check';
|
||||
},
|
||||
|
||||
|
||||
getStudentStatusText(index) {
|
||||
const studentName = this.state.studentList[index];
|
||||
if (this.state.boardData.attendance.absent.includes(studentName)) return '请假';
|
||||
@ -1434,49 +1393,6 @@ export default {
|
||||
return '到课';
|
||||
},
|
||||
|
||||
handleSearchChange(value) {
|
||||
// 生成搜索建议
|
||||
this.generateSearchSuggestions(value);
|
||||
},
|
||||
|
||||
generateSearchSuggestions(searchTerm) {
|
||||
if (!searchTerm || searchTerm.length < 1) {
|
||||
this.searchSuggestions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找匹配的学生名字
|
||||
const matchingStudents = this.state.studentList.filter(student =>
|
||||
student.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// 如果有精确匹配,不显示建议
|
||||
if (matchingStudents.some(student => student === searchTerm)) {
|
||||
this.searchSuggestions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取匹配的部分作为建议
|
||||
const suggestions = new Set();
|
||||
|
||||
// 如果是单个字符,可能是在搜索姓氏
|
||||
if (searchTerm.length === 1) {
|
||||
// 添加所有以该字符开头的学生的完整姓名作为建议
|
||||
matchingStudents.forEach(student => {
|
||||
if (student.startsWith(searchTerm)) {
|
||||
suggestions.add(student);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 对于多字符搜索,添加包含该字符串的学生名字
|
||||
matchingStudents.forEach(student => {
|
||||
suggestions.add(student);
|
||||
});
|
||||
}
|
||||
|
||||
// 限制建议数量
|
||||
this.searchSuggestions = Array.from(suggestions).slice(0, 5);
|
||||
},
|
||||
|
||||
toggleFilter(filter) {
|
||||
const index = this.attendanceFilter.indexOf(filter);
|
||||
@ -1486,136 +1402,22 @@ export default {
|
||||
this.attendanceFilter.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
openRandomPicker() {
|
||||
if (this.$refs.randomPicker) {
|
||||
this.$refs.randomPicker.open();
|
||||
}
|
||||
},
|
||||
|
||||
checkHashForRandomPicker() {
|
||||
if (window.location.hash === '#random-picker') {
|
||||
this.$nextTick(() => {
|
||||
console.log("打开随机点名");
|
||||
window.location.hash = '';
|
||||
this.openRandomPicker();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
// 添加卡片发光效果
|
||||
.glow-track {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
rgba(255, 255, 255, 0.15) 0%,
|
||||
rgba(255, 255, 255, 0) 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加卡片悬浮效果
|
||||
.grid-item .v-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加空科目卡片样式
|
||||
.empty-subject-card {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 修改防烧屏提示卡片,使用 tonal 样式减少信息密度
|
||||
.anti-burn-card {
|
||||
animation: subtle-glow 4s infinite alternate;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 5px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 15px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// 出勤管理对话框样式
|
||||
.attendance-stat {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 姓氏筛选按钮
|
||||
.surname-btn {
|
||||
margin: 2px;
|
||||
min-width: 0;
|
||||
padding: 0 8px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索建议按钮
|
||||
.suggestion-btn {
|
||||
margin: 2px;
|
||||
min-width: 0;
|
||||
padding: 0 6px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 适配触摸屏
|
||||
@media (hover: none) {
|
||||
.student-card .attendance-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 600px) {
|
||||
.student-card {
|
||||
.attendance-actions .v-btn {
|
||||
margin: 0 1px;
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤器芯片
|
||||
.filter-chip {
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<server-settings-card
|
||||
border
|
||||
border
|
||||
:loading="loading.server"
|
||||
@saved="onSettingsSaved"
|
||||
/>
|
||||
@ -37,16 +37,14 @@
|
||||
<display-settings-card @saved="onSettingsSaved" border/>
|
||||
</v-col>
|
||||
|
||||
<!-- 添加主题设置卡片 -->
|
||||
<v-col cols="12" md="6">
|
||||
<theme-settings-card border />
|
||||
</v-col>
|
||||
|
||||
|
||||
<!-- 开发者选项卡片 -->
|
||||
<v-col :cols="12" :md="settings.developer.enabled ? 12 : 6">
|
||||
<settings-card
|
||||
border
|
||||
border
|
||||
title="开发者选项"
|
||||
icon="mdi-developer-board"
|
||||
>
|
||||
@ -85,8 +83,6 @@
|
||||
</template>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider class="my-2" />
|
||||
|
||||
<v-expand-transition>
|
||||
<div v-if="settings.developer.showDebugConfig">
|
||||
<v-divider class="my-2" />
|
||||
@ -138,12 +134,34 @@
|
||||
<v-col cols="12">
|
||||
<echo-chamber-card border />
|
||||
</v-col>
|
||||
|
||||
<!-- 关于卡片 -->
|
||||
<v-col cols="12">
|
||||
<about-card />
|
||||
</v-col>
|
||||
|
||||
<!-- 开发者模式下显示所有设置 -->
|
||||
<v-col v-if="settings.developer.enabled" cols="12">
|
||||
<v-card border>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon icon="mdi-cog-outline" class="mr-2" />
|
||||
所有设置
|
||||
</v-card-title>
|
||||
<v-card-subtitle>
|
||||
浏览和修改所有可用设置
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<settings-explorer @update="onSettingUpdate" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<random-picker-settings-card />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- 消息记录组件 -->
|
||||
<message-log ref="messageLog" />
|
||||
</div>
|
||||
@ -170,6 +188,8 @@ import StudentListCard from '@/components/settings/StudentListCard.vue';
|
||||
import AboutCard from '@/components/settings/AboutCard.vue';
|
||||
import '../styles/settings.scss';
|
||||
import dataProvider from '@/utils/dataProvider';
|
||||
import SettingsExplorer from '@/components/settings/SettingsExplorer.vue';
|
||||
import RandomPickerSettingsCard from '@/components/settings/cards/RandomPickerSettingsCard.vue';
|
||||
|
||||
export default {
|
||||
name: 'Settings',
|
||||
@ -184,7 +204,9 @@ export default {
|
||||
AboutCard,
|
||||
DataProviderSettingsCard,
|
||||
ThemeSettingsCard,
|
||||
EchoChamberCard
|
||||
EchoChamberCard,
|
||||
SettingsExplorer,
|
||||
RandomPickerSettingsCard
|
||||
},
|
||||
setup() {
|
||||
const { mobile } = useDisplay();
|
||||
@ -565,6 +587,11 @@ export default {
|
||||
onSettingsSaved() {
|
||||
this.showMessage('设置已更新', '您的设置已成功保存');
|
||||
// 如果需要,可以在这里重新加载相关数据
|
||||
},
|
||||
|
||||
onSettingUpdate(key, value) {
|
||||
// 处理设置更新
|
||||
this.showMessage('设置已更新', `${key} 已保存为 ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,124 @@
|
||||
|
||||
|
||||
|
||||
// 添加卡片发光效果
|
||||
.glow-track {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
rgba(255, 255, 255, 0.15) 0%,
|
||||
rgba(255, 255, 255, 0) 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加卡片悬浮效果
|
||||
.grid-item .v-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加空科目卡片样式
|
||||
.empty-subject-card {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 修改防烧屏提示卡片,使用 tonal 样式减少信息密度
|
||||
.anti-burn-card {
|
||||
animation: subtle-glow 4s infinite alternate;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 5px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 15px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// 出勤管理对话框样式
|
||||
.attendance-stat {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
// 搜索建议按钮
|
||||
.suggestion-btn {
|
||||
margin: 2px;
|
||||
min-width: 0;
|
||||
padding: 0 6px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 适配触摸屏
|
||||
@media (hover: none) {
|
||||
.student-card .attendance-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 600px) {
|
||||
.student-card {
|
||||
.attendance-actions .v-btn {
|
||||
margin: 0 1px;
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤器芯片
|
||||
.filter-chip {
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
.grid-masonry {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@ -173,4 +293,124 @@
|
||||
.empty-subject-card:not(:disabled):hover {
|
||||
opacity: 1;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 添加卡片发光效果
|
||||
.glow-track {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
|
||||
rgba(255, 255, 255, 0.15) 0%,
|
||||
rgba(255, 255, 255, 0) 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加卡片悬浮效果
|
||||
.grid-item .v-card {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加空科目卡片样式
|
||||
.empty-subject-card {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 修改防烧屏提示卡片,使用 tonal 样式减少信息密度
|
||||
.anti-burn-card {
|
||||
animation: subtle-glow 4s infinite alternate;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtle-glow {
|
||||
0% {
|
||||
box-shadow: 0 0 5px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 15px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// 出勤管理对话框样式
|
||||
.attendance-stat {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
// 搜索建议按钮
|
||||
.suggestion-btn {
|
||||
margin: 2px;
|
||||
min-width: 0;
|
||||
padding: 0 6px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 适配触摸屏
|
||||
@media (hover: none) {
|
||||
.student-card .attendance-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 600px) {
|
||||
.student-card {
|
||||
.attendance-actions .v-btn {
|
||||
margin: 0 1px;
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤器芯片
|
||||
.filter-chip {
|
||||
cursor: pointer;
|
||||
margin: 2px;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { openDB } from 'idb';
|
||||
import { formatResponse, formatError } from '../dataProvider';
|
||||
import { defaultConfig, defaultHomework } from '../defaults/defaultData';
|
||||
|
||||
const DB_NAME = "HomeworkDB";
|
||||
const DB_NAME = "ClassworksDB";
|
||||
const DB_VERSION = 1;
|
||||
|
||||
const initDB = async () => {
|
||||
|
@ -20,10 +20,6 @@ async function requestNotificationPermission() {
|
||||
* @returns {Promise<boolean>} 是否成功启用持久性存储
|
||||
*/
|
||||
async function requestPersistentStorage() {
|
||||
if (!getSetting("storage.persistentStorage")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (navigator.storage?.persist) {
|
||||
return await navigator.storage.persist();
|
||||
@ -58,6 +54,7 @@ window.addEventListener("load", initializeStorage);
|
||||
* @property {string} [description] - 配置项描述
|
||||
* @property {string} [legacyKey] - 旧版本localStorage键名(用于迁移)
|
||||
* @property {boolean} [requireDeveloper] - 是否需要开发者选项启用
|
||||
* @property {string} [icon] - 设置项的图标
|
||||
*/
|
||||
|
||||
// 存储所有设置的localStorage键名
|
||||
@ -69,15 +66,11 @@ const SETTINGS_STORAGE_KEY = "Classworks_settings";
|
||||
*/
|
||||
const settingsDefinitions = {
|
||||
// 存储设置
|
||||
"storage.persistentStorage": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否启用持久性存储",
|
||||
},
|
||||
"storage.persistOnLoad": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否在页面加载时自动请求持久性存储",
|
||||
icon: "mdi-database-sync",
|
||||
},
|
||||
|
||||
// 显示设置
|
||||
@ -85,37 +78,48 @@ const settingsDefinitions = {
|
||||
type: "string",
|
||||
default: "card", // 修改默认值为 'button'
|
||||
validate: (value) => ["card", "button"].includes(value),
|
||||
description: "空科目的显示方式:卡片或按钮",
|
||||
description: "空科目的显示方式",
|
||||
icon: "mdi-card-outline",
|
||||
},
|
||||
"display.dynamicSort": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否启用动态排序以优化显示效果",
|
||||
description: "是否启用动态排序",
|
||||
icon: "mdi-sort-variant",
|
||||
// 启用后会根据内容自动调整卡片顺序,提供更好的视觉体验
|
||||
},
|
||||
"display.showRandomButton": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否显示随机按钮",
|
||||
description: "是否显示随机点人按钮",
|
||||
icon: "mdi-shuffle-variant",
|
||||
// 控制是否显示随机排序按钮,可用于随机调整卡片顺序
|
||||
},
|
||||
"display.showFullscreenButton": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否显示全屏按钮",
|
||||
icon: "mdi-fullscreen",
|
||||
// 控制是否显示进入全屏模式的按钮
|
||||
},
|
||||
"display.cardHoverEffect": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否启用卡片悬浮效果",
|
||||
icon: "mdi-gesture-tap",
|
||||
// 启用后鼠标悬停在卡片上时会显示视觉反馈效果
|
||||
},
|
||||
"display.enhancedTouchMode": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否启用增强触摸模式(更大的触摸目标)",
|
||||
description: "是否启用增强触摸模式",
|
||||
icon: "mdi-gesture-tap-button",
|
||||
},
|
||||
"display.showAntiScreenBurnCard": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否显示防烧屏提示卡片",
|
||||
description: "是否显示防烧屏忽悠卡片",
|
||||
icon: "mdi-monitor-shimmer",
|
||||
},
|
||||
|
||||
// 服务器设置(合并了数据提供者设置)
|
||||
@ -135,20 +139,25 @@ const settingsDefinitions = {
|
||||
}
|
||||
},
|
||||
description: "后端服务器域名",
|
||||
icon: "mdi-web",
|
||||
// 设置后端服务器的域名,用于从远程服务器获取数据
|
||||
},
|
||||
"server.classNumber": {
|
||||
type: "string",
|
||||
default: "高三八班",
|
||||
//validate: (value) => /^[A-Za-z0-9]*$/.test(value),
|
||||
validate: (value) => /.*/.test(value),
|
||||
|
||||
description: "班级编号",
|
||||
icon: "mdi-account-group",
|
||||
// 设置班级标识,用于区分不同班级的数据
|
||||
},
|
||||
"server.provider": {
|
||||
type: "string",
|
||||
default: "indexedDB",
|
||||
validate: (value) => ["server", "indexedDB"].includes(value),
|
||||
description: "数据提供者,用于决定数据存储方式",
|
||||
description: "数据提供者",
|
||||
icon: "mdi-database",
|
||||
// 选择数据存储方式:使用本地IndexedDB或远程服务器
|
||||
},
|
||||
|
||||
// 刷新设置
|
||||
@ -156,12 +165,16 @@ const settingsDefinitions = {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否启用自动刷新",
|
||||
icon: "mdi-refresh-auto",
|
||||
// 启用后将按设定的时间间隔自动刷新数据
|
||||
},
|
||||
"refresh.interval": {
|
||||
type: "number",
|
||||
default: 300,
|
||||
validate: (value) => value >= 10 && value <= 3600,
|
||||
description: "自动刷新间隔(秒)",
|
||||
icon: "mdi-timer-outline",
|
||||
// 设置自动刷新的时间间隔,范围10-3600秒
|
||||
},
|
||||
|
||||
// 字体设置
|
||||
@ -169,7 +182,8 @@ const settingsDefinitions = {
|
||||
type: "number",
|
||||
default: 28,
|
||||
validate: (value) => value >= 16 && value <= 100,
|
||||
description: "字体大小(像素)",
|
||||
description: "字体大小",
|
||||
icon: "mdi-format-size",
|
||||
},
|
||||
|
||||
// 编辑设置
|
||||
@ -177,23 +191,30 @@ const settingsDefinitions = {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否启用自动保存",
|
||||
icon: "mdi-content-save-outline",
|
||||
// 启用后编辑内容时会自动保存更改,无需手动点击保存按钮
|
||||
},
|
||||
"edit.blockNonTodayAutoSave": {
|
||||
// 添加新选项
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "禁止自动保存非当天数据",
|
||||
icon: "mdi-calendar-lock",
|
||||
// 启用后只有当天的数据会自动保存,防止意外修改历史数据
|
||||
},
|
||||
"edit.refreshBeforeEdit": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "编辑前是否自动刷新",
|
||||
icon: "mdi-refresh",
|
||||
// 启用后在开始编辑前会自动刷新数据,确保编辑的是最新内容
|
||||
},
|
||||
"edit.confirmNonTodaySave": {
|
||||
// 添加新选项
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "保存非当天数据时显示确认对话框,禁用则允许直接保存",
|
||||
description: "保存非当天数据需确认",
|
||||
icon: "mdi-calendar-alert",
|
||||
},
|
||||
|
||||
// 开发者选项
|
||||
@ -201,11 +222,15 @@ const settingsDefinitions = {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否启用开发者选项",
|
||||
icon: "mdi-developer-board",
|
||||
// 启用后可以访问高级开发者功能和设置项
|
||||
},
|
||||
"developer.showDebugConfig": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否显示调试配置",
|
||||
icon: "mdi-bug-outline",
|
||||
// 启用后在控制台显示详细的配置信息和设置变更日志
|
||||
},
|
||||
"developer.disableMessageLog": {
|
||||
// 添加新的设置项
|
||||
@ -213,6 +238,8 @@ const settingsDefinitions = {
|
||||
default: false,
|
||||
description: "禁用消息日志记录",
|
||||
requireDeveloper: true,
|
||||
icon: "mdi-message-off-outline",
|
||||
// 启用后将不再记录应用消息到日志,可减少内存占用
|
||||
},
|
||||
|
||||
// 消息设置
|
||||
@ -221,6 +248,8 @@ const settingsDefinitions = {
|
||||
default: true,
|
||||
description: "是否显示消息记录侧栏",
|
||||
requireDeveloper: true, // 添加标记
|
||||
icon: "mdi-message-text-outline",
|
||||
// 控制是否显示消息历史记录侧栏,需要开发者模式
|
||||
},
|
||||
"message.maxActiveMessages": {
|
||||
type: "number",
|
||||
@ -228,6 +257,8 @@ const settingsDefinitions = {
|
||||
validate: (value) => value >= 1 && value <= 10,
|
||||
description: "同时显示的最大消息数量",
|
||||
requireDeveloper: true,
|
||||
icon: "mdi-message-badge-outline",
|
||||
// 控制界面上同时显示的最大消息数量,范围1-10条
|
||||
},
|
||||
"message.timeout": {
|
||||
type: "number",
|
||||
@ -235,12 +266,16 @@ const settingsDefinitions = {
|
||||
validate: (value) => value >= 1000 && value <= 30000,
|
||||
description: "消息自动关闭时间(毫秒)",
|
||||
requireDeveloper: true,
|
||||
icon: "mdi-timer-sand",
|
||||
// 设置消息自动消失的时间,范围1000-30000毫秒
|
||||
},
|
||||
"message.saveHistory": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否保存消息历史记录",
|
||||
requireDeveloper: true,
|
||||
icon: "mdi-history",
|
||||
// 启用后将保存消息历史记录,可在侧栏中查看
|
||||
},
|
||||
|
||||
// 主题设置
|
||||
@ -249,6 +284,47 @@ const settingsDefinitions = {
|
||||
default: "dark",
|
||||
validate: (value) => ["light", "dark"].includes(value),
|
||||
description: "主题模式",
|
||||
icon: "mdi-theme-light-dark",
|
||||
// 设置应用的主题模式,可选亮色或暗色主题
|
||||
},
|
||||
|
||||
// 随机点名设置
|
||||
"randomPicker.enabled": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否启用随机点名功能",
|
||||
icon: "mdi-account-question",
|
||||
},
|
||||
"randomPicker.animation": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否启用随机点名动画效果",
|
||||
icon: "mdi-animation-play",
|
||||
},
|
||||
"randomPicker.defaultCount": {
|
||||
type: "number",
|
||||
default: 1,
|
||||
validate: (value) => value >= 1 && value <= 10,
|
||||
description: "默认抽取人数",
|
||||
icon: "mdi-counter",
|
||||
},
|
||||
"randomPicker.excludeAbsent": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否排除请假学生",
|
||||
icon: "mdi-account-off",
|
||||
},
|
||||
"randomPicker.excludeLate": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description: "是否排除迟到学生",
|
||||
icon: "mdi-clock-alert",
|
||||
},
|
||||
"randomPicker.excludeExcluded": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "是否排除不参与学生",
|
||||
icon: "mdi-account-cancel",
|
||||
},
|
||||
};
|
||||
|
||||
@ -488,6 +564,36 @@ function watchSettings(callback) {
|
||||
// 初始化设置
|
||||
loadSettings();
|
||||
|
||||
/**
|
||||
* 获取设置项的定义
|
||||
* @param {string} key - 设置项键名
|
||||
* @returns {SettingDefinition|null} 设置项的定义或null
|
||||
*/
|
||||
function getSettingDefinition(key) {
|
||||
return settingsDefinitions[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前配置导出为简单的键值对对象
|
||||
* @returns {Object} 包含所有设置的键值对对象
|
||||
*/
|
||||
function exportSettingsAsKeyValue() {
|
||||
if (!settingsCache) {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
// 创建一个新对象,避免直接返回引用
|
||||
const exportedSettings = {};
|
||||
|
||||
// 遍历所有设置项
|
||||
for (const key in settingsDefinitions) {
|
||||
// 获取当前值(确保使用getSetting以应用所有规则,如开发者选项依赖)
|
||||
exportedSettings[key] = getSetting(key);
|
||||
}
|
||||
|
||||
return exportedSettings;
|
||||
}
|
||||
|
||||
export {
|
||||
settingsDefinitions,
|
||||
getSetting,
|
||||
@ -495,4 +601,6 @@ export {
|
||||
resetSetting,
|
||||
resetAllSettings,
|
||||
watchSettings,
|
||||
getSettingDefinition,
|
||||
exportSettingsAsKeyValue
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ import Layouts from 'vite-plugin-vue-layouts'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// Utilities
|
||||
import { defineConfig } from 'vite'
|
||||
@ -18,6 +19,71 @@ export default defineConfig({
|
||||
Layouts(),
|
||||
Vue({
|
||||
template: { transformAssetUrls }
|
||||
}), VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
devOptions: {
|
||||
navigateFallback: '/index.html', // 离线支持(navigateFallback)
|
||||
enabled: true,
|
||||
suppressWarnings: true, // 是否抑制 Workbox 的警告
|
||||
},
|
||||
|
||||
lang: 'zh-CN',
|
||||
manifest: {
|
||||
name: 'Classworks作业板',
|
||||
short_name: 'Classworks',
|
||||
description: '记录,查看并同步作业',
|
||||
theme_color: '#212121',
|
||||
background_color: '#212121',
|
||||
display: 'standalone',
|
||||
start_url: '/',
|
||||
edge_side_panel: {
|
||||
default_path: '/',
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,png,svg}'],
|
||||
navigateFallback: '/index.html', // 离线支持(navigateFallback)
|
||||
runtimeCaching: [
|
||||
//所有资源都使用网络优先
|
||||
{
|
||||
urlPattern: /./,
|
||||
handler: 'NetworkFirst',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
icons: [
|
||||
{
|
||||
src: '/image/pwa-64x64.png',
|
||||
sizes: '64x64',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/image/pwa-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/image/pwa-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/image/maskable-icon-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable'
|
||||
}
|
||||
],
|
||||
|
||||
shortcuts: [
|
||||
{
|
||||
name: '随机点名',
|
||||
short_name: '随机点名',
|
||||
url: '/#random-picker',
|
||||
},
|
||||
],
|
||||
|
||||
}
|
||||
}),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||
Vuetify({
|
||||
|