1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2025-12-07 13:03:59 +00:00

规范代码格式

This commit is contained in:
Sunwuyuan 2025-11-16 15:14:17 +08:00
parent 0af2c4cc63
commit 76c2dba502
No known key found for this signature in database
GPG Key ID: A6A54CF66F56BB64
87 changed files with 3403 additions and 3247 deletions

View File

@ -3,8 +3,8 @@
<!-- 正常路由 --> <!-- 正常路由 -->
<router-view v-slot="{ Component, route }"> <router-view v-slot="{ Component, route }">
<transition <transition
name="md3"
mode="out-in" mode="out-in"
name="md3"
> >
<component <component
:is="Component" :is="Component"
@ -12,17 +12,18 @@
/> />
</transition> </transition>
</router-view> </router-view>
<global-message /> <global-message/>
<rate-limit-modal /> <rate-limit-modal/>
</v-app> </v-app>
</template> </template>
<script setup> <script setup>
import { onMounted } from "vue"; import {onMounted} from "vue";
import { useTheme } from "vuetify"; import {useTheme} from "vuetify";
import { getSetting } from "@/utils/settings"; import {getSetting} from "@/utils/settings";
import RateLimitModal from "@/components/RateLimitModal.vue"; import RateLimitModal from "@/components/RateLimitModal.vue";
import Clarity from "@microsoft/clarity"; import Clarity from "@microsoft/clarity";
const theme = useTheme(); const theme = useTheme();
onMounted(() => { onMounted(() => {
@ -44,7 +45,7 @@ onMounted(() => {
.md3-enter-active, .md3-enter-active,
.md3-leave-active { .md3-leave-active {
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.md3-enter-from { .md3-enter-from {

View File

@ -1,23 +1,25 @@
<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"> <svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="256" height="256"
<g clip-path="url(#clip-path-74_1)"> viewBox="0 0 256 256" fill="none">
<path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z"> <g clip-path="url(#clip-path-74_1)">
</path> <path fill="#FFFFFF" d="M0 256L256 256L256 0L0 0L0 256Z">
<rect x="0" y="0" width="256" height="128" fill="#D8C4A0" > </path>
</rect> <rect x="0" y="0" width="256" height="128" fill="#D8C4A0">
<rect x="0" y="128" width="256" height="128" fill="#F5E0BB" > </rect>
</rect> <rect x="0" y="128" width="256" height="128" fill="#F5E0BB">
<path d="M28 228L128 128L228 128L128 228L28 228Z" fill-rule="evenodd" fill="#241A04" > </rect>
</path> <path d="M28 228L128 128L228 128L128 228L28 228Z" fill-rule="evenodd" fill="#241A04">
<path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A" > </path>
</path> <path d="M28 128L128 28L228 28L128 128L28 128Z" fill-rule="evenodd" fill="#52452A">
<g > </path>
<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"> <g>
</path> <path fill="#000000"
</g> 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">
</g> </path>
<defs> </g>
<clipPath id="clip-path-74_1"> </g>
<path d="M0 256L256 256L256 0L0 0L0 256Z" fill="white"/> <defs>
</clipPath> <clipPath id="clip-path-74_1">
</defs> <path d="M0 256L256 256L256 0L0 0L0 256Z" fill="white"/>
</clipPath>
</defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,8 +1,8 @@
import axios from "axios"; import axios from "axios";
import { getSetting } from "@/utils/settings"; import {getSetting} from "@/utils/settings";
import { parseRateLimit } from "ratelimit-header-parser"; import {parseRateLimit} from "ratelimit-header-parser";
import RateLimitModal from "@/components/RateLimitModal.vue"; import RateLimitModal from "@/components/RateLimitModal.vue";
import { Base64 } from "js-base64"; import {Base64} from "js-base64";
// 基本配置 // 基本配置
const axiosInstance = axios.create({ const axiosInstance = axios.create({

View File

@ -1,11 +1,11 @@
<template> <template>
<v-app-bar :elevation="2"> <v-app-bar :elevation="2">
<template v-slot:prepend> <template v-slot:prepend>
<v-app-bar-nav-icon></v-app-bar-nav-icon> <v-app-bar-nav-icon></v-app-bar-nav-icon>
</template> </template>
<v-app-bar-title>作业</v-app-bar-title> <v-app-bar-title>作业</v-app-bar-title>
</v-app-bar> </v-app-bar>
</template> </template>
<script> <script>

View File

@ -3,16 +3,16 @@
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<span>缓存管理</span> <span>缓存管理</span>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="error" @click="clearAllCaches" :loading="loading"> <v-btn :loading="loading" color="error" @click="clearAllCaches">
清除所有缓存 清除所有缓存
</v-btn> </v-btn>
<v-btn icon class="ml-2" @click="refreshCaches"> <v-btn class="ml-2" icon @click="refreshCaches">
<v-icon>mdi-refresh</v-icon> <v-icon>mdi-refresh</v-icon>
</v-btn> </v-btn>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-alert v-if="!serviceWorkerActive" type="warning" class="mb-4"> <v-alert v-if="!serviceWorkerActive" class="mb-4" type="warning">
Service Worker 未激活缓存管理功能不可用 Service Worker 未激活缓存管理功能不可用
</v-alert> </v-alert>
@ -30,7 +30,7 @@
</v-expansion-panel-title> </v-expansion-panel-title>
<v-expansion-panel-text> <v-expansion-panel-text>
<div class="d-flex justify-end mb-2"> <div class="d-flex justify-end mb-2">
<v-btn color="error" size="small" @click="clearCache(cache.name)" :loading="loading"> <v-btn :loading="loading" color="error" size="small" @click="clearCache(cache.name)">
清除此缓存 清除此缓存
</v-btn> </v-btn>
</div> </div>
@ -43,7 +43,7 @@
{{ url }} {{ url }}
</v-list-item-subtitle> </v-list-item-subtitle>
<template v-slot:append> <template v-slot:append>
<v-btn icon size="small" color="error" @click="clearUrl(cache.name, url)"> <v-btn color="error" icon size="small" @click="clearUrl(cache.name, url)">
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
</v-btn> </v-btn>
</template> </template>
@ -53,7 +53,7 @@
</v-expansion-panel> </v-expansion-panel>
</v-expansion-panels> </v-expansion-panels>
<v-skeleton-loader v-else-if="loading" type="article" /> <v-skeleton-loader v-else-if="loading" type="article"/>
<v-alert v-else type="info"> <v-alert v-else type="info">
没有找到缓存数据 没有找到缓存数据
@ -99,7 +99,7 @@ export default {
try { try {
// //
const cacheNames = await this.sendMessageToSW({ type: 'CACHE_KEYS' }); const cacheNames = await this.sendMessageToSW({type: 'CACHE_KEYS'});
// //
for (const cacheName of cacheNames.cacheNames) { for (const cacheName of cacheNames.cacheNames) {
@ -167,7 +167,7 @@ export default {
this.loading = true; this.loading = true;
try { try {
const result = await this.sendMessageToSW({ type: 'CLEAR_ALL_CACHES' }); const result = await this.sendMessageToSW({type: 'CLEAR_ALL_CACHES'});
if (result.success) { if (result.success) {
this.showMessage('已清除所有缓存', 'success'); this.showMessage('已清除所有缓存', 'success');

View File

@ -2,12 +2,12 @@
<!-- Floating toggle button --> <!-- Floating toggle button -->
<div <div
v-if="showToggleButton" v-if="showToggleButton"
class="chat-toggle"
:style="toggleStyle" :style="toggleStyle"
class="chat-toggle"
> >
<v-btn <v-btn
icon
color="primary" color="primary"
icon
variant="flat" variant="flat"
@click="open()" @click="open()"
> >
@ -27,26 +27,26 @@
<!-- Chat panel --> <!-- Chat panel -->
<div <div
v-show="visible" v-show="visible"
class="chat-panel"
:style="panelStyle" :style="panelStyle"
class="chat-panel"
> >
<v-card <v-card
border border
elevation="8"
class="chat-card" class="chat-card"
elevation="8"
> >
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon class="mr-2"> <v-icon class="mr-2">
mdi-chat-processing mdi-chat-processing
</v-icon> </v-icon>
<span class="text-subtitle-1">设备聊天室</span> <span class="text-subtitle-1">设备聊天室</span>
<v-spacer /> <v-spacer/>
<v-tooltip location="top"> <v-tooltip location="top">
<template #activator="{ props }"> <template #activator="{ props }">
<v-chip <v-chip
v-bind="props"
size="x-small"
:color="connected ? 'success' : 'grey'" :color="connected ? 'success' : 'grey'"
size="x-small"
v-bind="props"
variant="tonal" variant="tonal"
> >
{{ connected ? '已连接' : '未连接' }} {{ connected ? '已连接' : '未连接' }}
@ -63,7 +63,7 @@
</v-btn> </v-btn>
</v-card-title> </v-card-title>
<v-divider /> <v-divider/>
<v-card-text class="chat-body"> <v-card-text class="chat-body">
<div <div
@ -78,21 +78,21 @@
v-if="msg._type === 'divider'" v-if="msg._type === 'divider'"
class="divider-row" class="divider-row"
> >
<v-divider class="my-2" /> <v-divider class="my-2"/>
<div class="divider-text"> <div class="divider-text">
今天 - 上次访问 今天 - 上次访问
</div> </div>
<v-divider class="my-2" /> <v-divider class="my-2"/>
</div> </div>
<div <div
v-else v-else
class="message-row"
:class="{ self: msg.self }" :class="{ self: msg.self }"
class="message-row"
> >
<div class="avatar"> <div class="avatar">
<v-avatar <v-avatar
size="24"
:color="msg.self ? 'primary' : 'grey'" :color="msg.self ? 'primary' : 'grey'"
size="24"
> >
<v-icon size="small"> <v-icon size="small">
{{ msg.self ? 'mdi-account' : 'mdi-account-outline' }} {{ msg.self ? 'mdi-account' : 'mdi-account-outline' }}
@ -112,13 +112,13 @@
</div> </div>
</v-card-text> </v-card-text>
<v-divider /> <v-divider/>
<v-card-actions class="chat-input"> <v-card-actions class="chat-input">
<v-btn <v-btn
class="mr-1"
icon icon
variant="text" variant="text"
class="mr-1"
@click="insertEmoji('😄')" @click="insertEmoji('😄')"
> >
<v-icon>mdi-emoticon-outline</v-icon> <v-icon>mdi-emoticon-outline</v-icon>
@ -126,19 +126,19 @@
<v-textarea <v-textarea
ref="inputRef" ref="inputRef"
v-model="text" v-model="text"
class="flex-grow-1"
rows="1"
auto-grow auto-grow
variant="solo" class="flex-grow-1"
hide-details hide-details
placeholder="输入消息" placeholder="输入消息"
rows="1"
variant="solo"
@keydown.enter.prevent="handleEnter" @keydown.enter.prevent="handleEnter"
@keydown.shift.enter.stop @keydown.shift.enter.stop
/> />
<v-btn <v-btn
color="primary"
:disabled="!canSend" :disabled="!canSend"
class="ml-2" class="ml-2"
color="primary"
@click="send" @click="send"
> >
<v-icon start> <v-icon start>
@ -152,8 +152,8 @@
</template> </template>
<script> <script>
import { getSetting } from '@/utils/settings' import {getSetting} from '@/utils/settings'
import { getSocket, joinToken, on as socketOn } from '@/utils/socketClient' import {getSocket, joinToken, on as socketOn} from '@/utils/socketClient'
export default { export default {
name: 'ChatWidget', name: 'ChatWidget',
@ -222,7 +222,7 @@ export default {
const after = this.messages.slice(idx) const after = this.messages.slice(idx)
return [ return [
...before, ...before,
{ _id: 'divider', _type: 'divider' }, {_id: 'divider', _type: 'divider'},
...after, ...after,
] ]
}, },
@ -239,7 +239,9 @@ export default {
try { try {
const stored = localStorage.getItem('chat.lastVisit') const stored = localStorage.getItem('chat.lastVisit')
if (stored) this.lastVisit = stored if (stored) this.lastVisit = stored
} catch (e) { void e } } catch (e) {
void e
}
// Prepare socket // Prepare socket
const s = getSocket() const s = getSocket()
@ -280,7 +282,9 @@ export default {
this.$emit('update:modelValue', false) this.$emit('update:modelValue', false)
try { try {
localStorage.setItem('chat.lastVisit', new Date().toISOString()) localStorage.setItem('chat.lastVisit', new Date().toISOString())
} catch (e) { void e } } catch (e) {
void e
}
this.unreadCount = 0 this.unreadCount = 0
}, },
onOpen() { onOpen() {
@ -340,7 +344,9 @@ export default {
if (!el) return if (!el) return
try { try {
el.scrollTop = el.scrollHeight el.scrollTop = el.scrollHeight
} catch (e) { void e } } catch (e) {
void e
}
}, },
}, },
} }
@ -351,46 +357,80 @@ export default {
position: fixed; position: fixed;
z-index: 1100; z-index: 1100;
} }
.chat-panel { .chat-panel {
position: fixed; position: fixed;
z-index: 1101; z-index: 1101;
} }
.chat-card { .chat-card {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.chat-body { .chat-body {
padding: 8px 12px; padding: 8px 12px;
height: calc(100% - 120px); height: calc(100% - 120px);
} }
.messages { .messages {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
.message-row { .message-row {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
margin: 8px 0; margin: 8px 0;
} }
.message-row.self { .message-row.self {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.message-row .avatar { width: 28px; display: flex; justify-content: center; }
.message-row .avatar {
width: 28px;
display: flex;
justify-content: center;
}
.message-row .bubble { .message-row .bubble {
max-width: 70%; max-width: 70%;
background: rgba(255,255,255,0.06); background: rgba(255, 255, 255, 0.06);
border-radius: 10px; border-radius: 10px;
padding: 6px 10px; padding: 6px 10px;
margin: 0 8px; margin: 0 8px;
} }
.message-row.self .bubble { .message-row.self .bubble {
background: rgba(33,150,243,0.15); background: rgba(33, 150, 243, 0.15);
}
.bubble .text {
white-space: pre-wrap;
word-break: break-word;
}
.bubble .meta {
font-size: 12px;
opacity: 0.6;
margin-top: 2px;
text-align: right;
}
.divider-row {
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
}
.divider-text {
margin: 4px 0;
}
.chat-input {
padding: 8px;
} }
.bubble .text { white-space: pre-wrap; word-break: break-word; }
.bubble .meta { font-size: 12px; opacity: 0.6; margin-top: 2px; text-align: right; }
.divider-row { text-align: center; color: rgba(255,255,255,0.6); font-size: 12px; }
.divider-text { margin: 4px 0; }
.chat-input { padding: 8px; }
</style> </style>

View File

@ -3,11 +3,11 @@
<!-- 错误提示 --> <!-- 错误提示 -->
<v-alert <v-alert
v-if="error" v-if="error"
type="error"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start" border="start"
class="mb-4 mt-3 mx-2"
closable closable
type="error"
variant="tonal"
@click:close="error = ''" @click:close="error = ''"
> >
<div class="d-flex align-center"> <div class="d-flex align-center">
@ -19,11 +19,11 @@
<!-- 成功提示 --> <!-- 成功提示 -->
<v-alert <v-alert
v-if="success" v-if="success"
type="success"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start" border="start"
class="mb-4 mt-3 mx-2"
closable closable
type="success"
variant="tonal"
@click:close="success = ''" @click:close="success = ''"
> >
<div class="d-flex align-center"> <div class="d-flex align-center">
@ -35,22 +35,22 @@
<!-- 验证错误提示 --> <!-- 验证错误提示 -->
<v-alert <v-alert
v-if="hasValidationErrors && !loading" v-if="hasValidationErrors && !loading"
type="warning"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start" border="start"
class="mb-4 mt-3 mx-2"
type="warning"
variant="tonal"
> >
<div class="d-flex align-center"> <div class="d-flex align-center">
<span class="font-weight-bold">配置验证失败请检查以下问题</span> <span class="font-weight-bold">配置验证失败请检查以下问题</span>
</div> </div>
<v-list density="compact" class="bg-transparent"> <v-list class="bg-transparent" density="compact">
<v-list-item <v-list-item
v-for="(error, index) in validationErrors" v-for="(error, index) in validationErrors"
:key="index" :key="index"
class="px-0 py-0" class="px-0 py-0"
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-icon size="small" color="warning">mdi-circle-small</v-icon> <v-icon color="warning" size="small">mdi-circle-small</v-icon>
</template> </template>
<v-list-item-title class="text-body-2">{{ error }}</v-list-item-title> <v-list-item-title class="text-body-2">{{ error }}</v-list-item-title>
</v-list-item> </v-list-item>
@ -60,7 +60,7 @@
<!-- 加载状态 --> <!-- 加载状态 -->
<v-card v-if="loading" class="my-4" outlined> <v-card v-if="loading" class="my-4" outlined>
<v-card-text> <v-card-text>
<v-skeleton-loader type="article" class="mx-auto"></v-skeleton-loader> <v-skeleton-loader class="mx-auto" type="article"></v-skeleton-loader>
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -68,12 +68,12 @@
<div v-if="!loading" class="d-flex justify-space-between align-center mb-4"> <div v-if="!loading" class="d-flex justify-space-between align-center mb-4">
<div class="d-flex align-center gap-2"> <div class="d-flex align-center gap-2">
<v-btn <v-btn
color="success"
variant="elevated"
prepend-icon="mdi-open-in-new"
@click="openConfig"
class="mr-2 text-none"
:disabled="!isValidConfig" :disabled="!isValidConfig"
class="mr-2 text-none"
color="success"
prepend-icon="mdi-open-in-new"
variant="elevated"
@click="openConfig"
> >
打开 ExamSchedule 打开 ExamSchedule
</v-btn> </v-btn>
@ -89,9 +89,10 @@
<v-btn-toggle <v-btn-toggle
v-model="isEditMode" v-model="isEditMode"
color="primary" color="primary"
variant="outlined"
divided divided
> <v-btn variant="outlined"
>
<v-btn
class="text-error" class="text-error"
prepend-icon="mdi-delete" prepend-icon="mdi-delete"
@click="confirmDelete" @click="confirmDelete"
@ -99,8 +100,8 @@
> >
删除配置 删除配置
</v-btn> </v-btn>
<v-btn :value="false" prepend-icon="mdi-eye"> 预览 </v-btn> <v-btn :value="false" prepend-icon="mdi-eye"> 预览</v-btn>
<v-btn :value="true" prepend-icon="mdi-pencil"> 编辑 </v-btn> <v-btn :value="true" prepend-icon="mdi-pencil"> 编辑</v-btn>
</v-btn-toggle> </v-btn-toggle>
</div> </div>
@ -116,7 +117,7 @@
> >
{{ localConfig.message || "未设置考试提示" }} {{ localConfig.message || "未设置考试提示" }}
</div> </div>
<v-chip v-if="localConfig.room" size="large" class="px-4 py-2"> <v-chip v-if="localConfig.room" class="px-4 py-2" size="large">
<v-icon start>mdi-home</v-icon> <v-icon start>mdi-home</v-icon>
考场{{ localConfig.room }} 考场{{ localConfig.room }}
</v-chip> </v-chip>
@ -130,10 +131,10 @@
v-for="(examInfo, index) in localConfig.examInfos" v-for="(examInfo, index) in localConfig.examInfos"
:key="index" :key="index"
cols="12" cols="12"
md="6"
lg="4" lg="4"
md="6"
> >
<v-card variant="tonal" class="h-100" hover> <v-card class="h-100" hover variant="tonal">
<v-card-title class="bg-primary-lighten-5 pa-4"> <v-card-title class="bg-primary-lighten-5 pa-4">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon class="mr-2">mdi-book-open-page-variant</v-icon> <v-icon class="mr-2">mdi-book-open-page-variant</v-icon>
@ -143,8 +144,9 @@
<v-card-text class="pa-4"> <v-card-text class="pa-4">
<div class="mb-3"> <div class="mb-3">
<div class="d-flex align-center mb-1"> <div class="d-flex align-center mb-1">
<v-icon size="small" class="mr-2" color="success" <v-icon class="mr-2" color="success" size="small"
>mdi-clock-start</v-icon >mdi-clock-start
</v-icon
> >
<span class="text-body-2 text-grey-darken-1">开始时间</span> <span class="text-body-2 text-grey-darken-1">开始时间</span>
</div> </div>
@ -154,8 +156,9 @@
</div> </div>
<div> <div>
<div class="d-flex align-center mb-1"> <div class="d-flex align-center mb-1">
<v-icon size="small" class="mr-2" color="error" <v-icon class="mr-2" color="error" size="small"
>mdi-clock-end</v-icon >mdi-clock-end
</v-icon
> >
<span class="text-body-2 text-grey-darken-1">结束时间</span> <span class="text-body-2 text-grey-darken-1">结束时间</span>
</div> </div>
@ -169,7 +172,7 @@
</v-row> </v-row>
</div> </div>
<div v-else class="text-center py-12"> <div v-else class="text-center py-12">
<v-icon size="80" color="grey-lighten-2" class="mb-4"> <v-icon class="mb-4" color="grey-lighten-2" size="80">
mdi-calendar-blank mdi-calendar-blank
</v-icon> </v-icon>
<div class="text-h5 text-grey-darken-1 mb-2">暂无考试科目安排</div> <div class="text-h5 text-grey-darken-1 mb-2">暂无考试科目安排</div>
@ -183,7 +186,7 @@
</div> </div>
<!-- JSON预览 --> <!-- JSON预览 -->
<v-card class="mb-4" elevation="2" border> <v-card border class="mb-4" elevation="2">
<v-card-title <v-card-title
class="d-flex align-center text-white cursor-pointer" class="d-flex align-center text-white cursor-pointer"
@click="showJsonPreview = !showJsonPreview" @click="showJsonPreview = !showJsonPreview"
@ -193,25 +196,25 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
color="white" color="white"
variant="outlined"
prepend-icon="mdi-content-copy" prepend-icon="mdi-content-copy"
size="small" size="small"
variant="outlined"
@click.stop="copyToClipboard" @click.stop="copyToClipboard"
> >
复制 复制
</v-btn> </v-btn>
<v-btn <v-btn
color="white"
variant="text"
size="small"
:icon="showJsonPreview ? 'mdi-chevron-up' : 'mdi-chevron-down'" :icon="showJsonPreview ? 'mdi-chevron-up' : 'mdi-chevron-down'"
class="ml-2" class="ml-2"
color="white"
size="small"
variant="text"
> >
</v-btn> </v-btn>
</v-card-title> </v-card-title>
<v-expand-transition> <v-expand-transition>
<v-card-text v-show="showJsonPreview" class="pa-4"> <v-card-text v-show="showJsonPreview" class="pa-4">
<v-card variant="tonal" class="pa-4"> <v-card class="pa-4" variant="tonal">
<pre class="json-preview"><code>{{ formattedJson }}</code></pre> <pre class="json-preview"><code>{{ formattedJson }}</code></pre>
</v-card> </v-card>
</v-card-text> </v-card-text>
@ -222,7 +225,7 @@
<!-- 编辑模式 --> <!-- 编辑模式 -->
<div v-if="!loading && isEditMode"> <div v-if="!loading && isEditMode">
<!-- 基本信息 --> <!-- 基本信息 -->
<v-card class="mb-4" elevation="1" border> <v-card border class="mb-4" elevation="1">
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-information</v-icon> <v-icon class="mr-2">mdi-information</v-icon>
基本信息 基本信息
@ -232,11 +235,11 @@
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="localConfig.examName" v-model="localConfig.examName"
:rules="[(v) => !!v || '考试名称不能为空']"
label="考试名称" label="考试名称"
prepend-inner-icon="mdi-calendar-text" prepend-inner-icon="mdi-calendar-text"
variant="outlined"
:rules="[(v) => !!v || '考试名称不能为空']"
required required
variant="outlined"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
@ -251,10 +254,10 @@
<v-textarea <v-textarea
v-model="localConfig.message" v-model="localConfig.message"
label="考试提示" label="考试提示"
prepend-inner-icon="mdi-message-text"
variant="outlined"
rows="3"
placeholder="输入考试相关的提示信息..." placeholder="输入考试相关的提示信息..."
prepend-inner-icon="mdi-message-text"
rows="3"
variant="outlined"
></v-textarea> ></v-textarea>
<!-- 默认提示选项 --> <!-- 默认提示选项 -->
@ -266,25 +269,25 @@
<v-chip <v-chip
v-for="(tip, index) in defaultExamTips" v-for="(tip, index) in defaultExamTips"
:key="index" :key="index"
color="primary"
variant="outlined"
size="small"
@click="selectDefaultTip(tip)"
class="ma-1" class="ma-1"
color="primary"
size="small"
variant="outlined"
@click="selectDefaultTip(tip)"
> >
<v-icon start size="small">mdi-plus</v-icon> <v-icon size="small" start>mdi-plus</v-icon>
{{ tip }} {{ tip }}
</v-chip> </v-chip>
</v-chip-group> </v-chip-group>
<div class="text-caption text-medium-emphasis ml-2"> <div class="text-caption text-medium-emphasis ml-2">
<v-icon size="small" class="mr-1">mdi-lightbulb-outline</v-icon> <v-icon class="mr-1" size="small">mdi-lightbulb-outline</v-icon>
点击上方选项快速添加常用考试提示 点击上方选项快速添加常用考试提示
</div> </div>
</v-card-text> </v-card-text>
</v-card> </v-card>
<!-- 考试科目安排 --> <!-- 考试科目安排 -->
<v-card class="mb-4" elevation="1" border> <v-card border class="mb-4" elevation="1">
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon class="mr-2">mdi-format-list-bulleted</v-icon> <v-icon class="mr-2">mdi-format-list-bulleted</v-icon>
考试科目安排 考试科目安排
@ -312,31 +315,31 @@
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<v-text-field <v-text-field
v-model="examInfo.name" v-model="examInfo.name"
:rules="[(v) => !!v || '科目名称不能为空']"
density="comfortable"
label="科目名称" label="科目名称"
prepend-inner-icon="mdi-book" prepend-inner-icon="mdi-book"
variant="outlined" variant="outlined"
density="comfortable"
:rules="[(v) => !!v || '科目名称不能为空']"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="3"> <v-col cols="12" md="3">
<v-menu <v-menu
v-model="examInfo.startDateMenu" v-model="examInfo.startDateMenu"
:close-on-content-click="false" :close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto" min-width="auto"
offset-y
transition="scale-transition"
> >
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-text-field <v-text-field
v-model="examInfo.startFormatted" v-model="examInfo.startFormatted"
:rules="[(v) => !!v || '开始时间不能为空']"
density="comfortable"
label="开始时间" label="开始时间"
prepend-inner-icon="mdi-clock-start" prepend-inner-icon="mdi-clock-start"
variant="outlined"
density="comfortable"
readonly readonly
v-bind="props" v-bind="props"
:rules="[(v) => !!v || '开始时间不能为空']" variant="outlined"
></v-text-field> ></v-text-field>
</template> </template>
<v-card min-width="600"> <v-card min-width="600">
@ -345,24 +348,24 @@
</v-card-title> </v-card-title>
<v-card-text class="pa-0"> <v-card-text class="pa-0">
<v-row no-gutters> <v-row no-gutters>
<v-col cols="6" class="border-e"> <v-col class="border-e" cols="6">
<v-date-picker <v-date-picker
v-model="examInfo.startDate" v-model="examInfo.startDate"
@update:model-value="updateStartDateTime(index)"
color="primary" color="primary"
elevation="0"
locale="zh-cn" locale="zh-cn"
show-adjacent-months show-adjacent-months
elevation="0" @update:model-value="updateStartDateTime(index)"
></v-date-picker> ></v-date-picker>
</v-col> </v-col>
<v-col cols="6"> <v-col cols="6">
<v-time-picker <v-time-picker
v-model="examInfo.startTime" v-model="examInfo.startTime"
@update:model-value="updateStartDateTime(index)"
color="primary" color="primary"
elevation="0"
format="24hr" format="24hr"
scrollable scrollable
elevation="0" @update:model-value="updateStartDateTime(index)"
></v-time-picker> ></v-time-picker>
</v-col> </v-col>
</v-row> </v-row>
@ -391,20 +394,20 @@
<v-menu <v-menu
v-model="examInfo.endDateMenu" v-model="examInfo.endDateMenu"
:close-on-content-click="false" :close-on-content-click="false"
transition="scale-transition"
offset-y
min-width="auto" min-width="auto"
offset-y
transition="scale-transition"
> >
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-text-field <v-text-field
v-model="examInfo.endFormatted" v-model="examInfo.endFormatted"
:rules="[(v) => !!v || '结束时间不能为空']"
density="comfortable"
label="结束时间" label="结束时间"
prepend-inner-icon="mdi-clock-end" prepend-inner-icon="mdi-clock-end"
variant="outlined"
density="comfortable"
readonly readonly
v-bind="props" v-bind="props"
:rules="[(v) => !!v || '结束时间不能为空']" variant="outlined"
></v-text-field> ></v-text-field>
</template> </template>
<v-card min-width="600"> <v-card min-width="600">
@ -413,24 +416,24 @@
</v-card-title> </v-card-title>
<v-card-text class="pa-0"> <v-card-text class="pa-0">
<v-row no-gutters> <v-row no-gutters>
<v-col cols="6" class="border-e"> <v-col class="border-e" cols="6">
<v-date-picker <v-date-picker
v-model="examInfo.endDate" v-model="examInfo.endDate"
@update:model-value="updateEndDateTime(index)"
color="primary" color="primary"
elevation="0"
locale="zh-cn" locale="zh-cn"
show-adjacent-months show-adjacent-months
elevation="0" @update:model-value="updateEndDateTime(index)"
></v-date-picker> ></v-date-picker>
</v-col> </v-col>
<v-col cols="6"> <v-col cols="6">
<v-time-picker <v-time-picker
v-model="examInfo.endTime" v-model="examInfo.endTime"
@update:model-value="updateEndDateTime(index)"
color="primary" color="primary"
elevation="0"
format="24hr" format="24hr"
scrollable scrollable
elevation="0" @update:model-value="updateEndDateTime(index)"
></v-time-picker> ></v-time-picker>
</v-col> </v-col>
</v-row> </v-row>
@ -455,32 +458,32 @@
</v-card> </v-card>
</v-menu> </v-menu>
</v-col> </v-col>
<v-col cols="12" md="2" class="d-flex align-center"> <v-col class="d-flex align-center" cols="12" md="2">
<v-btn <v-btn
icon="mdi-delete"
color="error" color="error"
variant="text" icon="mdi-delete"
size="small" size="small"
variant="text"
@click="removeExamInfo(index)" @click="removeExamInfo(index)"
> >
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
v-if="index > 0" v-if="index > 0"
icon="mdi-arrow-up"
color="primary" color="primary"
variant="text" icon="mdi-arrow-up"
size="small" size="small"
variant="text"
@click="moveExamInfo(index, -1)" @click="moveExamInfo(index, -1)"
> >
<v-icon>mdi-arrow-up</v-icon> <v-icon>mdi-arrow-up</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
v-if="index < localConfig.examInfos.length - 1" v-if="index < localConfig.examInfos.length - 1"
icon="mdi-arrow-down"
color="primary" color="primary"
variant="text" icon="mdi-arrow-down"
size="small" size="small"
variant="text"
@click="moveExamInfo(index, 1)" @click="moveExamInfo(index, 1)"
> >
<v-icon>mdi-arrow-down</v-icon> <v-icon>mdi-arrow-down</v-icon>
@ -491,7 +494,7 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
<div v-else class="text-center py-8"> <div v-else class="text-center py-8">
<v-icon size="48" color="grey-lighten-1" class="mb-2"> <v-icon class="mb-2" color="grey-lighten-1" size="48">
mdi-book-plus mdi-book-plus
</v-icon> </v-icon>
<p class="text-body-2 text-grey-darken-1 mb-4"> <p class="text-body-2 text-grey-darken-1 mb-4">
@ -509,7 +512,7 @@
<v-dialog v-model="deleteDialog" max-width="400"> <v-dialog v-model="deleteDialog" max-width="400">
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon color="error" class="mr-2">mdi-delete-alert</v-icon> <v-icon class="mr-2" color="error">mdi-delete-alert</v-icon>
确认删除配置 确认删除配置
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
@ -526,10 +529,10 @@
取消 取消
</v-btn> </v-btn>
<v-btn <v-btn
:loading="deleting"
color="error" color="error"
variant="outlined" variant="outlined"
@click="deleteConfig" @click="deleteConfig"
:loading="deleting"
> >
删除 删除
</v-btn> </v-btn>
@ -1063,7 +1066,7 @@ export default {
window.open(examUrl, '_blank'); window.open(examUrl, '_blank');
this.success = '配置已在新窗口中打开'; this.success = '配置已在新窗口中打开';
this.$emit('opened', { configId: this.configId, url: result.url }); this.$emit('opened', {configId: this.configId, url: result.url});
} else { } else {
throw new Error(result.error || '获取云端地址失败'); throw new Error(result.error || '获取云端地址失败');
} }
@ -1074,7 +1077,6 @@ export default {
}, },
/** /**
* 确认删除配置 * 确认删除配置
*/ */

View File

@ -1,10 +1,10 @@
<template> <template>
<a <a
aria-label="浙ICP备2024068645号"
class="floating-icp-link" class="floating-icp-link"
href="https://beian.miit.gov.cn/" href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label="浙ICP备2024068645号" target="_blank"
> >
浙ICP备2024068645号 浙ICP备2024068645号
</a> </a>

View File

@ -1,47 +1,47 @@
<template> <template>
<v-slide-y-transition> <v-slide-y-transition>
<v-card <v-card
:class="{ 'toolbar-expanded': isExpanded }"
class="floating-toolbar" class="floating-toolbar"
elevation="4" elevation="4"
rounded="xl" rounded="xl"
:class="{ 'toolbar-expanded': isExpanded }"
> >
<v-btn-group variant="text" class="toolbar-buttons"> <v-btn-group class="toolbar-buttons" variant="text">
<v-btn <v-btn
icon="mdi-chevron-left" v-ripple
variant="text" :title="'查看昨天'"
@click="$emit('prev-day')" class="toolbar-btn"
:title="'查看昨天'" icon="mdi-chevron-left"
class="toolbar-btn" variant="text"
v-ripple @click="$emit('prev-day')"
/> />
<v-btn <v-btn
v-ripple
:title="'缩小字体'"
class="toolbar-btn"
icon="mdi-format-font-size-decrease" icon="mdi-format-font-size-decrease"
variant="text" variant="text"
@click="$emit('zoom', 'out')" @click="$emit('zoom', 'out')"
:title="'缩小字体'"
class="toolbar-btn"
v-ripple
/> />
<v-btn <v-btn
v-ripple
:title="'放大字体'"
class="toolbar-btn"
icon="mdi-format-font-size-increase" icon="mdi-format-font-size-increase"
variant="text" variant="text"
@click="$emit('zoom', 'up')" @click="$emit('zoom', 'up')"
:title="'放大字体'"
class="toolbar-btn"
v-ripple
/> />
<v-menu location="top" :close-on-content-click="false"> <v-menu :close-on-content-click="false" location="top">
<template #activator="{ props }"> <template #activator="{ props }">
<v-btn <v-btn
icon="mdi-calendar" v-ripple
variant="text"
v-bind="props"
:title="'选择日期'" :title="'选择日期'"
class="toolbar-btn" class="toolbar-btn"
v-ripple icon="mdi-calendar"
v-bind="props"
variant="text"
/> />
</template> </template>
<v-card border class="date-picker-card"> <v-card border class="date-picker-card">
@ -53,24 +53,24 @@
</v-card> </v-card>
</v-menu> </v-menu>
<v-btn <v-btn
icon="mdi-refresh" v-ripple
variant="text"
:loading="loading" :loading="loading"
@click="$emit('refresh')"
:title="'刷新数据'" :title="'刷新数据'"
class="toolbar-btn" class="toolbar-btn"
v-ripple icon="mdi-refresh"
variant="text"
@click="$emit('refresh')"
/> />
<v-btn <v-btn
v-if="!isToday" v-if="!isToday"
icon="mdi-chevron-right" v-ripple
variant="text" :title="'查看明天'"
@click="$emit('next-day')" class="toolbar-btn"
:title="'查看明天'" icon="mdi-chevron-right"
class="toolbar-btn" variant="text"
v-ripple @click="$emit('next-day')"
/> />
</v-btn-group> </v-btn-group>

View File

@ -8,20 +8,20 @@
variant="tonal" variant="tonal"
> >
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon :icon="icons[message?.type] || icons.info" class="mr-2" /> <v-icon :icon="icons[message?.type] || icons.info" class="mr-2"/>
<div> <div>
<div class="text-subtitle-2 font-weight-medium">{{ message?.title }}</div> <div class="text-subtitle-2 font-weight-medium">{{ message?.title }}</div>
<div v-if="message?.content" class="text-body-2">{{ message?.content }}</div> <div v-if="message?.content" class="text-body-2">{{ message?.content }}</div>
</div> </div>
</div> </div>
<template #actions> <template #actions>
<v-btn variant="text" icon="mdi-close" @click="snackbar = false" /> <v-btn icon="mdi-close" variant="text" @click="snackbar = false"/>
</template> </template>
</v-snackbar> </v-snackbar>
</template> </template>
<script> <script>
import { defineComponent, ref, onBeforeUnmount, nextTick } from 'vue'; import {defineComponent, ref, onBeforeUnmount, nextTick} from 'vue';
import messageService from '@/utils/message'; import messageService from '@/utils/message';
export default defineComponent({ export default defineComponent({
@ -56,7 +56,7 @@ export default defineComponent({
onBeforeUnmount(() => unsubscribe?.()); onBeforeUnmount(() => unsubscribe?.());
return { snackbar, message, icons, colors }; return {snackbar, message, icons, colors};
} }
}); });
</script> </script>

View File

@ -16,7 +16,7 @@
<h1 class="text-h2 font-weight-bold">Vuetify</h1> <h1 class="text-h2 font-weight-bold">Vuetify</h1>
</div> </div>
<div class="py-4" /> <div class="py-4"/>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
@ -29,7 +29,7 @@
variant="outlined" variant="outlined"
> >
<template #image> <template #image>
<v-img position="top right" /> <v-img position="top right"/>
</template> </template>
<template #title> <template #title>
@ -38,16 +38,23 @@
<template #subtitle> <template #subtitle>
<div class="text-subtitle-1"> <div class="text-subtitle-1">
Replace this page by removing <v-kbd>{{ `<HelloWorld />` }}</v-kbd> in <v-kbd>pages/index.vue</v-kbd>. Replace this page by removing
<v-kbd>{{ `
<HelloWorld/>
` }}
</v-kbd>
in
<v-kbd>pages/index.vue</v-kbd>
.
</div> </div>
</template> </template>
<v-overlay <v-overlay
opacity=".12"
scrim="primary"
contained contained
model-value model-value
opacity=".12"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -67,11 +74,11 @@
variant="text" variant="text"
> >
<v-overlay <v-overlay
opacity=".06"
scrim="primary"
contained contained
model-value model-value
opacity=".06"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -91,11 +98,11 @@
variant="text" variant="text"
> >
<v-overlay <v-overlay
opacity=".06"
scrim="primary"
contained contained
model-value model-value
opacity=".06"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -115,11 +122,11 @@
variant="text" variant="text"
> >
<v-overlay <v-overlay
opacity=".06"
scrim="primary"
contained contained
model-value model-value
opacity=".06"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -139,11 +146,11 @@
variant="text" variant="text"
> >
<v-overlay <v-overlay
opacity=".06"
scrim="primary"
contained contained
model-value model-value
opacity=".06"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -153,5 +160,5 @@
</template> </template>
<script setup> <script setup>
// //
</script> </script>

View File

@ -1,6 +1,6 @@
# 创建新的作业编辑对话框组件 # 创建新的作业编辑对话框组件
<template> <template>
<v-dialog v-model="dialogVisible" width="auto" max-width="900" @click:outside="handleClose"> <v-dialog v-model="dialogVisible" max-width="900" width="auto" @click:outside="handleClose">
<v-card border> <v-card border>
<v-card-title>{{ title }}</v-card-title> <v-card-title>{{ title }}</v-card-title>
<v-card-subtitle> <v-card-subtitle>
@ -15,9 +15,9 @@
auto-grow auto-grow
placeholder="使用换行表示分条" placeholder="使用换行表示分条"
rows="5" rows="5"
width="480"
@click="updateCurrentLine" @click="updateCurrentLine"
@keyup="updateCurrentLine" @keyup="updateCurrentLine"
width="480"
/> />
<!-- Template Buttons Section --> <!-- Template Buttons Section -->
@ -29,9 +29,9 @@
<template v-if="subjectBooks"> <template v-if="subjectBooks">
<div v-for="(pages, book) in subjectBooks" :key="book" class="button-group"> <div v-for="(pages, book) in subjectBooks" :key="book" class="button-group">
<v-chip <v-chip
class="ma-1 book-chip"
:color="isBookSelected(book) ? 'success' : 'default'" :color="isBookSelected(book) ? 'success' : 'default'"
:variant="isBookSelected(book) ? 'elevated' : 'flat'" :variant="isBookSelected(book) ? 'elevated' : 'flat'"
class="ma-1 book-chip"
@click="handleBookClick(book)" @click="handleBookClick(book)"
> >
{{ book }} {{ book }}
@ -42,9 +42,9 @@
<v-chip <v-chip
v-for="page in pages" v-for="page in pages"
:key="page" :key="page"
class="ma-1"
:color="isPageSelected(book, page) ? 'info' : 'default'" :color="isPageSelected(book, page) ? 'info' : 'default'"
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'" :variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
class="ma-1"
@click="handlePageClick(book, page)" @click="handlePageClick(book, page)"
> >
{{ page }} {{ page }}
@ -57,9 +57,9 @@
<template v-if="commonBooks"> <template v-if="commonBooks">
<div v-for="(pages, book) in commonBooks" :key="book" class="button-group"> <div v-for="(pages, book) in commonBooks" :key="book" class="button-group">
<v-chip <v-chip
class="ma-1 book-chip"
:color="isBookSelected(book) ? 'success' : 'default'" :color="isBookSelected(book) ? 'success' : 'default'"
:variant="isBookSelected(book) ? 'elevated' : 'flat'" :variant="isBookSelected(book) ? 'elevated' : 'flat'"
class="ma-1 book-chip"
@click="handleBookClick(book)" @click="handleBookClick(book)"
> >
{{ book }} {{ book }}
@ -70,9 +70,9 @@
<v-chip <v-chip
v-for="page in pages" v-for="page in pages"
:key="page" :key="page"
class="ma-1"
:color="isPageSelected(book, page) ? 'info' : 'default'" :color="isPageSelected(book, page) ? 'info' : 'default'"
:variant="isPageSelected(book, page) ? 'elevated' : 'flat'" :variant="isPageSelected(book, page) ? 'elevated' : 'flat'"
class="ma-1"
@click="handlePageClick(book, page)" @click="handlePageClick(book, page)"
> >
{{ page }} {{ page }}
@ -102,16 +102,16 @@
</div> </div>
<!-- Quick Tools Section --> <!-- Quick Tools Section -->
<div class="quick-tools ml-4" style="min-width: 180px;" v-if="showQuickTools"> <div v-if="showQuickTools" class="quick-tools ml-4" style="min-width: 180px;">
<!-- Numeric Keypad --> <!-- Numeric Keypad -->
<div class="numeric-keypad mb-4"> <div class="numeric-keypad mb-4">
<div class="keypad-row"> <div class="keypad-row">
<v-btn <v-btn
v-for="n in 3" v-for="n in 3"
:key="n" :key="n"
class="keypad-btn"
size="small" size="small"
variant="tonal" variant="tonal"
class="keypad-btn"
@click="insertAtCursor(String(n))" @click="insertAtCursor(String(n))"
> >
{{ n }} {{ n }}
@ -121,9 +121,9 @@
<v-btn <v-btn
v-for="n in 3" v-for="n in 3"
:key="n" :key="n"
class="keypad-btn"
size="small" size="small"
variant="tonal" variant="tonal"
class="keypad-btn"
@click="insertAtCursor(String(n + 3))" @click="insertAtCursor(String(n + 3))"
> >
{{ n + 3 }} {{ n + 3 }}
@ -133,9 +133,9 @@
<v-btn <v-btn
v-for="n in 3" v-for="n in 3"
:key="n" :key="n"
class="keypad-btn"
size="small" size="small"
variant="tonal" variant="tonal"
class="keypad-btn"
@click="insertAtCursor(String(n + 6))" @click="insertAtCursor(String(n + 6))"
> >
{{ n + 6 }} {{ n + 6 }}
@ -143,26 +143,26 @@
</div> </div>
<div class="keypad-row"> <div class="keypad-row">
<v-btn <v-btn
class="keypad-btn"
size="small" size="small"
variant="tonal" variant="tonal"
class="keypad-btn"
@click="insertAtCursor('-')" @click="insertAtCursor('-')"
> >
- -
</v-btn> </v-btn>
<v-btn <v-btn
class="keypad-btn"
size="small" size="small"
variant="tonal" variant="tonal"
class="keypad-btn"
@click="insertAtCursor('0')" @click="insertAtCursor('0')"
> >
0 0
</v-btn> </v-btn>
<v-btn <v-btn
size="small"
variant="tonal"
class="keypad-btn" class="keypad-btn"
color="error" color="error"
size="small"
variant="tonal"
@click="deleteLastChar" @click="deleteLastChar"
> >
@ -170,16 +170,17 @@
</div> </div>
<div class="keypad-row"> <div class="keypad-row">
<v-btn <v-btn
class="keypad-btn space-btn"
size="small" size="small"
variant="tonal" variant="tonal"
class="keypad-btn space-btn"
@click="insertAtCursor(' ')" @click="insertAtCursor(' ')"
> >
空格 空格
</v-btn><v-btn </v-btn>
<v-btn
class="keypad-btn space-btn"
size="small" size="small"
variant="tonal" variant="tonal"
class="keypad-btn space-btn"
@click="insertAtCursor('\n')" @click="insertAtCursor('\n')"
> >
换行 换行
@ -212,7 +213,7 @@
<script> <script>
import dataProvider from "@/utils/dataProvider"; import dataProvider from "@/utils/dataProvider";
import { getSetting } from "@/utils/settings"; import {getSetting} from "@/utils/settings";
export default { export default {
name: "HomeworkEditDialog", name: "HomeworkEditDialog",
@ -242,7 +243,7 @@ export default {
currentLine: "", currentLine: "",
currentLineStart: 0, currentLineStart: 0,
currentLineEnd: 0, currentLineEnd: 0,
quickTexts: ["课", "题", "例","变","T", "P"] quickTexts: ["课", "题", "例", "变", "T", "P"]
}; };
}, },
computed: { computed: {
@ -391,8 +392,8 @@ export default {
currentLineContent.slice(0, lastIndex) + currentLineContent.slice(0, lastIndex) +
currentLineContent.slice(lastIndex + page.length); currentLineContent.slice(lastIndex + page.length);
this.content = this.content.slice(0, start) + this.content = this.content.slice(0, start) +
newLineContent.trim() + newLineContent.trim() +
this.content.slice(end); this.content.slice(end);
} }
} else { } else {
// //
@ -400,10 +401,10 @@ export default {
const end = this.currentLineEnd; const end = this.currentLineEnd;
const currentLineContent = this.content.slice(start, end); const currentLineContent = this.content.slice(start, end);
this.content = this.content.slice(0, start) + this.content = this.content.slice(0, start) +
currentLineContent.trim() + currentLineContent.trim() +
(currentLineContent.trim().length > 0 ? ' ' : '') + (currentLineContent.trim().length > 0 ? ' ' : '') +
page + page +
this.content.slice(end); this.content.slice(end);
} }
this.$nextTick(() => { this.$nextTick(() => {
const textarea = this.$refs.inputRef.$el.querySelector('textarea'); const textarea = this.$refs.inputRef.$el.querySelector('textarea');
@ -492,7 +493,6 @@ export default {
} }
.book-chip { .book-chip {
align-self: flex-start; align-self: flex-start;
} }
@ -547,4 +547,4 @@ export default {
.space-btn { .space-btn {
width: 100% !important; width: 100% !important;
} }
</style> </style>

View File

@ -25,8 +25,8 @@
<div class="card-horizontal-layout"> <div class="card-horizontal-layout">
<div class="card-icon-wrapper"> <div class="card-icon-wrapper">
<v-icon <v-icon
size="48"
color="primary" color="primary"
size="48"
> >
mdi-new-box mdi-new-box
</v-icon> </v-icon>
@ -53,8 +53,8 @@
<div class="card-horizontal-layout"> <div class="card-horizontal-layout">
<div class="card-icon-wrapper"> <div class="card-icon-wrapper">
<v-icon <v-icon
size="48"
color="success" color="success"
size="48"
> >
mdi-account-check mdi-account-check
</v-icon> </v-icon>
@ -81,8 +81,8 @@
<div class="card-horizontal-layout"> <div class="card-horizontal-layout">
<div class="card-icon-wrapper"> <div class="card-icon-wrapper">
<v-icon <v-icon
size="48"
color="info" color="info"
size="48"
> >
mdi-database-cog mdi-database-cog
</v-icon> </v-icon>
@ -102,33 +102,33 @@
<div class="options-buttons"> <div class="options-buttons">
<v-btn <v-btn
variant="tonal"
prepend-icon="mdi-laptop" prepend-icon="mdi-laptop"
size="small" size="small"
variant="tonal"
@click="useLocalMode" @click="useLocalMode"
> >
使用本地模式 使用本地模式
</v-btn> </v-btn>
<v-btn <v-btn
variant="tonal"
prepend-icon="mdi-flash" prepend-icon="mdi-flash"
size="small" size="small"
variant="tonal"
@click="handleAutoAuthorize" @click="handleAutoAuthorize"
> >
授权码式授权弃用 授权码式授权弃用
</v-btn> </v-btn>
<v-btn <v-btn
variant="tonal"
prepend-icon="mdi-key" prepend-icon="mdi-key"
size="small" size="small"
variant="tonal"
@click="showTokenDialog = true" @click="showTokenDialog = true"
> >
输入 Token 输入 Token
</v-btn> </v-btn>
<v-btn <v-btn
variant="tonal"
prepend-icon="mdi-code-tags" prepend-icon="mdi-code-tags"
size="small" size="small"
variant="tonal"
@click="showAlternativeCodeDialog = true" @click="showAlternativeCodeDialog = true"
> >
输入替代代码 输入替代代码
@ -158,10 +158,10 @@
> >
<DeviceAuthDialog <DeviceAuthDialog
ref="deviceAuthDialog" ref="deviceAuthDialog"
:show-cancel="true"
:preconfig="deviceAuthPreconfig" :preconfig="deviceAuthPreconfig"
@success="handleAuthSuccess" :show-cancel="true"
@cancel="showDeviceAuthDialog = false" @cancel="showDeviceAuthDialog = false"
@success="handleAuthSuccess"
/> />
</v-dialog> </v-dialog>
@ -171,8 +171,8 @@
> >
<TokenInputDialog <TokenInputDialog
:show-cancel="true" :show-cancel="true"
@success="handleTokenSuccess"
@cancel="showTokenDialog = false" @cancel="showTokenDialog = false"
@success="handleTokenSuccess"
/> />
</v-dialog> </v-dialog>
@ -182,16 +182,16 @@
> >
<AlternativeCodeDialog <AlternativeCodeDialog
:show-cancel="true" :show-cancel="true"
@submit="handleAlternativeCodeSubmit"
@cancel="showAlternativeCodeDialog = false" @cancel="showAlternativeCodeDialog = false"
@submit="handleAlternativeCodeSubmit"
/> />
</v-dialog> </v-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import {ref, computed, onMounted, watch} from 'vue'
import { getSetting, setSetting } from '@/utils/settings' import {getSetting, setSetting} from '@/utils/settings'
import DeviceAuthDialog from './auth/DeviceAuthDialog.vue' import DeviceAuthDialog from './auth/DeviceAuthDialog.vue'
import TokenInputDialog from './auth/TokenInputDialog.vue' import TokenInputDialog from './auth/TokenInputDialog.vue'
import AlternativeCodeDialog from './auth/AlternativeCodeDialog.vue' import AlternativeCodeDialog from './auth/AlternativeCodeDialog.vue'
@ -258,7 +258,7 @@ watch(
}, 500) }, 500)
} }
}, },
{ immediate: true, deep: true } {immediate: true, deep: true}
) )
onMounted(() => { onMounted(() => {
@ -339,7 +339,7 @@ const openClassworksKV = () => {
.init-header .title { .init-header .title {
font-size: 28px; font-size: 28px;
font-weight: 700; font-weight: 700;
text-align:left; text-align: left;
margin-bottom: 8px; margin-bottom: 8px;
} }
@ -364,7 +364,7 @@ const openClassworksKV = () => {
} }
.main-service-card:hover { .main-service-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
} }
.main-service-card .v-card-item { .main-service-card .v-card-item {
@ -387,18 +387,18 @@ const openClassworksKV = () => {
} }
.gradient-new { .gradient-new {
background: linear-gradient(135deg, rgba(33,150,243,.12), rgba(103,80,164,0.08) 60%); background: linear-gradient(135deg, rgba(33, 150, 243, .12), rgba(103, 80, 164, 0.08) 60%);
border: 2px solid rgba(33,150,243,.2); border: 2px solid rgba(33, 150, 243, .2);
} }
.gradient-registered { .gradient-registered {
background: linear-gradient(135deg, rgba(76,175,80,.12), rgba(0,184,212,0.08) 60%); background: linear-gradient(135deg, rgba(76, 175, 80, .12), rgba(0, 184, 212, 0.08) 60%);
border: 2px solid rgba(76,175,80,.2); border: 2px solid rgba(76, 175, 80, .2);
} }
.gradient-kv { .gradient-kv {
background: linear-gradient(135deg, rgba(0,184,212,.12), rgba(33,150,243,0.08) 60%); background: linear-gradient(135deg, rgba(0, 184, 212, .12), rgba(33, 150, 243, 0.08) 60%);
border: 2px solid rgba(0,184,212,.2); border: 2px solid rgba(0, 184, 212, .2);
} }
/* 其他选项 */ /* 其他选项 */

View File

@ -8,22 +8,22 @@
<v-card <v-card
class="kvinit-card" class="kvinit-card"
elevation="8" elevation="8"
title="初始化云端存储授权"
subtitle="请完成授权以启用云端存储功能"
prepend-icon="mdi-cloud-lock" prepend-icon="mdi-cloud-lock"
subtitle="请完成授权以启用云端存储功能"
title="初始化云端存储授权"
> >
<v-card-actions class="justify-end"> <v-card-actions class="justify-end">
<v-btn <v-btn
text
class="me-3" class="me-3"
text
@click="useLocalMode" @click="useLocalMode"
> >
使用本地模式 使用本地模式
</v-btn> </v-btn>
<v-btn <v-btn
:loading="loading"
color="primary" color="primary"
variant="flat" variant="flat"
:loading="loading"
@click="goToAuthorize" @click="goToAuthorize"
> >
前往授权 前往授权
@ -36,10 +36,10 @@
class="d-flex align-center" class="d-flex align-center"
> >
<v-progress-circular <v-progress-circular
class="me-2"
indeterminate indeterminate
size="20" size="20"
width="2" width="2"
class="me-2"
/> />
<span class="body-2"> 正在检查授权状态 </span> <span class="body-2"> 正在检查授权状态 </span>
</div> </div>
@ -57,10 +57,10 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount } from "vue"; import {ref, onMounted, onBeforeUnmount} from "vue";
import { useRoute } from "vue-router"; import {useRoute} from "vue-router";
import { getSetting,setSetting } from "@/utils/settings"; import {getSetting, setSetting} from "@/utils/settings";
import { kvServerProvider } from "@/utils/providers/kvServerProvider"; import {kvServerProvider} from "@/utils/providers/kvServerProvider";
const visible = ref(false); const visible = ref(false);
const loading = ref(false); const loading = ref(false);
@ -103,7 +103,7 @@ const goToAuthorize = () => {
// set a short-lived guard to prevent immediate re-redirect // set a short-lived guard to prevent immediate re-redirect
try { try {
const guardObj = { ts: Date.now() }; const guardObj = {ts: Date.now()};
sessionStorage.setItem(REDIRECT_GUARD_KEY, JSON.stringify(guardObj)); sessionStorage.setItem(REDIRECT_GUARD_KEY, JSON.stringify(guardObj));
} catch (err) { } catch (err) {
// sessionStorage may be unavailable in some environments // sessionStorage may be unavailable in some environments

View File

@ -1,5 +1,5 @@
<template> <template>
<v-navigation-drawer v-model="drawer" location="right" temporary width="400" v-if="drawer"> <v-navigation-drawer v-if="drawer" v-model="drawer" location="right" temporary width="400">
<v-toolbar color="primary"> <v-toolbar color="primary">
<v-toolbar-title>消息记录</v-toolbar-title> <v-toolbar-title>消息记录</v-toolbar-title>
@ -8,13 +8,14 @@
<v-list> <v-list>
<v-list-item v-for="msg in messages" :key="msg.id" rounded> <v-list-item v-for="msg in messages" :key="msg.id" rounded>
<template #prepend> <template #prepend>
<v-icon :icon="icons[msg.type]" :color="colors[msg.type]" size="20" /> <v-icon :color="colors[msg.type]" :icon="icons[msg.type]" size="20"/>
</template> </template>
<v-list-item-title>{{ msg.title }}</v-list-item-title> <v-list-item-title>{{ msg.title }}</v-list-item-title>
<v-list-item-subtitle v-if="msg.content">{{ <v-list-item-subtitle v-if="msg.content">{{
msg.content msg.content
}}</v-list-item-subtitle> }}
</v-list-item-subtitle>
<span class="text-caption text-grey"> <span class="text-caption text-grey">
{{ new Date(msg.timestamp).toLocaleTimeString() }} {{ new Date(msg.timestamp).toLocaleTimeString() }}
</span> </span>
@ -24,7 +25,7 @@
<v-list-item v-if="!messages.length"> <v-list-item v-if="!messages.length">
<template #prepend> <template #prepend>
<v-icon icon="mdi-inbox" color="grey" /> <v-icon color="grey" icon="mdi-inbox"/>
</template> </template>
<v-list-item-title class="text-grey">暂无消息</v-list-item-title> <v-list-item-title class="text-grey">暂无消息</v-list-item-title>
</v-list-item> </v-list-item>
@ -33,7 +34,7 @@
</template> </template>
<script> <script>
import { defineComponent, ref } from "vue"; import {defineComponent, ref} from "vue";
import messageService from "@/utils/message"; import messageService from "@/utils/message";
export default defineComponent({ export default defineComponent({

View File

@ -7,8 +7,8 @@
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="classNumber" v-model="classNumber"
label="班级编号"
hint="请输入需要迁移的班级编号" hint="请输入需要迁移的班级编号"
label="班级编号"
persistent-hint persistent-hint
prepend-icon="mdi-account-group" prepend-icon="mdi-account-group"
></v-text-field> ></v-text-field>
@ -17,8 +17,8 @@
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="machineId" v-model="machineId"
label="设备标识 (UUID)"
hint="系统已自动填充设备标识,通常无需修改" hint="系统已自动填充设备标识,通常无需修改"
label="设备标识 (UUID)"
persistent-hint persistent-hint
prepend-icon="mdi-identifier" prepend-icon="mdi-identifier"
readonly readonly
@ -27,32 +27,32 @@
</v-row> </v-row>
<v-radio-group v-model="migrationType" class="mt-2"> <v-radio-group v-model="migrationType" class="mt-2">
<v-radio value="local" label="本地数据迁移"></v-radio> <v-radio label="本地数据迁移" value="local"></v-radio>
<v-radio value="server" label="服务器数据迁移"></v-radio> <v-radio label="服务器数据迁移" value="server"></v-radio>
</v-radio-group> </v-radio-group>
<div v-if="migrationType === 'server'" class="mt-4"> <div v-if="migrationType === 'server'" class="mt-4">
<v-text-field <v-text-field
v-model="serverUrl" v-model="serverUrl"
label="服务器地址"
hint="输入服务器域名例如https://example.com" hint="输入服务器域名例如https://example.com"
label="服务器地址"
persistent-hint persistent-hint
prepend-icon="mdi-server" prepend-icon="mdi-server"
></v-text-field> ></v-text-field>
<v-alert <v-alert
class="mt-2"
density="compact" density="compact"
type="info" type="info"
variant="outlined" variant="outlined"
class="mt-2"
> >
服务器接口格式<br /> 服务器接口格式<br/>
- 配置接口域名/班号/config<br /> - 配置接口域名/班号/config<br/>
- 作业数据接口域名/班号/homework?date=YYYY-MM-DD - 作业数据接口域名/班号/homework?date=YYYY-MM-DD
</v-alert> </v-alert>
<div class="d-flex align-center mt-4"> <div class="d-flex align-center mt-4">
<v-icon color="warning" class="mr-2">mdi-calendar-range</v-icon> <v-icon class="mr-2" color="warning">mdi-calendar-range</v-icon>
<span class="text-subtitle-1">选择迁移时间范围</span> <span class="text-subtitle-1">选择迁移时间范围</span>
</div> </div>
@ -61,8 +61,8 @@
<v-text-field <v-text-field
v-model="startDate" v-model="startDate"
label="开始日期" label="开始日期"
type="date"
prepend-icon="mdi-calendar-start" prepend-icon="mdi-calendar-start"
type="date"
></v-text-field> ></v-text-field>
</v-col> </v-col>
@ -70,8 +70,8 @@
<v-text-field <v-text-field
v-model="endDate" v-model="endDate"
label="结束日期" label="结束日期"
type="date"
prepend-icon="mdi-calendar-end" prepend-icon="mdi-calendar-end"
type="date"
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@ -87,13 +87,13 @@
}}</span> }}</span>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
:loading="loading || scanning"
color="primary" color="primary"
@click=" @click="
migrationType === 'local' migrationType === 'local'
? scanLocalDatabase() ? scanLocalDatabase()
: previewServerData() : previewServerData()
" "
:loading="loading || scanning"
> >
{{ migrationType === "local" ? "扫描数据" : "加载数据" }} {{ migrationType === "local" ? "扫描数据" : "加载数据" }}
</v-btn> </v-btn>
@ -104,9 +104,9 @@
type="info" type="info"
> >
{{ {{
migrationType === "local" migrationType === "local"
? '尚未扫描本地数据或未找到可迁移的数据。点击"扫描数据"按钮开始扫描。' ? '尚未扫描本地数据或未找到可迁移的数据。点击"扫描数据"按钮开始扫描。'
: '尚未预览服务器数据或未找到可迁移的数据。点击"加载数据"按钮开始查询。' : '尚未预览服务器数据或未找到可迁移的数据。点击"加载数据"按钮开始查询。'
}} }}
</v-alert> </v-alert>
@ -115,8 +115,8 @@
:headers="headers" :headers="headers"
:items="displayItems" :items="displayItems"
:items-per-page="10" :items-per-page="10"
item-value="key"
class="elevation-1" class="elevation-1"
item-value="key"
> >
<template #[`item.type`]="{ item }"> <template #[`item.type`]="{ item }">
<v-chip <v-chip
@ -134,9 +134,9 @@
<v-alert <v-alert
v-if="displayItems.length > 0" v-if="displayItems.length > 0"
type="info"
density="compact"
class="mt-2" class="mt-2"
density="compact"
type="info"
> >
系统将迁移表格中显示的所有数据项迁移前请确认数据完整性 系统将迁移表格中显示的所有数据项迁移前请确认数据完整性
</v-alert> </v-alert>
@ -152,15 +152,15 @@
<v-card-title>迁移目标</v-card-title> <v-card-title>迁移目标</v-card-title>
<v-card-text> <v-card-text>
<v-radio-group v-model="targetStorage"> <v-radio-group v-model="targetStorage">
<v-radio value="kv-local" label="本地 KV 存储"></v-radio> <v-radio label="本地 KV 存储" value="kv-local"></v-radio>
<v-radio value="kv-server" label="服务器 KV 存储"></v-radio> <v-radio label="服务器 KV 存储" value="kv-server"></v-radio>
</v-radio-group> </v-radio-group>
<div v-if="targetStorage === 'kv-server'" class="mt-4"> <div v-if="targetStorage === 'kv-server'" class="mt-4">
<v-text-field <v-text-field
v-model="targetServerUrl" v-model="targetServerUrl"
label="目标服务器地址"
hint="输入KV服务器地址例如https://example.com/kv-api" hint="输入KV服务器地址例如https://example.com/kv-api"
label="目标服务器地址"
persistent-hint persistent-hint
prepend-icon="mdi-server-network" prepend-icon="mdi-server-network"
></v-text-field> ></v-text-field>
@ -170,10 +170,10 @@
<div class="d-flex justify-end mb-6"> <div class="d-flex justify-end mb-6">
<v-btn <v-btn
:disabled="!canMigrate"
:loading="migrating"
color="success" color="success"
@click="startMigration" @click="startMigration"
:loading="migrating"
:disabled="!canMigrate"
> >
开始迁移 开始迁移
</v-btn> </v-btn>
@ -188,7 +188,7 @@
<span>{{ migrationSuccess ? "迁移成功" : "迁移失败" }}</span> <span>{{ migrationSuccess ? "迁移成功" : "迁移失败" }}</span>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-alert v-if="migrationError" type="error" class="mb-4"> <v-alert v-if="migrationError" class="mb-4" type="error">
{{ migrationError }} {{ migrationError }}
</v-alert> </v-alert>
@ -216,7 +216,7 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="primary" @click="showResult = false"> 关闭 </v-btn> <v-btn color="primary" @click="showResult = false"> 关闭</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -224,9 +224,9 @@
</template> </template>
<script> <script>
import { openDB } from "idb"; import {openDB} from "idb";
import axios from "@/axios/axios"; import axios from "@/axios/axios";
import { getSetting, setSetting } from "@/utils/settings"; import {getSetting, setSetting} from "@/utils/settings";
export default { export default {
name: "MigrationTool", name: "MigrationTool",
@ -259,10 +259,10 @@ export default {
serverItems: [], serverItems: [],
selectedItems: [], selectedItems: [],
headers: [ headers: [
{ title: "类型", key: "type", sortable: true }, {title: "类型", key: "type", sortable: true},
{ title: "键名", key: "key", sortable: true }, {title: "键名", key: "key", sortable: true},
{ title: "日期", key: "date", sortable: true }, {title: "日期", key: "date", sortable: true},
{ title: "大小", key: "size", sortable: true }, {title: "大小", key: "size", sortable: true},
], ],
}; };
}, },
@ -317,7 +317,7 @@ export default {
// //
getRequestHeaders() { getRequestHeaders() {
const headers = { Accept: "application/json" }; const headers = {Accept: "application/json"};
const siteKey = getSetting("server.siteKey"); const siteKey = getSetting("server.siteKey");
if (siteKey) { if (siteKey) {
@ -330,7 +330,7 @@ export default {
// //
async scanLocalDatabase() { async scanLocalDatabase() {
if (!this.classNumber) { if (!this.classNumber) {
this.$emit("message", { text: "请先输入班级编号", type: "error" }); this.$emit("message", {text: "请先输入班级编号", type: "error"});
return; return;
} }
@ -606,7 +606,7 @@ export default {
); );
// Remove studentList from config // Remove studentList from config
const configWithoutStudentList = { ...value }; const configWithoutStudentList = {...value};
delete configWithoutStudentList.studentList; delete configWithoutStudentList.studentList;
// Save the modified config // Save the modified config
@ -619,7 +619,7 @@ export default {
// Just store the config as is // Just store the config as is
await db.put("kv", JSON.stringify(value), `classworks-config`); await db.put("kv", JSON.stringify(value), `classworks-config`);
} }
return { success: true, message: "配置已迁移" }; return {success: true, message: "配置已迁移"};
} else { } else {
// : classNumber/classworks-data-YYYYMMDD // : classNumber/classworks-data-YYYYMMDD
const itemDate = this.getItemDate(item); const itemDate = this.getItemDate(item);
@ -634,7 +634,7 @@ export default {
const [, year, month, day] = match; const [, year, month, day] = match;
dateStr = `${year}${month}${day}`; dateStr = `${year}${month}${day}`;
} else { } else {
return { success: false, message: "无法确定日期格式" }; return {success: false, message: "无法确定日期格式"};
} }
} }
@ -643,11 +643,11 @@ export default {
JSON.stringify(value), JSON.stringify(value),
`classworks-data-${dateStr}` `classworks-data-${dateStr}`
); );
return { success: true, message: `${dateStr} 数据已迁移` }; return {success: true, message: `${dateStr} 数据已迁移`};
} }
} catch (error) { } catch (error) {
console.error("本地KV迁移失败:", error); console.error("本地KV迁移失败:", error);
return { success: false, message: error.message }; return {success: false, message: error.message};
} }
}, },
@ -670,7 +670,7 @@ export default {
); );
// //
const configWithoutStudentList = { ...value }; const configWithoutStudentList = {...value};
delete configWithoutStudentList.studentList; delete configWithoutStudentList.studentList;
// //
@ -721,7 +721,7 @@ export default {
} }
); );
} }
return { success: true, message: "配置已迁移到服务器" }; return {success: true, message: "配置已迁移到服务器"};
} else { } else {
// //
const itemDate = this.getItemDate(item); const itemDate = this.getItemDate(item);
@ -736,7 +736,7 @@ export default {
const [, year, month, day] = match; const [, year, month, day] = match;
dateStr = `${year}${month}${day}`; dateStr = `${year}${month}${day}`;
} else { } else {
return { success: false, message: "无法确定日期格式" }; return {success: false, message: "无法确定日期格式"};
} }
} }
@ -747,7 +747,7 @@ export default {
headers: this.getRequestHeaders(), headers: this.getRequestHeaders(),
} }
); );
return { success: true, message: `${dateStr} 数据已迁移到服务器` }; return {success: true, message: `${dateStr} 数据已迁移到服务器`};
} }
} catch (error) { } catch (error) {
console.error("服务器KV迁移失败:", error); console.error("服务器KV迁移失败:", error);
@ -787,7 +787,7 @@ export default {
); );
// //
const configWithoutStudentList = { ...value }; const configWithoutStudentList = {...value};
delete configWithoutStudentList.studentList; delete configWithoutStudentList.studentList;
// //
@ -923,7 +923,7 @@ export default {
} }
} }
return { success: true }; return {success: true};
} catch (error) { } catch (error) {
console.error("批量迁移到服务器失败:", error); console.error("批量迁移到服务器失败:", error);
return { return {

View File

@ -4,7 +4,9 @@ Vue template files in this folder are automatically imported.
## 🚀 Usage ## 🚀 Usage
Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin automatically imports `.vue` files created in the `src/components` directory, and registers them as global components. This means that you can use any component in your application without having to manually import it. Importing is handled by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components). This plugin
automatically imports `.vue` files created in the `src/components` directory, and registers them as global components.
This means that you can use any component in your application without having to manually import it.
The following example assumes a component located at `src/components/MyComponent.vue`: The following example assumes a component located at `src/components/MyComponent.vue`:

View File

@ -1,16 +1,16 @@
<template> <template>
<v-dialog <v-dialog
v-model="dialog" v-model="dialog"
max-width="600"
fullscreen-breakpoint="sm" fullscreen-breakpoint="sm"
max-width="600"
persistent persistent
> >
<v-card class="random-picker-card" rounded="xl" border> <v-card border class="random-picker-card" rounded="xl">
<v-card-title class="text-h5 d-flex align-center"> <v-card-title class="text-h5 d-flex align-center">
<v-icon icon="mdi-account-question" class="mr-2" /> <v-icon class="mr-2" icon="mdi-account-question"/>
随机点名 随机点名
<v-spacer /> <v-spacer/>
<v-btn icon="mdi-close" variant="text" @click="dialog = false" /> <v-btn icon="mdi-close" variant="text" @click="dialog = false"/>
</v-card-title> </v-card-title>
<v-card-text v-if="!isPickingStarted" class="text-center py-6"> <v-card-text v-if="!isPickingStarted" class="text-center py-6">
@ -18,13 +18,13 @@
<div class="d-flex justify-center align-center counter-container"> <div class="d-flex justify-center align-center counter-container">
<v-btn <v-btn
size="x-large"
icon="mdi-minus"
variant="tonal"
color="primary"
:disabled="count <= 1" :disabled="count <= 1"
@click="decrementCount"
class="counter-btn" class="counter-btn"
color="primary"
icon="mdi-minus"
size="x-large"
variant="tonal"
@click="decrementCount"
/> />
<div class="count-display mx-8"> <div class="count-display mx-8">
@ -33,13 +33,13 @@
</div> </div>
<v-btn <v-btn
size="x-large"
icon="mdi-plus"
variant="tonal"
color="primary"
:disabled="count >= maxAllowedCount" :disabled="count >= maxAllowedCount"
@click="incrementCount"
class="counter-btn" class="counter-btn"
color="primary"
icon="mdi-plus"
size="x-large"
variant="tonal"
@click="incrementCount"
/> />
</div> </div>
@ -47,13 +47,13 @@
<div class="mode-switch-container mt-6"> <div class="mode-switch-container mt-6">
<v-btn-toggle <v-btn-toggle
v-model="pickerMode" v-model="pickerMode"
color="primary"
rounded="pill"
mandatory
class="mode-toggle" class="mode-toggle"
color="primary"
mandatory
rounded="pill"
> >
<v-btn value="name" prepend-icon="mdi-account">姓名模式</v-btn> <v-btn prepend-icon="mdi-account" value="name">姓名模式</v-btn>
<v-btn value="number" prepend-icon="mdi-numeric">学号模式</v-btn> <v-btn prepend-icon="mdi-numeric" value="number">学号模式</v-btn>
</v-btn-toggle> </v-btn-toggle>
</div> </div>
@ -63,36 +63,36 @@
<div class="d-flex justify-center align-center gap-4"> <div class="d-flex justify-center align-center gap-4">
<v-text-field <v-text-field
v-model.number="minNumber" v-model.number="minNumber"
label="最小值"
type="number"
min="1"
max="100"
hide-details
class="number-input" class="number-input"
density="compact" density="compact"
hide-details
label="最小值"
max="100"
min="1"
type="number"
/> />
<span class="mx-2"></span> <span class="mx-2"></span>
<v-text-field <v-text-field
v-model.number="maxNumber" v-model.number="maxNumber"
label="最大值"
type="number"
min="1"
max="100"
hide-details
class="number-input" class="number-input"
density="compact" density="compact"
hide-details
label="最大值"
max="100"
min="1"
type="number"
/> />
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<v-btn <v-btn
size="x-large"
color="primary"
prepend-icon="mdi-dice-multiple"
@click="startPicking"
:disabled="filteredStudents.length === 0" :disabled="filteredStudents.length === 0"
class="start-btn" class="start-btn"
color="primary"
prepend-icon="mdi-dice-multiple"
size="x-large"
@click="startPicking"
> >
开始抽取 开始抽取
</v-btn> </v-btn>
@ -112,10 +112,10 @@
<v-tooltip v-if="pickerMode === 'name'" location="bottom"> <v-tooltip v-if="pickerMode === 'name'" location="bottom">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-icon <v-icon
v-bind="props" class="ml-1"
icon="mdi-information-outline" icon="mdi-information-outline"
size="small" size="small"
class="ml-1" v-bind="props"
/> />
</template> </template>
<div class="pa-2"> <div class="pa-2">
@ -136,18 +136,18 @@
<v-chip <v-chip
:color="tempFilters.excludeLate ? 'warning' : 'default'" :color="tempFilters.excludeLate ? 'warning' : 'default'"
:variant="tempFilters.excludeLate ? 'elevated' : 'text'" :variant="tempFilters.excludeLate ? 'elevated' : 'text'"
@click="tempFilters.excludeLate = !tempFilters.excludeLate"
prepend-icon="mdi-clock-alert"
class="filter-chip" class="filter-chip"
prepend-icon="mdi-clock-alert"
@click="tempFilters.excludeLate = !tempFilters.excludeLate"
> >
{{ tempFilters.excludeLate ? "排除" : "包含" }}迟到学生 {{ tempFilters.excludeLate ? "排除" : "包含" }}迟到学生
</v-chip> </v-chip>
<v-chip <v-chip
:color="tempFilters.excludeAbsent ? 'error' : 'default'" :color="tempFilters.excludeAbsent ? 'error' : 'default'"
:variant="tempFilters.excludeAbsent ? 'elevated' : 'text'" :variant="tempFilters.excludeAbsent ? 'elevated' : 'text'"
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent"
prepend-icon="mdi-account-off"
class="filter-chip" class="filter-chip"
prepend-icon="mdi-account-off"
@click="tempFilters.excludeAbsent = !tempFilters.excludeAbsent"
> >
{{ tempFilters.excludeAbsent ? "排除" : "包含" }}请假学生 {{ tempFilters.excludeAbsent ? "排除" : "包含" }}请假学生
</v-chip> </v-chip>
@ -155,9 +155,9 @@
<v-chip <v-chip
:color="tempFilters.excludeExcluded ? 'grey' : 'default'" :color="tempFilters.excludeExcluded ? 'grey' : 'default'"
:variant="tempFilters.excludeExcluded ? 'elevated' : 'text'" :variant="tempFilters.excludeExcluded ? 'elevated' : 'text'"
@click="tempFilters.excludeExcluded = !tempFilters.excludeExcluded"
prepend-icon="mdi-account-cancel"
class="filter-chip" class="filter-chip"
prepend-icon="mdi-account-cancel"
@click="tempFilters.excludeExcluded = !tempFilters.excludeExcluded"
> >
{{ tempFilters.excludeExcluded ? "排除" : "包含" }}不参与学生 {{ tempFilters.excludeExcluded ? "排除" : "包含" }}不参与学生
</v-chip> </v-chip>
@ -169,15 +169,15 @@
<div v-if="isAnimating" class="animation-container"> <div v-if="isAnimating" class="animation-container">
<div class="animation-wrapper"> <div class="animation-wrapper">
<transition-group <transition-group
class="shuffle-container"
name="shuffle" name="shuffle"
tag="div" tag="div"
class="shuffle-container"
> >
<div <div
v-for="(student, index) in animationStudents" v-for="(student, index) in animationStudents"
:key="student.id" :key="student.id"
class="student-item"
:class="{ highlighted: highlightedIndices.includes(index) }" :class="{ highlighted: highlightedIndices.includes(index) }"
class="student-item"
> >
{{ student.name }} {{ student.name }}
</div> </div>
@ -190,46 +190,46 @@
<v-card <v-card
v-for="(student, index) in pickedStudents" v-for="(student, index) in pickedStudents"
:key="index" :key="index"
variant="outlined"
color="primary"
class="mb-2 result-card" class="mb-2 result-card"
color="primary"
variant="outlined"
> >
<v-card-text <v-card-text
class="text-h4 text-center py-4 d-flex align-center justify-center" class="text-h4 text-center py-4 d-flex align-center justify-center"
> >
{{ student }} {{ student }}
<v-btn <v-btn
icon="mdi-refresh"
variant="text"
size="small"
class="ml-2 refresh-btn"
@click="refreshSingleStudent(index)"
:disabled="remainingStudents.length === 0" :disabled="remainingStudents.length === 0"
:title=" :title="
remainingStudents.length === 0 remainingStudents.length === 0
? '没有更多可用学生' ? '没有更多可用学生'
: '重新抽取此学生' : '重新抽取此学生'
" "
class="ml-2 refresh-btn"
icon="mdi-refresh"
size="small"
variant="text"
@click="refreshSingleStudent(index)"
/> />
</v-card-text> </v-card-text>
</v-card> </v-card>
<div class="mt-8 d-flex justify-center"> <div class="mt-8 d-flex justify-center">
<v-btn <v-btn
class="mx-2"
color="primary" color="primary"
prepend-icon="mdi-refresh" prepend-icon="mdi-refresh"
@click="resetPicker"
size="large" size="large"
class="mx-2" @click="resetPicker"
> >
重新抽取 重新抽取
</v-btn> </v-btn>
<v-btn <v-btn
class="mx-2"
color="grey" color="grey"
size="large"
variant="outlined" variant="outlined"
@click="dialog = false" @click="dialog = false"
size="large"
class="mx-2"
> >
关闭 关闭
</v-btn> </v-btn>
@ -241,7 +241,7 @@
</template> </template>
<script> <script>
import { getSetting, setSetting } from "@/utils/settings"; import {getSetting, setSetting} from "@/utils/settings";
export default { export default {
name: "RandomPicker", name: "RandomPicker",
@ -253,7 +253,7 @@ export default {
attendance: { attendance: {
type: Object, type: Object,
required: true, required: true,
default: () => ({ absent: [], late: [], exclude: [] }), default: () => ({absent: [], late: [], exclude: []}),
}, },
}, },
data() { data() {

View File

@ -2,20 +2,21 @@
<v-dialog v-model="isVisible" max-width="500" persistent> <v-dialog v-model="isVisible" max-width="500" persistent>
<v-card class="rate-limit-modal"> <v-card class="rate-limit-modal">
<v-card-title class="text-center pa-4 bg-error text-white"> <v-card-title class="text-center pa-4 bg-error text-white">
<v-icon icon="mdi-clock-alert-outline" size="large" class="mr-2" /> <v-icon class="mr-2" icon="mdi-clock-alert-outline" size="large"/>
请求频率超限 请求频率超限
</v-card-title> </v-card-title>
<v-card-text class="pa-6"> <v-card-text class="pa-6">
<div class="text-body-1 mb-4">您的请求过于频繁请稍后再试</div> <div class="text-body-1 mb-4">您的请求过于频繁请稍后再试</div>
<v-card flat class="mb-4" v-if="activeRequests.length > 0"> <v-card v-if="activeRequests.length > 0" class="mb-4" flat>
<v-card-text> <v-card-text>
<v-list <v-list
v-for="(request, index) in activeRequests" v-for="(request, index) in activeRequests"
:key="index" :key="index"
class="mb-4" class="mb-4"
><v-list-item prepend-icon="mdi-web" color="primary"> >
<v-list-item color="primary" prepend-icon="mdi-web">
<v-list-item-title> <v-list-item-title>
等待时间: 等待时间:
<span class="text-primary font-weight-bold">{{ <span class="text-primary font-weight-bold">{{
@ -25,7 +26,8 @@
<v-list-item-subtitle> <v-list-item-subtitle>
{{ request.method }} {{ request.path }} {{ request.method }} {{ request.path }}
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item></v-list </v-list-item>
</v-list
> >
<v-divider <v-divider
v-if="index < activeRequests.length - 1" v-if="index < activeRequests.length - 1"
@ -41,7 +43,7 @@
<v-card-actions class="pa-4 pt-0"> <v-card-actions class="pa-4 pt-0">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" @click="close"> 我知道了 </v-btn> <v-btn color="primary" variant="tonal" @click="close"> 我知道了</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>

View File

@ -1,15 +1,15 @@
<template> <template>
<v-alert <v-alert
v-if="isReadOnly" v-if="isReadOnly"
class="readonly-warning"
closable
prominent
type="warning" type="warning"
variant="tonal" variant="tonal"
prominent
closable
class="readonly-warning"
@click:close="dismissed = true" @click:close="dismissed = true"
> >
<template #prepend> <template #prepend>
<v-icon icon="mdi-lock-alert" /> <v-icon icon="mdi-lock-alert"/>
</template> </template>
<v-alert-title>当前使用只读 Token</v-alert-title> <v-alert-title>当前使用只读 Token</v-alert-title>
<div class="text-body-2"> <div class="text-body-2">
@ -32,8 +32,8 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import {ref, computed, onMounted, watch} from 'vue'
import { getSetting } from '@/utils/settings' import {getSetting} from '@/utils/settings'
import axios from '@/axios/axios' import axios from '@/axios/axios'
const props = defineProps({ const props = defineProps({

View File

@ -1,11 +1,11 @@
<template> <template>
<v-card elevation="2" class="settings-card rounded-lg"> <v-card class="settings-card rounded-lg" elevation="2">
<v-card-item> <v-card-item>
<template #prepend> <template #prepend>
<v-icon <v-icon
:icon="icon" :icon="icon"
size="large"
class="mr-2" class="mr-2"
size="large"
/> />
</template> </template>
<v-card-title class="text-h6">{{ title }}</v-card-title> <v-card-title class="text-h6">{{ title }}</v-card-title>
@ -14,15 +14,15 @@
<v-card-text> <v-card-text>
<v-progress-linear <v-progress-linear
v-if="loading" v-if="loading"
indeterminate
color="primary"
class="mb-4" class="mb-4"
color="primary"
indeterminate
/> />
<slot /> <slot/>
</v-card-text> </v-card-text>
<v-card-actions v-if="$slots.actions" class="pa-4"> <v-card-actions v-if="$slots.actions" class="pa-4">
<slot name="actions" /> <slot name="actions"/>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</template> </template>

View File

@ -3,7 +3,7 @@
<!-- 统一链接生成器卡片 --> <!-- 统一链接生成器卡片 -->
<v-card border class="unified-link-generator"> <v-card border class="unified-link-generator">
<v-card-title class="text-h6"> <v-card-title class="text-h6">
<v-icon start icon="mdi-link-variant" class="mr-2" /> <v-icon class="mr-2" icon="mdi-link-variant" start/>
统一链接生成器 统一链接生成器
</v-card-title> </v-card-title>
@ -13,7 +13,7 @@
</div> </div>
<!-- 预配置认证信息部分 --> <!-- 预配置认证信息部分 -->
<v-card variant="tonal" class="mb-4"> <v-card class="mb-4" variant="tonal">
<v-card-title class="text-subtitle-1"> <v-card-title class="text-subtitle-1">
<v-icon start>mdi-account-key</v-icon> <v-icon start>mdi-account-key</v-icon>
预配置认证信息 预配置认证信息
@ -24,23 +24,23 @@
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="preconfigForm.namespace" v-model="preconfigForm.namespace"
label="命名空间"
variant="outlined"
prepend-inner-icon="mdi-identifier"
placeholder="例如: classroom-001"
hint="设备的命名空间标识符" hint="设备的命名空间标识符"
label="命名空间"
persistent-hint persistent-hint
placeholder="例如: classroom-001"
prepend-inner-icon="mdi-identifier"
variant="outlined"
/> />
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="preconfigForm.authCode" v-model="preconfigForm.authCode"
label="认证码"
variant="outlined"
prepend-inner-icon="mdi-lock-outline"
placeholder="设备认证码(可选)"
hint="留空则需要用户手动输入" hint="留空则需要用户手动输入"
label="认证码"
persistent-hint persistent-hint
placeholder="设备认证码(可选)"
prepend-inner-icon="mdi-lock-outline"
variant="outlined"
/> />
</v-col> </v-col>
</v-row> </v-row>
@ -49,10 +49,10 @@
<v-col cols="12"> <v-col cols="12">
<v-checkbox <v-checkbox
v-model="preconfigForm.autoExecute" v-model="preconfigForm.autoExecute"
label="自动执行认证"
hint="启用后会自动尝试认证,即使没有认证码也会尝试"
persistent-hint
density="compact" density="compact"
hint="启用后会自动尝试认证,即使没有认证码也会尝试"
label="自动执行认证"
persistent-hint
/> />
</v-col> </v-col>
</v-row> </v-row>
@ -60,36 +60,38 @@
<!-- 预配置信息预览 --> <!-- 预配置信息预览 -->
<v-alert <v-alert
v-if="preconfigForm.namespace" v-if="preconfigForm.namespace"
class="mt-3"
type="info" type="info"
variant="tonal" variant="tonal"
class="mt-3"
> >
<div class="text-subtitle-2 mb-2">预配置信息</div> <div class="text-subtitle-2 mb-2">预配置信息</div>
<v-chip size="small" class="mr-2 mb-1"> <v-chip class="mr-2 mb-1" size="small">
<v-icon start size="small">mdi-identifier</v-icon> <v-icon size="small" start>mdi-identifier</v-icon>
命名空间: {{ preconfigForm.namespace }} 命名空间: {{ preconfigForm.namespace }}
</v-chip> </v-chip>
<v-chip <v-chip
v-if="preconfigForm.authCode" v-if="preconfigForm.authCode"
size="small"
class="mr-2 mb-1" class="mr-2 mb-1"
color="warning" color="warning"
size="small"
> >
<v-icon start size="small">mdi-lock</v-icon> <v-icon size="small" start>mdi-lock</v-icon>
认证码: {{ preconfigForm.authCode.length > 8 ? preconfigForm.authCode.substring(0, 8) + "..." : preconfigForm.authCode }} 认证码: {{ preconfigForm.authCode.length > 8 ? preconfigForm.authCode.substring(0, 8) + "..." :
preconfigForm.authCode }}
</v-chip> </v-chip>
<v-chip v-else size="small" class="mr-2 mb-1" color="grey"> <v-chip v-else class="mr-2 mb-1" color="grey" size="small">
<v-icon start size="small">mdi-lock-open</v-icon> <v-icon size="small" start>mdi-lock-open</v-icon>
无认证码 无认证码
</v-chip> </v-chip>
<v-chip <v-chip
size="small"
class="mr-2 mb-1"
:color="preconfigForm.autoExecute ? 'success' : 'orange'" :color="preconfigForm.autoExecute ? 'success' : 'orange'"
class="mr-2 mb-1"
size="small"
> >
<v-icon start size="small">{{ <v-icon size="small" start>{{
preconfigForm.autoExecute ? "mdi-play-circle" : "mdi-hand-back-left" preconfigForm.autoExecute ? "mdi-play-circle" : "mdi-hand-back-left"
}}</v-icon> }}
</v-icon>
{{ preconfigForm.autoExecute ? "自动认证" : "手动认证" }} {{ preconfigForm.autoExecute ? "自动认证" : "手动认证" }}
</v-chip> </v-chip>
</v-alert> </v-alert>
@ -97,7 +99,7 @@
</v-card> </v-card>
<!-- 设置分享部分 --> <!-- 设置分享部分 -->
<v-card variant="tonal" class="mb-4"> <v-card class="mb-4" variant="tonal">
<v-card-title class="text-subtitle-1"> <v-card-title class="text-subtitle-1">
<v-icon start>mdi-cog-transfer</v-icon> <v-icon start>mdi-cog-transfer</v-icon>
设置分享可选 设置分享可选
@ -111,37 +113,37 @@
<!-- 设置快速选择按钮 --> <!-- 设置快速选择按钮 -->
<div class="d-flex mb-3 gap-2 flex-wrap"> <div class="d-flex mb-3 gap-2 flex-wrap">
<v-btn <v-btn
size="small"
variant="tonal"
color="primary" color="primary"
prepend-icon="mdi-server-network" prepend-icon="mdi-server-network"
size="small"
variant="tonal"
@click="selectDataSourceSettings" @click="selectDataSourceSettings"
> >
数据源设置 数据源设置
</v-btn> </v-btn>
<v-btn <v-btn
size="small"
variant="tonal"
color="primary" color="primary"
prepend-icon="mdi-compare" prepend-icon="mdi-compare"
size="small"
variant="tonal"
@click="selectChangedSettings" @click="selectChangedSettings"
> >
已变更设置 已变更设置
</v-btn> </v-btn>
<v-btn <v-btn
size="small"
variant="tonal"
color="success" color="success"
prepend-icon="mdi-select-all" prepend-icon="mdi-select-all"
size="small"
variant="tonal"
@click="selectAll" @click="selectAll"
> >
全选 全选
</v-btn> </v-btn>
<v-btn <v-btn
size="small"
variant="tonal"
color="error" color="error"
prepend-icon="mdi-select-remove" prepend-icon="mdi-select-remove"
size="small"
variant="tonal"
@click="resetSelection" @click="resetSelection"
> >
清除选择 清除选择
@ -150,7 +152,7 @@
<!-- 选择摘要 --> <!-- 选择摘要 -->
<div class="d-flex align-center mb-3 flex-wrap gap-2"> <div class="d-flex align-center mb-3 flex-wrap gap-2">
<v-chip color="primary" class="mr-2"> <v-chip class="mr-2" color="primary">
已选 {{ selectedItems.length }} 项设置 已选 {{ selectedItems.length }} 项设置
</v-chip> </v-chip>
@ -158,17 +160,17 @@
<v-chip <v-chip
v-for="item in selectedItems.slice(0, 3)" v-for="item in selectedItems.slice(0, 3)"
:key="item" :key="item"
size="small"
class="mr-1" class="mr-1"
size="small"
variant="text" variant="text"
> >
{{ getSettingDescription(item) }} {{ getSettingDescription(item) }}
</v-chip> </v-chip>
<v-chip <v-chip
v-if="selectedItems.length > 3" v-if="selectedItems.length > 3"
color="grey"
size="small" size="small"
variant="text" variant="text"
color="grey"
> >
+{{ selectedItems.length - 3 }} 更多 +{{ selectedItems.length - 3 }} 更多
</v-chip> </v-chip>
@ -190,39 +192,39 @@
<v-expansion-panel-text> <v-expansion-panel-text>
<v-text-field <v-text-field
v-model="search" v-model="search"
class="mb-4"
clearable
hide-details
label="搜索设置" label="搜索设置"
prepend-inner-icon="mdi-magnify" prepend-inner-icon="mdi-magnify"
single-line single-line
hide-details
class="mb-4"
clearable
/> />
<v-data-table <v-data-table
:items-per-page="settingItems.length" v-model="selectedItems"
:headers="headers" :headers="headers"
:items="filteredItems" :items="filteredItems"
item-value="key" :items-per-page="settingItems.length"
v-model="selectedItems"
show-select
density="compact"
class="rounded setting-table"
@update:selected="handleSelectionChange"
:sort-by="[{ key: 'isChanged', order: 'desc' }]" :sort-by="[{ key: 'isChanged', order: 'desc' }]"
class="rounded setting-table"
density="compact"
item-value="key"
show-select
@update:selected="handleSelectionChange"
> >
<template #[`item.description`]="{ item }"> <template #[`item.description`]="{ item }">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon <v-icon
size="small"
:icon="item.icon" :icon="item.icon"
class="mr-2" class="mr-2"
size="small"
/> />
{{ item.description }} {{ item.description }}
<v-chip <v-chip
v-if="item.key === 'server.kvToken'" v-if="item.key === 'server.kvToken'"
size="x-small"
color="error"
class="ml-2" class="ml-2"
color="error"
size="x-small"
> >
敏感 敏感
</v-chip> </v-chip>
@ -245,10 +247,10 @@
<template #[`item.isChanged`]="{ item }"> <template #[`item.isChanged`]="{ item }">
<v-chip <v-chip
size="x-small"
:color="item.isChanged ? 'warning' : 'success'" :color="item.isChanged ? 'warning' : 'success'"
:text="item.isChanged ? '已修改' : '默认'" :text="item.isChanged ? '已修改' : '默认'"
density="compact" density="compact"
size="x-small"
/> />
</template> </template>
</v-data-table> </v-data-table>
@ -259,7 +261,7 @@
</v-card> </v-card>
<!-- 链接生成和操作部分 --> <!-- 链接生成和操作部分 -->
<v-card variant="outlined" class="mb-4"> <v-card class="mb-4" variant="outlined">
<v-card-title class="text-subtitle-1"> <v-card-title class="text-subtitle-1">
<v-icon start>mdi-link</v-icon> <v-icon start>mdi-link</v-icon>
生成的统一链接 生成的统一链接
@ -269,27 +271,27 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="d-flex mb-3 gap-2 flex-wrap"> <div class="d-flex mb-3 gap-2 flex-wrap">
<v-btn <v-btn
variant="flat" :disabled="!preconfigForm.namespace.trim()"
color="primary" color="primary"
prepend-icon="mdi-auto-fix" prepend-icon="mdi-auto-fix"
variant="flat"
@click="generateUnifiedLink" @click="generateUnifiedLink"
:disabled="!preconfigForm.namespace.trim()"
> >
生成统一链接 生成统一链接
</v-btn> </v-btn>
<v-btn <v-btn
variant="tonal" :disabled="!unifiedLink"
color="success" color="success"
prepend-icon="mdi-test-tube" prepend-icon="mdi-test-tube"
variant="tonal"
@click="openTestLink" @click="openTestLink"
:disabled="!unifiedLink"
> >
测试链接 测试链接
</v-btn> </v-btn>
<v-btn <v-btn
variant="tonal"
color="error" color="error"
prepend-icon="mdi-delete" prepend-icon="mdi-delete"
variant="tonal"
@click="clearAll" @click="clearAll"
> >
清空所有 清空所有
@ -299,38 +301,38 @@
<!-- 生成的链接 --> <!-- 生成的链接 -->
<v-text-field <v-text-field
v-model="unifiedLink" v-model="unifiedLink"
:append-inner-icon="linkCopied ? 'mdi-check' : 'mdi-content-copy'"
:placeholder="preconfigForm.namespace ? '点击「生成统一链接」按钮' : '请先输入命名空间'"
class="mb-3"
label="统一链接" label="统一链接"
readonly readonly
variant="outlined" variant="outlined"
class="mb-3"
:append-inner-icon="linkCopied ? 'mdi-check' : 'mdi-content-copy'"
@click:append-inner="copyUnifiedLink" @click:append-inner="copyUnifiedLink"
:placeholder="preconfigForm.namespace ? '点击「生成统一链接」按钮' : '请先输入命名空间'"
/> />
<!-- 链接内容预览 --> <!-- 链接内容预览 -->
<v-alert <v-alert
v-if="unifiedLink" v-if="unifiedLink"
class="mb-3"
type="success" type="success"
variant="tonal" variant="tonal"
class="mb-3"
> >
<div class="text-subtitle-2 mb-2">链接包含内容</div> <div class="text-subtitle-2 mb-2">链接包含内容</div>
<div class="d-flex flex-wrap gap-1"> <div class="d-flex flex-wrap gap-1">
<v-chip size="small" color="primary"> <v-chip color="primary" size="small">
<v-icon start size="small">mdi-account-key</v-icon> <v-icon size="small" start>mdi-account-key</v-icon>
预配置认证 预配置认证
</v-chip> </v-chip>
<v-chip <v-chip
v-if="selectedItems.length > 0" v-if="selectedItems.length > 0"
size="small"
color="secondary" color="secondary"
size="small"
> >
<v-icon start size="small">mdi-cog</v-icon> <v-icon size="small" start>mdi-cog</v-icon>
{{ selectedItems.length }} 项设置 {{ selectedItems.length }} 项设置
</v-chip> </v-chip>
<v-chip v-else size="small" color="grey"> <v-chip v-else color="grey" size="small">
<v-icon start size="small">mdi-cog-off</v-icon> <v-icon size="small" start>mdi-cog-off</v-icon>
无额外设置 无额外设置
</v-chip> </v-chip>
</div> </div>
@ -389,16 +391,16 @@ export default {
unifiedLink: "", unifiedLink: "",
headers: [ headers: [
{ title: "", key: "data-table-select" }, {title: "", key: "data-table-select"},
{ title: "设置项", key: "description", sortable: true }, {title: "设置项", key: "description", sortable: true},
{ title: "当前值", key: "value", sortable: true }, {title: "当前值", key: "value", sortable: true},
{ {
title: "键名", title: "键名",
key: "key", key: "key",
class: "d-none d-sm-table-cell", class: "d-none d-sm-table-cell",
sortable: true, sortable: true,
}, },
{ title: "状态", key: "isChanged", sortable: true }, {title: "状态", key: "isChanged", sortable: true},
], ],
}; };
}, },
@ -550,7 +552,7 @@ export default {
); );
// //
const queryParams = { config: base64String }; const queryParams = {config: base64String};
// URL // URL
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);

View File

@ -14,12 +14,12 @@
<v-autocomplete <v-autocomplete
v-model="selectedName" v-model="selectedName"
:items="studentList" :items="studentList"
clearable
hide-details
item-title="name" item-title="name"
item-value="name" item-value="name"
label="学生姓名" label="学生姓名"
placeholder="选择您的姓名" placeholder="选择您的姓名"
clearable
hide-details
/> />
<div <div
v-if="studentList.length > 0" v-if="studentList.length > 0"
@ -29,9 +29,9 @@
</div> </div>
<v-alert <v-alert
v-if="error" v-if="error"
class="mt-3"
type="error" type="error"
variant="tonal" variant="tonal"
class="mt-3"
> >
{{ error }} {{ error }}
</v-alert> </v-alert>
@ -43,7 +43,7 @@
> >
稍后设置 稍后设置
</v-btn> </v-btn>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
:disabled="!selectedName || saving" :disabled="!selectedName || saving"
:loading="saving" :loading="saving"
@ -58,18 +58,19 @@
<!-- 顶栏学生姓名显示通过插槽暴露给父组件 --> <!-- 顶栏学生姓名显示通过插槽暴露给父组件 -->
<slot <slot
name="header-display"
:student-name="currentStudentName"
:is-student="isStudentToken" :is-student="isStudentToken"
:open-dialog="openDialog" :open-dialog="openDialog"
:student-name="currentStudentName"
name="header-display"
/> />
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, onMounted } from 'vue' import {ref, computed, watch, onMounted} from 'vue'
import { getSetting, watchSettings } from '@/utils/settings' import {getSetting, watchSettings} from '@/utils/settings'
import axios from '@/axios/axios' import axios from '@/axios/axios'
import dataProvider from '@/utils/dataProvider' import dataProvider from '@/utils/dataProvider'
const emit = defineEmits(['token-info-updated']) const emit = defineEmits(['token-info-updated'])
const showDialog = ref(false) const showDialog = ref(false)
@ -236,7 +237,7 @@ watchSettings(() => {
// tokenInfo // tokenInfo
watch(tokenInfo, () => { watch(tokenInfo, () => {
emit('token-info-updated') emit('token-info-updated')
}, { deep: true }) }, {deep: true})
// //
onMounted(() => { onMounted(() => {

View File

@ -4,23 +4,23 @@
<v-card-text> <v-card-text>
<v-textarea <v-textarea
v-model="code" v-model="code"
density="comfortable"
hide-details="auto"
label="替代代码" label="替代代码"
placeholder="请输入替代代码" placeholder="请输入替代代码"
variant="outlined"
density="comfortable"
rows="5" rows="5"
hide-details="auto" variant="outlined"
/> />
<v-alert <v-alert
class="mt-3"
type="info" type="info"
variant="tonal" variant="tonal"
class="mt-3"
> >
替代代码功能暂未实现敬请期待 替代代码功能暂未实现敬请期待
</v-alert> </v-alert>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
v-if="showCancel" v-if="showCancel"
variant="text" variant="text"
@ -40,7 +40,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import {ref} from 'vue'
defineProps({ defineProps({
showCancel: { showCancel: {

View File

@ -3,9 +3,9 @@
<v-card-text class="pa-8"> <v-card-text class="pa-8">
<div class="text-center mb-6"> <div class="text-center mb-6">
<v-icon <v-icon
size="80"
color="success"
class="mb-4" class="mb-4"
color="success"
size="80"
> >
mdi-account-key mdi-account-key
</v-icon> </v-icon>
@ -18,29 +18,29 @@
</div> </div>
<v-card <v-card
variant="tonal"
color="info"
class="pa-4 mb-6" class="pa-4 mb-6"
color="info"
variant="tonal"
> >
<div class="text-body-2"> <div class="text-body-2">
<v-icon <v-icon
size="20"
class="mr-2" class="mr-2"
size="20"
> >
mdi-information mdi-information
</v-icon> </v-icon>
对于已有UUID的用户您应当使用UUID与您的密码登录 对于已有UUID的用户您应当使用UUID与您的密码登录
</div> </div>
</v-card> </v-card>
<div class="form-section"> <div class="form-section">
<v-text-field <v-text-field
v-model="form.namespace" v-model="form.namespace"
label="命名空间"
class="mb-4" class="mb-4"
variant="outlined"
hide-details="auto" hide-details="auto"
label="命名空间"
prepend-inner-icon="mdi-identifier" prepend-inner-icon="mdi-identifier"
variant="outlined"
> >
</v-text-field> </v-text-field>
@ -48,19 +48,19 @@
<v-text-field <v-text-field
v-model="form.password" v-model="form.password"
label="认证码" label="认证码"
prepend-inner-icon="mdi-lock-outline"
type="text" type="text"
variant="outlined" variant="outlined"
prepend-inner-icon="mdi-lock-outline"
> >
</v-text-field> </v-text-field>
<v-alert <v-alert
v-if="error" v-if="error"
type="error"
variant="tonal"
class="mt-4" class="mt-4"
closable closable
type="error"
variant="tonal"
@click:close="error = ''" @click:close="error = ''"
> >
{{ error }} {{ error }}
@ -77,19 +77,19 @@
> >
取消 取消
</v-btn> </v-btn>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
:disabled="!form.namespace || authenticating" :disabled="!form.namespace || authenticating"
:loading="authenticating" :loading="authenticating"
size="x-large"
color="primary"
variant="elevated"
class="px-8" class="px-8"
color="primary"
size="x-large"
variant="elevated"
@click="authenticate" @click="authenticate"
> >
<v-icon <v-icon
start
size="24" size="24"
start
> >
mdi-login mdi-login
</v-icon> </v-icon>
@ -100,8 +100,8 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue' import {ref, watch} from 'vue'
import { getSetting, setSetting } from '@/utils/settings' import {getSetting, setSetting} from '@/utils/settings'
import axios from '@/axios/axios' import axios from '@/axios/axios'
const props = defineProps({ const props = defineProps({
@ -145,7 +145,7 @@ watch(
} }
} }
}, },
{ immediate: true, deep: true } {immediate: true, deep: true}
) )
const authenticate = async () => { const authenticate = async () => {
@ -197,7 +197,7 @@ const authenticate = async () => {
// //
defineExpose({ defineExpose({
reset: () => { reset: () => {
form.value = { namespace: '', password: '' } form.value = {namespace: '', password: ''}
error.value = '' error.value = ''
} }
}) })

View File

@ -15,9 +15,9 @@
> >
<div class="text-center mb-6"> <div class="text-center mb-6">
<v-icon <v-icon
size="80"
color="primary"
class="mb-4" class="mb-4"
color="primary"
size="80"
> >
mdi-hand-wave mdi-hand-wave
</v-icon> </v-icon>
@ -40,22 +40,22 @@
</h3> </h3>
<v-card <v-card
variant="tonal"
color="primary"
class="pa-6 mb-6" class="pa-6 mb-6"
color="primary"
variant="tonal"
> >
<div class="relationship-diagram"> <div class="relationship-diagram">
<!-- Classworks 应用 --> <!-- Classworks 应用 -->
<div class="diagram-item"> <div class="diagram-item">
<v-card <v-card
elevation="8"
color="blue-darken-1"
class="pa-4" class="pa-4"
color="blue-darken-1"
elevation="8"
> >
<div class="text-center"> <div class="text-center">
<v-icon <v-icon
size="60"
color="white" color="white"
size="60"
> >
mdi-laptop mdi-laptop
</v-icon> </v-icon>
@ -70,10 +70,10 @@
<div class="diagram-description mt-3"> <div class="diagram-description mt-3">
<v-chip <v-chip
color="blue"
variant="flat"
size="small"
class="mb-2" class="mb-2"
color="blue"
size="small"
variant="flat"
> >
前端应用 前端应用
</v-chip> </v-chip>
@ -88,8 +88,8 @@
<!-- 连接线 --> <!-- 连接线 -->
<div class="diagram-connector"> <div class="diagram-connector">
<v-icon <v-icon
size="40"
color="primary" color="primary"
size="40"
> >
mdi-swap-horizontal mdi-swap-horizontal
</v-icon> </v-icon>
@ -101,14 +101,14 @@
<!-- Classworks KV --> <!-- Classworks KV -->
<div class="diagram-item"> <div class="diagram-item">
<v-card <v-card
elevation="8"
color="green-darken-1"
class="pa-4" class="pa-4"
color="green-darken-1"
elevation="8"
> >
<div class="text-center"> <div class="text-center">
<v-icon <v-icon
size="60"
color="white" color="white"
size="60"
> >
mdi-cloud-sync mdi-cloud-sync
</v-icon> </v-icon>
@ -123,10 +123,10 @@
<div class="diagram-description mt-3"> <div class="diagram-description mt-3">
<v-chip <v-chip
color="green"
variant="flat"
size="small"
class="mb-2" class="mb-2"
color="green"
size="small"
variant="flat"
> >
后端服务 后端服务
</v-chip> </v-chip>
@ -153,9 +153,9 @@
</h3> </h3>
<v-card <v-card
variant="tonal"
color="info"
class="mb-6 pa-4" class="mb-6 pa-4"
color="info"
variant="tonal"
> >
<div class="text-body-2"> <div class="text-body-2">
比如在家里电脑手机上查看或者多个教室设备共享数据 比如在家里电脑手机上查看或者多个教室设备共享数据
@ -164,17 +164,17 @@
<div class="button-group"> <div class="button-group">
<v-btn <v-btn
size="x-large"
block block
variant="elevated"
color="primary"
class="mb-4 py-6" class="mb-4 py-6"
color="primary"
size="x-large"
variant="elevated"
@click="selectStorageType('cloud')" @click="selectStorageType('cloud')"
> >
<div class="d-flex flex-column align-center py-2"> <div class="d-flex flex-column align-center py-2">
<v-icon <v-icon
size="40"
class="mb-2" class="mb-2"
size="40"
> >
mdi-cloud-check mdi-cloud-check
</v-icon> </v-icon>
@ -184,16 +184,16 @@
</v-btn> </v-btn>
<v-btn <v-btn
size="x-large"
block block
variant="outlined"
class="py-6" class="py-6"
size="x-large"
variant="outlined"
@click="selectStorageType('local')" @click="selectStorageType('local')"
> >
<div class="d-flex flex-column align-center py-2"> <div class="d-flex flex-column align-center py-2">
<v-icon <v-icon
size="40"
class="mb-2" class="mb-2"
size="40"
> >
mdi-laptop mdi-laptop
</v-icon> </v-icon>
@ -211,9 +211,9 @@
> >
<div class="text-center mb-6"> <div class="text-center mb-6">
<v-icon <v-icon
size="80"
color="success"
class="mb-4" class="mb-4"
color="success"
size="80"
> >
mdi-check-circle mdi-check-circle
</v-icon> </v-icon>
@ -221,8 +221,8 @@
您可以使用本地模式 您可以使用本地模式
</h3> </h3>
<v-card <v-card
variant="tonal"
class="pa-4 text-left" class="pa-4 text-left"
variant="tonal"
> >
<div class="text-body-1 mb-2"> <div class="text-body-1 mb-2">
此数据将存储在您的浏览器中如果您的浏览器不支持IndexedDB可能会出现问题如果您经常清除浏览器数据请谨慎使用本地模式 此数据将存储在您的浏览器中如果您的浏览器不支持IndexedDB可能会出现问题如果您经常清除浏览器数据请谨慎使用本地模式
@ -241,9 +241,9 @@
> >
<div class="text-center mb-6"> <div class="text-center mb-6">
<v-icon <v-icon
size="80"
color="primary"
class="mb-4" class="mb-4"
color="primary"
size="80"
> >
mdi-cloud-cog mdi-cloud-cog
</v-icon> </v-icon>
@ -253,8 +253,8 @@
</div> </div>
<v-card <v-card
variant="tonal"
class="pa-6 mb-6" class="pa-6 mb-6"
variant="tonal"
> >
<div class="d-flex flex-column flex-sm-row align-center"> <div class="d-flex flex-column flex-sm-row align-center">
<div class="flex-grow-1"> <div class="flex-grow-1">
@ -266,9 +266,9 @@
</p> </p>
<v-btn <v-btn
color="primary" color="primary"
prepend-icon="mdi-flash"
size="large" size="large"
variant="elevated" variant="elevated"
prepend-icon="mdi-flash"
@click="goToProgressiveStep" @click="goToProgressiveStep"
> >
自动注册 自动注册
@ -277,16 +277,17 @@
</div> </div>
</v-card> </v-card>
<div class="mb-6"> <div class="mb-6">
也可以手动前往 Classworks KV 控制台获取认证信息</div> 也可以手动前往 Classworks KV 控制台获取认证信息
</div>
<v-card <v-card
:variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'"
:color=" kvserverurl=='https://kv.houlang.cloud'? 'primary' : 'error' " :color=" kvserverurl=='https://kv.houlang.cloud'? 'primary' : 'error' "
:variant="kvserverurl=='https://kv.houlang.cloud'? 'elevated' : 'outlined'"
class="pa-6 mb-6" class="pa-6 mb-6"
@click="openKVSite" @click="openKVSite"
> >
<v-icon <v-icon
size="48"
class="mb-3" class="mb-3"
size="48"
> >
mdi-open-in-new mdi-open-in-new
</v-icon> </v-icon>
@ -322,12 +323,13 @@
</v-expansion-panel-title> </v-expansion-panel-title>
<v-expansion-panel-text> <v-expansion-panel-text>
<v-card <v-card
variant="tonal"
color="success"
class="pa-4" class="pa-4"
color="success"
variant="tonal"
> >
<div class="text-body-2 mb-2"> <div class="text-body-2 mb-2">
如果您之前已经使用过 Classworks KV可以直接使用您的 <strong>UUID命名空间</strong> <strong>设置的密码</strong> 进行认证 如果您之前已经使用过 Classworks KV可以直接使用您的 <strong>UUID命名空间</strong>
<strong>设置的密码</strong> 进行认证
</div> </div>
<div class="text-body-2"> <div class="text-body-2">
返回上一页点击"已注册"按钮输入您的认证信息即可登录 返回上一页点击"已注册"按钮输入您的认证信息即可登录
@ -350,9 +352,9 @@
</v-expansion-panel-title> </v-expansion-panel-title>
<v-expansion-panel-text> <v-expansion-panel-text>
<v-card <v-card
variant="tonal"
color="info"
class="pa-4" class="pa-4"
color="info"
variant="tonal"
> >
<div class="text-body-2 mb-2"> <div class="text-body-2 mb-2">
不同的密码对应不同的设备类型这将由 <strong>管理员管理</strong> 不同的密码对应不同的设备类型这将由 <strong>管理员管理</strong>
@ -385,10 +387,10 @@
> >
<div class="text-center mb-6"> <div class="text-center mb-6">
<v-avatar <v-avatar
size="80"
color="primary"
variant="tonal"
class="mb-4" class="mb-4"
color="primary"
size="80"
variant="tonal"
> >
<v-icon size="48"> <v-icon size="48">
mdi-rocket-launch mdi-rocket-launch
@ -404,21 +406,20 @@
<v-progress-linear <v-progress-linear
:model-value="progressValue" :model-value="progressValue"
height="8"
color="primary"
rounded
class="mb-6" class="mb-6"
color="primary"
height="8"
rounded
/> />
<v-row> <v-row>
<v-col <v-col
cols="12" cols="12"
> >
<v-card <v-card
variant="tonal"
:color="statusColor" :color="statusColor"
variant="tonal"
> >
<v-card-item> <v-card-item>
<div class="d-flex align-center mb-3"> <div class="d-flex align-center mb-3">
@ -487,8 +488,8 @@
<v-btn <v-btn
v-if="progressiveStatus === 'idle'" v-if="progressiveStatus === 'idle'"
color="primary" color="primary"
size="large"
prepend-icon="mdi-play" prepend-icon="mdi-play"
size="large"
@click="startProgressiveRegister" @click="startProgressiveRegister"
> >
开始创建 开始创建
@ -497,8 +498,8 @@
<v-btn <v-btn
v-if="progressiveStatus === 'error'" v-if="progressiveStatus === 'error'"
color="error" color="error"
variant="outlined"
prepend-icon="mdi-refresh" prepend-icon="mdi-refresh"
variant="outlined"
@click="retryProgressiveRegister" @click="retryProgressiveRegister"
> >
重试 重试
@ -506,10 +507,10 @@
<v-btn <v-btn
v-if="progressiveStatus === 'registering'" v-if="progressiveStatus === 'registering'"
color="primary"
variant="tonal"
:loading="true" :loading="true"
color="primary"
prepend-icon="mdi-progress-clock" prepend-icon="mdi-progress-clock"
variant="tonal"
> >
正在执行 正在执行
</v-btn> </v-btn>
@ -517,9 +518,9 @@
<v-btn <v-btn
v-if="progressiveStatus === 'success'" v-if="progressiveStatus === 'success'"
color="success" color="success"
prepend-icon="mdi-check-circle"
size="large" size="large"
variant="elevated" variant="elevated"
prepend-icon="mdi-check-circle"
@click="applyTokenAndClose" @click="applyTokenAndClose"
> >
应用令牌并关闭 应用令牌并关闭
@ -528,9 +529,9 @@
<v-btn <v-btn
v-if="progressiveStatus === 'success'" v-if="progressiveStatus === 'success'"
color="primary" color="primary"
prepend-icon="mdi-open-in-new"
size="large" size="large"
variant="outlined" variant="outlined"
prepend-icon="mdi-open-in-new"
@click="openAuthPage" @click="openAuthPage"
> >
前往绑定账户 前往绑定账户
@ -552,12 +553,12 @@
</v-icon> </v-icon>
上一步 上一步
</v-btn> </v-btn>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
v-if="currentStep < totalSteps && currentStep !== 4" v-if="currentStep < totalSteps && currentStep !== 4"
:disabled="currentStep === 3 && !storageType" :disabled="currentStep === 3 && !storageType"
size="large"
color="primary" color="primary"
size="large"
variant="elevated" variant="elevated"
@click="nextStep" @click="nextStep"
> >
@ -568,8 +569,8 @@
</v-btn> </v-btn>
<v-btn <v-btn
v-if="currentStep === totalSteps || currentStep === 4" v-if="currentStep === totalSteps || currentStep === 4"
size="large"
color="primary" color="primary"
size="large"
variant="elevated" variant="elevated"
@click="finish" @click="finish"
> >
@ -580,10 +581,11 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import {ref, computed} from 'vue'
import { getSetting, setSetting } from '@/utils/settings' import {getSetting, setSetting} from '@/utils/settings'
import axios from '@/axios/axios' import axios from '@/axios/axios'
import { v4 as uuidv4 } from 'uuid' import {v4 as uuidv4} from 'uuid'
const emit = defineEmits(['close', 'success']) const emit = defineEmits(['close', 'success'])
const kvserverurl = getSetting('server.authDomain') const kvserverurl = getSetting('server.authDomain')
const currentStep = ref(1) const currentStep = ref(1)
@ -596,7 +598,7 @@ const progressiveError = ref('')
const deviceInfo = ref(null) const deviceInfo = ref(null)
const tokenData = ref(null) // token const tokenData = ref(null) // token
const logs = ref([]) const logs = ref([])
const stepStates = ref({ 1: false, 2: false, 3: false, 4: false }) const stepStates = ref({1: false, 2: false, 3: false, 4: false})
const nextStep = () => { const nextStep = () => {
if (currentStep.value < totalSteps) { if (currentStep.value < totalSteps) {
@ -638,28 +640,28 @@ const statusColor = computed(() => {
return progressiveStatus.value === 'success' return progressiveStatus.value === 'success'
? 'success' ? 'success'
: progressiveStatus.value === 'error' : progressiveStatus.value === 'error'
? 'error' ? 'error'
: 'primary' : 'primary'
}) })
const statusIcon = computed(() => { const statusIcon = computed(() => {
return progressiveStatus.value === 'success' return progressiveStatus.value === 'success'
? 'mdi-check-circle' ? 'mdi-check-circle'
: progressiveStatus.value === 'error' : progressiveStatus.value === 'error'
? 'mdi-alert-circle' ? 'mdi-alert-circle'
: progressiveStatus.value === 'registering' : progressiveStatus.value === 'registering'
? 'mdi-progress-clock' ? 'mdi-progress-clock'
: 'mdi-rocket-launch' : 'mdi-rocket-launch'
}) })
const statusTitle = computed(() => { const statusTitle = computed(() => {
return progressiveStatus.value === 'success' return progressiveStatus.value === 'success'
? '完成!设备已创建' ? '完成!设备已创建'
: progressiveStatus.value === 'error' : progressiveStatus.value === 'error'
? '创建失败' ? '创建失败'
: progressiveStatus.value === 'registering' : progressiveStatus.value === 'registering'
? '正在执行…' ? '正在执行…'
: '准备开始' : '准备开始'
}) })
const addLog = (message) => { const addLog = (message) => {
@ -667,7 +669,7 @@ const addLog = (message) => {
const hh = String(now.getHours()).padStart(2, '0') const hh = String(now.getHours()).padStart(2, '0')
const mm = String(now.getMinutes()).padStart(2, '0') const mm = String(now.getMinutes()).padStart(2, '0')
const ss = String(now.getSeconds()).padStart(2, '0') const ss = String(now.getSeconds()).padStart(2, '0')
logs.value.push({ time: `${hh}:${mm}:${ss}`, message }) logs.value.push({time: `${hh}:${mm}:${ss}`, message})
} }
@ -682,7 +684,7 @@ const startProgressiveRegister = async () => {
progressiveStatus.value = 'registering' progressiveStatus.value = 'registering'
progressiveError.value = '' progressiveError.value = ''
logs.value = [] logs.value = []
stepStates.value = { 1: false, 2: false, 3: false, 4: false } stepStates.value = {1: false, 2: false, 3: false, 4: false}
try { try {
addLog('正在生成设备信息…') addLog('正在生成设备信息…')
@ -692,12 +694,12 @@ const startProgressiveRegister = async () => {
stepStates.value[1] = true stepStates.value[1] = true
addLog('向服务器注册设备…') addLog('向服务器注册设备…')
const response = await axios.post(`${serverUrl}/devices`, { uuid, deviceName }) const response = await axios.post(`${serverUrl}/devices`, {uuid, deviceName})
void response void response
stepStates.value[2] = true stepStates.value[2] = true
// //
deviceInfo.value = { uuid, deviceName, createdAt: new Date().toISOString(), registered: true } deviceInfo.value = {uuid, deviceName, createdAt: new Date().toISOString(), registered: true}
localStorage.setItem('Classworks_progressive_device', JSON.stringify(deviceInfo.value)) localStorage.setItem('Classworks_progressive_device', JSON.stringify(deviceInfo.value))
addLog('获取访问令牌…') addLog('获取访问令牌…')
@ -742,7 +744,7 @@ const retryProgressiveRegister = () => {
progressiveStatus.value = 'idle' progressiveStatus.value = 'idle'
progressiveError.value = '' progressiveError.value = ''
logs.value = [] logs.value = []
stepStates.value = { 1: false, 2: false, 3: false, 4: false } stepStates.value = {1: false, 2: false, 3: false, 4: false}
} }
const openAuthPage = () => { const openAuthPage = () => {
@ -853,7 +855,7 @@ const applyTokenAndClose = () => {
} }
.progressive-register-card:hover { .progressive-register-card:hover {
box-shadow: 0 8px 24px rgba(0,0,0,0.12) !important; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12) !important;
} }
.progressive-register-card .card-icon-wrapper { .progressive-register-card .card-icon-wrapper {
@ -865,7 +867,7 @@ const applyTokenAndClose = () => {
} }
.progressive-register-card code { .progressive-register-card code {
background: rgba(0,0,0,0.1); background: rgba(0, 0, 0, 0.1);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;

View File

@ -2,8 +2,8 @@
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon <v-icon
icon="mdi-account-plus"
class="mr-2" class="mr-2"
icon="mdi-account-plus"
/> />
渐进式注册 渐进式注册
</v-card-title> </v-card-title>
@ -15,12 +15,12 @@
</p> </p>
<v-alert <v-alert
class="mb-4"
type="info" type="info"
variant="tonal" variant="tonal"
class="mb-4"
> >
<template #prepend> <template #prepend>
<v-icon icon="mdi-information" /> <v-icon icon="mdi-information"/>
</template> </template>
系统将自动为您创建设备并获取访问令牌无需手动配置 系统将自动为您创建设备并获取访问令牌无需手动配置
</v-alert> </v-alert>
@ -30,10 +30,10 @@
<div v-else-if="isRegistering"> <div v-else-if="isRegistering">
<div class="text-center py-4"> <div class="text-center py-4">
<v-progress-circular <v-progress-circular
class="mb-4"
color="primary"
indeterminate indeterminate
size="48" size="48"
color="primary"
class="mb-4"
/> />
<p class="text-h6 mb-2"> <p class="text-h6 mb-2">
正在注册设备... 正在注册设备...
@ -47,12 +47,12 @@
<!-- 注册成功 --> <!-- 注册成功 -->
<div v-else-if="isRegistered && deviceInfo"> <div v-else-if="isRegistered && deviceInfo">
<v-alert <v-alert
class="mb-4"
type="success" type="success"
variant="tonal" variant="tonal"
class="mb-4"
> >
<template #prepend> <template #prepend>
<v-icon icon="mdi-check-circle" /> <v-icon icon="mdi-check-circle"/>
</template> </template>
设备注册成功已自动获取访问令牌 设备注册成功已自动获取访问令牌
</v-alert> </v-alert>
@ -60,7 +60,7 @@
<v-list> <v-list>
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-identifier" /> <v-icon icon="mdi-identifier"/>
</template> </template>
<v-list-item-title>设备名称</v-list-item-title> <v-list-item-title>设备名称</v-list-item-title>
<v-list-item-subtitle>{{ deviceInfo.deviceName }}</v-list-item-subtitle> <v-list-item-subtitle>{{ deviceInfo.deviceName }}</v-list-item-subtitle>
@ -68,7 +68,7 @@
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-key" /> <v-icon icon="mdi-key"/>
</template> </template>
<v-list-item-title>设备 UUID</v-list-item-title> <v-list-item-title>设备 UUID</v-list-item-title>
<v-list-item-subtitle class="font-mono text-caption"> <v-list-item-subtitle class="font-mono text-caption">
@ -78,12 +78,12 @@
</v-list> </v-list>
<v-alert <v-alert
class="mt-4"
type="info" type="info"
variant="tonal" variant="tonal"
class="mt-4"
> >
<template #prepend> <template #prepend>
<v-icon icon="mdi-information" /> <v-icon icon="mdi-information"/>
</template> </template>
您可以点击下方按钮访问云端控制台来设置密码和管理高级功能 您可以点击下方按钮访问云端控制台来设置密码和管理高级功能
</v-alert> </v-alert>
@ -92,12 +92,12 @@
<!-- 错误状态 --> <!-- 错误状态 -->
<div v-else-if="errorMessage"> <div v-else-if="errorMessage">
<v-alert <v-alert
class="mb-4"
type="error" type="error"
variant="tonal" variant="tonal"
class="mb-4"
> >
<template #prepend> <template #prepend>
<v-icon icon="mdi-alert-circle" /> <v-icon icon="mdi-alert-circle"/>
</template> </template>
{{ errorMessage }} {{ errorMessage }}
</v-alert> </v-alert>
@ -105,14 +105,14 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<!-- 注册按钮 --> <!-- 注册按钮 -->
<v-btn <v-btn
v-if="!isRegistered && !isRegistering" v-if="!isRegistered && !isRegistering"
:loading="isRegistering"
color="primary" color="primary"
prepend-icon="mdi-plus" prepend-icon="mdi-plus"
:loading="isRegistering"
@click="registerDevice" @click="registerDevice"
> >
注册设备 注册设备
@ -149,8 +149,8 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import {ref} from 'vue'
import { getSetting, setSetting } from '@/utils/settings' import {getSetting, setSetting} from '@/utils/settings'
import axios from '@/axios/axios' import axios from '@/axios/axios'
// //
@ -168,7 +168,7 @@ const registrationStep = ref('')
// UUID // UUID
const generateUUID = () => { const generateUUID = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0 const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8) const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16) return v.toString(16)
@ -211,7 +211,7 @@ const registerDevice = async () => {
const serverUrl = getSetting('server.domain') const serverUrl = getSetting('server.domain')
registrationStep.value = '正在注册设备到服务器...' registrationStep.value = '正在注册设备到服务器...'
console.log('开始注册设备:', { uuid, deviceName, serverUrl }) console.log('开始注册设备:', {uuid, deviceName, serverUrl})
// //
const response = await axios.post(`${serverUrl}/devices`, { const response = await axios.post(`${serverUrl}/devices`, {

View File

@ -5,19 +5,24 @@
## 组件列表 ## 组件列表
### DeviceAuthDialog.vue ### DeviceAuthDialog.vue
设备认证对话框,用于通过 namespace 和密码进行设备认证。 设备认证对话框,用于通过 namespace 和密码进行设备认证。
**Props:** **Props:**
- `showCancel` (Boolean): 是否显示取消按钮,默认为 `false` - `showCancel` (Boolean): 是否显示取消按钮,默认为 `false`
**Events:** **Events:**
- `@success`: 认证成功时触发,传递认证数据 - `@success`: 认证成功时触发,传递认证数据
- `@cancel`: 点击取消按钮时触发 - `@cancel`: 点击取消按钮时触发
**暴露的方法:** **暴露的方法:**
- `reset()`: 清空表单和错误信息 - `reset()`: 清空表单和错误信息
**使用示例:** **使用示例:**
```vue ```vue
<template> <template>
<v-dialog v-model="dialog"> <v-dialog v-model="dialog">
@ -37,19 +42,24 @@ import DeviceAuthDialog from '@/components/auth/DeviceAuthDialog.vue'
--- ---
### TokenInputDialog.vue ### TokenInputDialog.vue
Token 输入对话框,用于手动输入 KV 授权 Token。 Token 输入对话框,用于手动输入 KV 授权 Token。
**Props:** **Props:**
- `showCancel` (Boolean): 是否显示取消按钮,默认为 `false` - `showCancel` (Boolean): 是否显示取消按钮,默认为 `false`
**Events:** **Events:**
- `@success`: Token 验证成功时触发 - `@success`: Token 验证成功时触发
- `@cancel`: 点击取消按钮时触发 - `@cancel`: 点击取消按钮时触发
**暴露的方法:** **暴露的方法:**
- `reset()`: 清空表单和错误信息 - `reset()`: 清空表单和错误信息
**使用示例:** **使用示例:**
```vue ```vue
<template> <template>
<v-dialog v-model="dialog"> <v-dialog v-model="dialog">
@ -69,19 +79,24 @@ import TokenInputDialog from '@/components/auth/TokenInputDialog.vue'
--- ---
### AlternativeCodeDialog.vue ### AlternativeCodeDialog.vue
替代代码输入对话框(功能暂未实现)。 替代代码输入对话框(功能暂未实现)。
**Props:** **Props:**
- `showCancel` (Boolean): 是否显示取消按钮,默认为 `false` - `showCancel` (Boolean): 是否显示取消按钮,默认为 `false`
**Events:** **Events:**
- `@submit`: 提交代码时触发,传递代码内容 - `@submit`: 提交代码时触发,传递代码内容
- `@cancel`: 点击取消按钮时触发 - `@cancel`: 点击取消按钮时触发
**暴露的方法:** **暴露的方法:**
- `reset()`: 清空表单 - `reset()`: 清空表单
**使用示例:** **使用示例:**
```vue ```vue
<template> <template>
<v-dialog v-model="dialog"> <v-dialog v-model="dialog">
@ -101,12 +116,15 @@ import AlternativeCodeDialog from '@/components/auth/AlternativeCodeDialog.vue'
--- ---
### FirstTimeGuide.vue ### FirstTimeGuide.vue
初次使用指南,介绍 Classworks KV 的功能和使用方式。 初次使用指南,介绍 Classworks KV 的功能和使用方式。
**Events:** **Events:**
- `@close`: 关闭指南时触发 - `@close`: 关闭指南时触发
**使用示例:** **使用示例:**
```vue ```vue
<template> <template>
<v-dialog v-model="dialog"> <v-dialog v-model="dialog">

View File

@ -4,24 +4,24 @@
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="token" v-model="token"
clearable
density="comfortable"
hide-details="auto"
label="KV 授权 Token" label="KV 授权 Token"
placeholder="粘贴从授权页面获取的 Token" placeholder="粘贴从授权页面获取的 Token"
variant="outlined" variant="outlined"
density="comfortable"
hide-details="auto"
clearable
/> />
<v-alert <v-alert
v-if="error" v-if="error"
class="mt-3"
type="error" type="error"
variant="tonal" variant="tonal"
class="mt-3"
> >
{{ error }} {{ error }}
</v-alert> </v-alert>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
v-if="showCancel" v-if="showCancel"
variant="text" variant="text"
@ -42,8 +42,8 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import {ref} from 'vue'
import { getSetting, setSetting } from '@/utils/settings' import {getSetting, setSetting} from '@/utils/settings'
import axios from '@/axios/axios' import axios from '@/axios/axios'
defineProps({ defineProps({

View File

@ -2,9 +2,9 @@
<div class="warning-container"> <div class="warning-container">
<v-chip <v-chip
v-if="show" v-if="show"
class="warning-chip"
color="warning" color="warning"
size="small" size="small"
class="warning-chip"
> >
{{ message }} {{ message }}
</v-chip> </v-chip>
@ -35,7 +35,13 @@ export default {
} }
@keyframes fade-in { @keyframes fade-in {
from { opacity: 0; transform: translateY(-10px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<v-container class="fill-height"> <v-container class="fill-height">
<v-responsive class="align-centerfill-height mx-auto" max-width="900"> <v-responsive class="align-centerfill-height mx-auto" max-width="900">
<v-img class="mb-4" height="150" src="@/assets/logo.svg" /> <v-img class="mb-4" height="150" src="@/assets/logo.svg"/>
<div class="text-center"> <div class="text-center">
<div class="text-body-2 font-weight-light mb-n1">出现了错误</div> <div class="text-body-2 font-weight-light mb-n1">出现了错误</div>
@ -9,7 +9,7 @@
<h1 class="text-h2 font-weight-bold">404</h1> <h1 class="text-h2 font-weight-bold">404</h1>
</div> </div>
<div class="py-4" /> <div class="py-4"/>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
@ -21,7 +21,7 @@
variant="outlined" variant="outlined"
> >
<template #image> <template #image>
<v-img position="top right" /> <v-img position="top right"/>
</template> </template>
<template #title> <template #title>
@ -33,11 +33,11 @@
</template> </template>
<v-overlay <v-overlay
opacity=".12"
scrim="primary"
contained contained
model-value model-value
opacity=".12"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -46,18 +46,18 @@
<v-card <v-card
class="py-4" class="py-4"
color="surface-variant" color="surface-variant"
to="/"
prepend-icon="mdi-home" prepend-icon="mdi-home"
rounded="lg" rounded="lg"
title="返回首页" title="返回首页"
to="/"
variant="text" variant="text"
> >
<v-overlay <v-overlay
opacity=".06"
scrim="primary"
contained contained
model-value model-value
opacity=".06"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -66,18 +66,18 @@
<v-card <v-card
class="py-4" class="py-4"
color="surface-variant" color="surface-variant"
@click="this.$router.back()"
prepend-icon="mdi-arrow-left-drop-circle" prepend-icon="mdi-arrow-left-drop-circle"
rounded="lg" rounded="lg"
title="返回上一页" title="返回上一页"
variant="text" variant="text"
@click="this.$router.back()"
> >
<v-overlay <v-overlay
opacity=".06"
scrim="primary"
contained contained
model-value model-value
opacity=".06"
persistent persistent
scrim="primary"
/> />
</v-card> </v-card>
</v-col> </v-col>
@ -88,4 +88,4 @@
<script setup> <script setup>
// //
</script> </script>

View File

@ -1,20 +1,20 @@
<template> <template>
<v-card border rounded="xl" hover> <v-card border hover rounded="xl">
<v-card-item> <v-card-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-information" size="large" class="mr-2" /> <v-icon class="mr-2" icon="mdi-information" size="large"/>
</template> </template>
<v-card-title class="text-h6">关于</v-card-title> <v-card-title class="text-h6">关于</v-card-title>
</v-card-item> </v-card-item>
<v-card-text> <v-card-text>
<v-row> <v-row>
<v-col cols="12" md="8" class="mx-auto"> <v-col class="mx-auto" cols="12" md="8">
<div class="d-flex flex-column align-start"> <div class="d-flex flex-column align-start">
<v-avatar size="120" class="mb-4"> <v-avatar class="mb-4" size="120">
<v-img <v-img
src="../../assets/cslogo.png"
alt="Classworks" alt="Classworks"
src="../../assets/cslogo.png"
/> />
</v-avatar> </v-avatar>
@ -24,35 +24,35 @@
<div class="d-flex gap-2 flex-wrap mb-6"> <div class="d-flex gap-2 flex-wrap mb-6">
<v-btn <v-btn
color="red" color="red"
variant="tonal"
href="https://github.com/ClassworksDev/Classworks/issues" href="https://github.com/ClassworksDev/Classworks/issues"
target="_blank"
prepend-icon="mdi-bug" prepend-icon="mdi-bug"
target="_blank"
variant="tonal"
> >
报告问题 报告问题
</v-btn> </v-btn>
<v-btn <v-btn
color="primary" color="primary"
variant="tonal"
href="https://qm.qq.com/q/qNBX4ZZVeg" href="https://qm.qq.com/q/qNBX4ZZVeg"
target="_blank"
prepend-icon="mdi-qqchat" prepend-icon="mdi-qqchat"
target="_blank"
variant="tonal"
> >
QQ QQ
</v-btn> </v-btn>
<v-btn <v-btn
variant="text"
href="https://github.com/ClassworksDev/Classworks" href="https://github.com/ClassworksDev/Classworks"
target="_blank"
prepend-icon="mdi-github" prepend-icon="mdi-github"
target="_blank"
variant="text"
> >
前端 前端
</v-btn> </v-btn>
<v-btn <v-btn
variant="text"
href="https://github.com/ClassworksDev/ClassworksServer" href="https://github.com/ClassworksDev/ClassworksServer"
target="_blank"
prepend-icon="mdi-github" prepend-icon="mdi-github"
target="_blank"
variant="text"
> >
后端 后端
</v-btn> </v-btn>
@ -63,9 +63,9 @@
<h3 class="text-h6 mb-2">备注与致谢</h3> <h3 class="text-h6 mb-2">备注与致谢</h3>
<v-list class="mb-4 bg-transparent"> <v-list class="mb-4 bg-transparent">
<v-list-item <v-list-item
append-icon="mdi-link"
href="https://github.com/EnderWolf006/HomeworkBoard" href="https://github.com/EnderWolf006/HomeworkBoard"
target="_blank" target="_blank"
append-icon="mdi-link"
> >
<v-list-item-title> <v-list-item-title>
本项目受到 HomeworkBoard 的启发而开发 本项目受到 HomeworkBoard 的启发而开发
@ -76,9 +76,9 @@
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
append-icon="mdi-link"
href="https://hlyun.org" href="https://hlyun.org"
target="_blank" target="_blank"
append-icon="mdi-link"
> >
<v-list-item-title> <v-list-item-title>
Classworks <strong>厚浪云</strong>提供 Classworks <strong>厚浪云</strong>提供
@ -88,9 +88,9 @@
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
append-icon="mdi-link"
href="https://zerocat.houlangs.com" href="https://zerocat.houlangs.com"
target="_blank" target="_blank"
append-icon="mdi-link"
> >
<v-list-item-title> <v-list-item-title>
感谢 ZeroCat 社区的开发者们 感谢 ZeroCat 社区的开发者们
@ -101,9 +101,9 @@
</v-list-item> </v-list-item>
<v-divider class="ma-1"></v-divider> <v-divider class="ma-1"></v-divider>
<v-list-item <v-list-item
append-icon="mdi-link"
href="https://github.com/HUSX100/IslandCaller" href="https://github.com/HUSX100/IslandCaller"
target="_blank" target="_blank"
append-icon="mdi-link"
> >
<v-list-item-title> <v-list-item-title>
本项目与 IslandCaller 没有从属关系 本项目与 IslandCaller 没有从属关系
@ -114,9 +114,9 @@
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
append-icon="mdi-link"
href="https://classisland.tech" href="https://classisland.tech"
target="_blank" target="_blank"
append-icon="mdi-link"
> >
<v-list-item-title> <v-list-item-title>
本项目与 ClassIsland 没有从属关系 本项目与 ClassIsland 没有从属关系
@ -129,9 +129,9 @@
</v-list> </v-list>
<v-btn <v-btn
variant="text"
class="mb-4" class="mb-4"
prepend-icon="mdi-package-variant" prepend-icon="mdi-package-variant"
variant="text"
@click="showDeps = true" @click="showDeps = true"
> >
查看使用的第三方库 查看使用的第三方库
@ -139,11 +139,12 @@
<v-dialog <v-dialog
v-model="showDeps" v-model="showDeps"
transition="dialog-bottom-transition"
fullscreen fullscreen
transition="dialog-bottom-transition"
> >
<v-card <v-card
><v-toolbar> >
<v-toolbar>
<v-btn icon="mdi-close" @click="showDeps = false"></v-btn> <v-btn icon="mdi-close" @click="showDeps = false"></v-btn>
<v-toolbar-title>使用的第三方库</v-toolbar-title> <v-toolbar-title>使用的第三方库</v-toolbar-title>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@ -154,8 +155,8 @@
v-for="dep in Dependencies" v-for="dep in Dependencies"
:key="dep.name" :key="dep.name"
:href="'https://www.npmjs.com/package/' + dep.name" :href="'https://www.npmjs.com/package/' + dep.name"
target="_blank"
append-icon="mdi-link" append-icon="mdi-link"
target="_blank"
> >
<v-list-item-title> <v-list-item-title>
{{ dep.name }} {{ dep.name }}
@ -180,7 +181,7 @@
</template> </template>
<script> <script>
import { ref, onMounted } from "vue"; import {ref, onMounted} from "vue";
import packageJson from "../../../package.json"; import packageJson from "../../../package.json";
export default { export default {

View File

@ -1,14 +1,14 @@
<template> <template>
<v-card :border="border" class="setting-group"> <v-card :border="border" class="setting-group">
<v-card-title v-if="title" class="d-flex align-center"> <v-card-title v-if="title" class="d-flex align-center">
<v-icon v-if="icon" :icon="icon" class="mr-2" /> <v-icon v-if="icon" :icon="icon" class="mr-2"/>
{{ title }} {{ title }}
</v-card-title> </v-card-title>
<v-card-subtitle v-if="description"> <v-card-subtitle v-if="description">
{{ description }} {{ description }}
</v-card-subtitle> </v-card-subtitle>
<v-card-text> <v-card-text>
<v-list> <v-list>
<slot> <slot>
@ -16,7 +16,7 @@
</slot> </slot>
</v-list> </v-list>
</v-card-text> </v-card-text>
<v-card-actions v-if="$slots.actions"> <v-card-actions v-if="$slots.actions">
<slot name="actions"></slot> <slot name="actions"></slot>
</v-card-actions> </v-card-actions>
@ -26,7 +26,7 @@
<script> <script>
export default { export default {
name: 'SettingGroup', name: 'SettingGroup',
props: { props: {
/** /**
* 设置组的标题 * 设置组的标题
@ -35,7 +35,7 @@ export default {
type: String, type: String,
default: null default: null
}, },
/** /**
* 设置组的描述 * 设置组的描述
*/ */
@ -43,7 +43,7 @@ export default {
type: String, type: String,
default: null default: null
}, },
/** /**
* 设置组的图标 * 设置组的图标
*/ */
@ -51,7 +51,7 @@ export default {
type: String, type: String,
default: null default: null
}, },
/** /**
* 是否显示边框 * 是否显示边框
*/ */
@ -60,12 +60,12 @@ export default {
default: false default: false
} }
}, },
methods: { methods: {
onSettingUpdate(key, value) { onSettingUpdate(key, value) {
this.$emit('update', key, value); this.$emit('update', key, value);
}, },
onSettingError(key) { onSettingError(key) {
this.$emit('error', key); this.$emit('error', key);
} }
@ -77,4 +77,4 @@ export default {
.setting-group { .setting-group {
margin-bottom: 16px; margin-bottom: 16px;
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<v-list-item class="setting-item" :disabled="disabled"> <v-list-item :disabled="disabled" class="setting-item">
<template #prepend> <template #prepend>
<v-icon :icon="settingIcon" /> <v-icon :icon="settingIcon"/>
</template> </template>
<v-list-item-title class="text-wrap"> <v-list-item-title class="text-wrap">
@ -19,57 +19,57 @@
<v-switch <v-switch
v-if="type === 'boolean'" v-if="type === 'boolean'"
v-model="localValue" v-model="localValue"
:disabled="disabled"
density="comfortable" density="comfortable"
hide-details hide-details
:disabled="disabled"
@update:model-value="updateSetting" @update:model-value="updateSetting"
/> />
<v-select <v-select
v-else-if="type === 'string' && hasOptions" v-else-if="type === 'string' && hasOptions"
v-model="localValue" v-model="localValue"
:disabled="disabled"
:items="selectOptions" :items="selectOptions"
bg-color="surface"
class="setting-select"
density="compact" density="compact"
hide-details hide-details
:disabled="disabled"
class="setting-select"
variant="outlined"
bg-color="surface"
@update:model-value="updateSetting"
item-title="title" item-title="title"
item-value="value" item-value="value"
variant="outlined"
@update:model-value="updateSetting"
/> />
<div v-else-if="type === 'number'" class="d-flex align-center"> <div v-else-if="type === 'number'" class="d-flex align-center">
<v-btn <v-btn
:disabled="disabled || localValue <= minValue"
icon="mdi-minus" icon="mdi-minus"
size="small" size="small"
variant="text" variant="text"
:disabled="disabled || localValue <= minValue"
@click="adjustValue(-stepValue)" @click="adjustValue(-stepValue)"
/> />
<v-text-field <v-text-field
v-model.number="localValue" v-model.number="localValue"
type="number" :disabled="disabled"
:max="maxValue"
:min="minValue"
:step="stepValue"
bg-color="surface"
class="mx-2 setting-number-field"
density="compact" density="compact"
hide-details hide-details
:min="minValue"
:max="maxValue"
:step="stepValue"
:disabled="disabled"
class="mx-2 setting-number-field"
style="width: 80px" style="width: 80px"
type="number"
variant="outlined" variant="outlined"
bg-color="surface"
@update:model-value="updateSetting" @update:model-value="updateSetting"
/> />
<v-btn <v-btn
:disabled="disabled || localValue >= maxValue"
icon="mdi-plus" icon="mdi-plus"
size="small" size="small"
variant="text" variant="text"
:disabled="disabled || localValue >= maxValue"
@click="adjustValue(stepValue)" @click="adjustValue(stepValue)"
/> />
</div> </div>
@ -78,34 +78,34 @@
<v-menu location="bottom"> <v-menu location="bottom">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn <v-btn
:disabled="disabled"
class="ml-2"
icon="mdi-dots-vertical" icon="mdi-dots-vertical"
size="small" size="small"
variant="text"
v-bind="props" v-bind="props"
class="ml-2" variant="text"
:disabled="disabled"
/> />
</template> </template>
<v-list density="compact"> <v-list density="compact">
<v-list-item @click="copySettingId"> <v-list-item @click="copySettingId">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-key" size="small" /> <v-icon icon="mdi-key" size="small"/>
</template> </template>
<v-list-item-title>复制设置ID</v-list-item-title> <v-list-item-title>复制设置ID</v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item @click="copySettingValue"> <v-list-item @click="copySettingValue">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-content-copy" size="small" /> <v-icon icon="mdi-content-copy" size="small"/>
</template> </template>
<v-list-item-title>复制设置值</v-list-item-title> <v-list-item-title>复制设置值</v-list-item-title>
</v-list-item> </v-list-item>
<v-divider></v-divider> <v-divider></v-divider>
<v-list-item @click="resetToDefault" :disabled="isDefaultValue"> <v-list-item :disabled="isDefaultValue" @click="resetToDefault">
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-restore" size="small" /> <v-icon icon="mdi-restore" size="small"/>
</template> </template>
<v-list-item-title>重置为默认值</v-list-item-title> <v-list-item-title>重置为默认值</v-list-item-title>
</v-list-item> </v-list-item>
@ -119,12 +119,12 @@
<div v-if="type === 'string' && !hasOptions" class="px-4 pb-2 pt-0"> <div v-if="type === 'string' && !hasOptions" class="px-4 pb-2 pt-0">
<v-text-field <v-text-field
v-model="localValue" v-model="localValue"
:disabled="disabled"
bg-color="surface"
class="setting-text-field mt-1"
density="compact" density="compact"
hide-details hide-details
:disabled="disabled"
class="setting-text-field mt-1"
variant="outlined" variant="outlined"
bg-color="surface"
@update:model-value="updateSetting" @update:model-value="updateSetting"
/> />
</div> </div>
@ -206,20 +206,20 @@ export default {
showSnackbar: false, showSnackbar: false,
snackbarText: "", snackbarText: "",
fontFamilies: [ fontFamilies: [
{ title: "Arial", value: "Arial, sans-serif" }, {title: "Arial", value: "Arial, sans-serif"},
{ title: "Calibri", value: "Calibri, sans-serif" }, {title: "Calibri", value: "Calibri, sans-serif"},
{ title: "Cambria", value: "Cambria, serif" }, {title: "Cambria", value: "Cambria, serif"},
{ title: "Consolas", value: "Consolas, monospace" }, {title: "Consolas", value: "Consolas, monospace"},
{ title: "Courier New", value: "Courier New, monospace" }, {title: "Courier New", value: "Courier New, monospace"},
{ title: "Georgia", value: "Georgia, serif" }, {title: "Georgia", value: "Georgia, serif"},
{ title: "Helvetica", value: "Helvetica, sans-serif" }, {title: "Helvetica", value: "Helvetica, sans-serif"},
{ title: "Segoe UI", value: "Segoe UI, sans-serif" }, {title: "Segoe UI", value: "Segoe UI, sans-serif"},
{ title: "Times New Roman", value: "Times New Roman, serif" }, {title: "Times New Roman", value: "Times New Roman, serif"},
{ title: "Trebuchet MS", value: "Trebuchet MS, sans-serif" }, {title: "Trebuchet MS", value: "Trebuchet MS, sans-serif"},
{ title: "Verdana", value: "Verdana, sans-serif" }, {title: "Verdana", value: "Verdana, sans-serif"},
{ title: "Monospace", value: "monospace" }, {title: "Monospace", value: "monospace"},
{ title: "Sans-serif", value: "sans-serif" }, {title: "Sans-serif", value: "sans-serif"},
{ title: "Serif", value: "serif" }, {title: "Serif", value: "serif"},
], ],
// //
displayValueMappings: { displayValueMappings: {

View File

@ -1,27 +1,28 @@
<template> <template>
<div class="settings-explorer"> <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" />
<div>
<v-text-field v-model="searchQuery" class="mb-4" clearable density="comfortable" label="搜索设置"
prepend-inner-icon="mdi-magnify" variant="outlined"/>
<v-list> <v-list>
<div v-for="setting in allSettings" :key="setting.key"> <div v-for="setting in allSettings" :key="setting.key">
<setting-item :key="setting.key" :setting-key="setting.key" <setting-item :key="setting.key" :disabled="setting.requireDeveloper && !isDeveloperMode"
:disabled="setting.requireDeveloper && !isDeveloperMode" @update="onSettingUpdate" @error="onSettingError" /> :setting-key="setting.key" @error="onSettingError"
<v-divider class="my-2" /> @update="onSettingUpdate"/>
<v-divider class="my-2"/>
</div> </div>
</v-list><v-card border> </v-list>
<v-card border>
<v-card-title class="text-subtitle-1">当前配置</v-card-title> <v-card-title class="text-subtitle-1">当前配置</v-card-title>
<v-card-text> <v-card-text>
<pre class="settings-json">{{ formattedSettings }}</pre> <pre class="settings-json">{{ formattedSettings }}</pre>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn @click="copySettingsToClipboard"> <v-btn @click="copySettingsToClipboard">
复制到剪贴板 复制到剪贴板
<v-icon right>mdi-content-copy</v-icon> <v-icon right>mdi-content-copy</v-icon>
</v-btn> </v-btn>
@ -32,7 +33,7 @@
</template> </template>
<script> <script>
import { getSetting, settingsDefinitions, exportSettingsAsKeyValue, watchSettings } from '@/utils/settings'; import {getSetting, settingsDefinitions, exportSettingsAsKeyValue, watchSettings} from '@/utils/settings';
import SettingItem from './SettingItem.vue'; import SettingItem from './SettingItem.vue';
export default { export default {
@ -82,7 +83,7 @@ export default {
created() { created() {
// //
this.updateCurrentSettings(); this.updateCurrentSettings();
// //
this.unwatchFunction = watchSettings(() => { this.unwatchFunction = watchSettings(() => {
this.updateCurrentSettings(); this.updateCurrentSettings();
@ -115,11 +116,11 @@ export default {
navigator.clipboard.writeText(JSON.stringify(this.currentSettings)) navigator.clipboard.writeText(JSON.stringify(this.currentSettings))
.then(() => { .then(() => {
// //
this.$emit('message', { type: 'success', text: '设置已复制到剪贴板' }); this.$emit('message', {type: 'success', text: '设置已复制到剪贴板'});
}) })
.catch(err => { .catch(err => {
console.error('复制到剪贴板失败:', err); console.error('复制到剪贴板失败:', err);
this.$emit('message', { type: 'error', text: '复制到剪贴板失败' }); this.$emit('message', {type: 'error', text: '复制到剪贴板失败'});
}); });
} }
} }
@ -145,4 +146,4 @@ export default {
.v-theme--dark .settings-json { .v-theme--dark .settings-json {
background-color: rgba(255, 255, 255, 0.05); background-color: rgba(255, 255, 255, 0.05);
} }
</style> </style>

View File

@ -1,29 +1,29 @@
<template> <template>
<v-card <v-card
border
:color="unsavedChanges ? 'warning-subtle' : undefined"
:class="{ 'unsaved-changes': unsavedChanges }" :class="{ 'unsaved-changes': unsavedChanges }"
:color="unsavedChanges ? 'warning-subtle' : undefined"
border
> >
<v-card-item> <v-card-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-account-group" size="large" class="mr-2" /> <v-icon class="mr-2" icon="mdi-account-group" size="large"/>
</template> </template>
<v-card-title class="text-h6">学生列表</v-card-title> <v-card-title class="text-h6">学生列表</v-card-title>
<template #append> <template #append>
<unsaved-warning :show="unsavedChanges" message="有未保存的更改" /> <unsaved-warning :show="unsavedChanges" message="有未保存的更改"/>
<v-btn <v-btn
:disabled="modelValue.list.length === 0"
class="mr-2"
prepend-icon="mdi-sort-alphabetical-variant" prepend-icon="mdi-sort-alphabetical-variant"
variant="text" variant="text"
class="mr-2"
@click="sortStudentsByPinyin" @click="sortStudentsByPinyin"
:disabled="modelValue.list.length === 0"
> >
按姓名首字母排序 按姓名首字母排序
</v-btn> </v-btn>
<v-btn <v-btn
:color="modelValue.advanced ? 'primary' : undefined" :color="modelValue.advanced ? 'primary' : undefined"
variant="text"
prepend-icon="mdi-code-braces" prepend-icon="mdi-code-braces"
variant="text"
@click="toggleAdvanced" @click="toggleAdvanced"
> >
{{ modelValue.advanced ? "返回基础编辑" : "高级编辑" }} {{ modelValue.advanced ? "返回基础编辑" : "高级编辑" }}
@ -34,12 +34,12 @@
<v-card-text> <v-card-text>
<v-progress-linear <v-progress-linear
v-if="loading" v-if="loading"
indeterminate
color="primary"
class="mb-4" class="mb-4"
color="primary"
indeterminate
/> />
<v-alert v-if="error" type="error" variant="tonal" closable class="mb-4"> <v-alert v-if="error" class="mb-4" closable type="error" variant="tonal">
{{ error }} {{ error }}
</v-alert> </v-alert>
@ -47,23 +47,23 @@
<!-- 普通编辑模式 --> <!-- 普通编辑模式 -->
<div v-if="!modelValue.advanced"> <div v-if="!modelValue.advanced">
<v-row class="mb-6"> <v-row class="mb-6">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" md="4" sm="6">
<v-text-field <v-text-field
v-model="newStudentName" v-model="newStudentName"
class="mb-4"
hide-details
label="添加学生" label="添加学生"
placeholder="输入学生姓名后回车添加" placeholder="输入学生姓名后回车添加"
prepend-inner-icon="mdi-account-plus" prepend-inner-icon="mdi-account-plus"
variant="outlined" variant="outlined"
hide-details
class="mb-4"
@keyup.enter="addStudent" @keyup.enter="addStudent"
> >
<template #append> <template #append>
<v-btn <v-btn
:disabled="!newStudentName.trim()"
color="primary"
icon="mdi-plus" icon="mdi-plus"
variant="text" variant="text"
color="primary"
:disabled="!newStudentName.trim()"
@click="addStudent" @click="addStudent"
/> />
</template> </template>
@ -76,25 +76,25 @@
v-for="(student, index) in modelValue.list" v-for="(student, index) in modelValue.list"
:key="index" :key="index"
cols="12" cols="12"
sm="6"
md="4"
lg="3" lg="3"
md="4"
sm="6"
> >
<v-hover v-slot="{ isHovering, props }"> <v-hover v-slot="{ isHovering, props }">
<v-card <v-card
v-bind="props"
:elevation="isMobile ? 1 : isHovering ? 4 : 1" :elevation="isMobile ? 1 : isHovering ? 4 : 1"
class="student-card"
border border
class="student-card"
v-bind="props"
> >
<v-card-text class="d-flex align-center pa-3"> <v-card-text class="d-flex align-center pa-3">
<v-menu location="bottom" :open-on-hover="!isMobile"> <v-menu :open-on-hover="!isMobile" location="bottom">
<template #activator="{ props: menuProps }"> <template #activator="{ props: menuProps }">
<v-btn <v-btn
variant="tonal"
size="small"
class="mr-3 font-weight-medium" class="mr-3 font-weight-medium"
size="small"
v-bind="menuProps" v-bind="menuProps"
variant="tonal"
> >
{{ index + 1 }} {{ index + 1 }}
</v-btn> </v-btn>
@ -102,23 +102,23 @@
<v-list density="compact" nav> <v-list density="compact" nav>
<v-list-item <v-list-item
prepend-icon="mdi-arrow-up-bold"
:disabled="index === 0" :disabled="index === 0"
prepend-icon="mdi-arrow-up-bold"
@click="moveStudent(index, 'top')" @click="moveStudent(index, 'top')"
> >
置顶 置顶
</v-list-item> </v-list-item>
<v-divider /> <v-divider/>
<v-list-item <v-list-item
prepend-icon="mdi-arrow-up"
:disabled="index === 0" :disabled="index === 0"
prepend-icon="mdi-arrow-up"
@click="moveStudent(index, 'up')" @click="moveStudent(index, 'up')"
> >
上移 上移
</v-list-item> </v-list-item>
<v-list-item <v-list-item
prepend-icon="mdi-arrow-down"
:disabled="index === modelValue.list.length - 1" :disabled="index === modelValue.list.length - 1"
prepend-icon="mdi-arrow-down"
@click="moveStudent(index, 'down')" @click="moveStudent(index, 'down')"
> >
下移 下移
@ -129,13 +129,13 @@
<v-text-field <v-text-field
v-if="editState.index === index" v-if="editState.index === index"
v-model="editState.name" v-model="editState.name"
density="compact"
variant="underlined"
hide-details
class="flex-grow-1"
autofocus autofocus
@keyup.enter="saveEdit" class="flex-grow-1"
density="compact"
hide-details
variant="underlined"
@blur="saveEdit" @blur="saveEdit"
@keyup.enter="saveEdit"
/> />
<span <span
v-else v-else
@ -146,21 +146,21 @@
</span> </span>
<div <div
class="d-flex gap-1 action-buttons"
:class="{ 'opacity-100': isHovering || isMobile }" :class="{ 'opacity-100': isHovering || isMobile }"
class="d-flex gap-1 action-buttons"
> >
<v-btn <v-btn
icon="mdi-pencil"
variant="text"
color="primary" color="primary"
icon="mdi-pencil"
size="small" size="small"
variant="text"
@click="startEdit(index, student)" @click="startEdit(index, student)"
/> />
<v-btn <v-btn
icon="mdi-delete"
variant="text"
color="error" color="error"
icon="mdi-delete"
size="small" size="small"
variant="text"
@click="removeStudent(index)" @click="removeStudent(index)"
/> />
</div> </div>
@ -175,36 +175,36 @@
<div v-else class="pt-2"> <div v-else class="pt-2">
<v-textarea <v-textarea
v-model="modelValue.text" v-model="modelValue.text"
label="批量编辑学生列表"
placeholder="每行输入一个学生姓名"
hint="使用文本编辑模式批量编辑学生名单,保存时会自动去除空行" hint="使用文本编辑模式批量编辑学生名单,保存时会自动去除空行"
label="批量编辑学生列表"
persistent-hint persistent-hint
variant="outlined" placeholder="每行输入一个学生姓名"
rows="10" rows="10"
variant="outlined"
@update:model-value="handleTextInput" @update:model-value="handleTextInput"
/> />
</div> </div>
</v-expand-transition> </v-expand-transition>
<v-row class="mt-6"> <v-row class="mt-6">
<v-col cols="12" class="d-flex gap-2"> <v-col class="d-flex gap-2" cols="12">
<v-btn <v-btn
:disabled="loading"
:loading="loading"
color="primary" color="primary"
prepend-icon="mdi-content-save" prepend-icon="mdi-content-save"
size="large" size="large"
:loading="loading"
:disabled="loading"
@click="saveStudents" @click="saveStudents"
> >
保存名单 保存名单
</v-btn> </v-btn>
<v-btn <v-btn
:disabled="loading"
:loading="loading"
color="error" color="error"
variant="outlined"
prepend-icon="mdi-refresh" prepend-icon="mdi-refresh"
size="large" size="large"
:loading="loading" variant="outlined"
:disabled="loading"
@click="loadStudents" @click="loadStudents"
> >
重载名单 重载名单
@ -218,9 +218,9 @@
<script> <script>
import UnsavedWarning from "../common/UnsavedWarning.vue"; import UnsavedWarning from "../common/UnsavedWarning.vue";
import "@/styles/warnings.scss"; import "@/styles/warnings.scss";
import { pinyin } from "pinyin-pro"; import {pinyin} from "pinyin-pro";
import dataProvider from "@/utils/dataProvider"; import dataProvider from "@/utils/dataProvider";
import { getSetting } from "@/utils/settings"; import {getSetting} from "@/utils/settings";
export default { export default {
name: "StudentListCard", name: "StudentListCard",
@ -289,7 +289,7 @@ export default {
if (response.success != false && Array.isArray(response)) { if (response.success != false && Array.isArray(response)) {
this.modelValue.list = response.map((item, index) => { this.modelValue.list = response.map((item, index) => {
if (typeof item === 'string') { if (typeof item === 'string') {
return { id: index + 1, name: item }; return {id: index + 1, name: item};
} }
return { return {
id: item.id || index + 1, id: item.id || index + 1,
@ -301,7 +301,7 @@ export default {
this.modelValue.text = this.modelValue.list.map(s => s.name).join("\n"); this.modelValue.text = this.modelValue.list.map(s => s.name).join("\n");
this.lastSavedData = JSON.parse(JSON.stringify(this.modelValue.list)); this.lastSavedData = JSON.parse(JSON.stringify(this.modelValue.list));
this.unsavedChanges = false; this.unsavedChanges = false;
return;
} }
} catch (error) { } catch (error) {
console.warn( console.warn(
@ -371,9 +371,9 @@ export default {
const newList = lines.map(name => { const newList = lines.map(name => {
name = name.trim(); name = name.trim();
if (currentIds.has(name)) { if (currentIds.has(name)) {
return { id: currentIds.get(name), name }; return {id: currentIds.get(name), name};
} }
return { id: ++maxId, name }; return {id: ++maxId, name};
}); });
// Update the list // Update the list
@ -384,7 +384,7 @@ export default {
const name = this.newStudentName.trim(); const name = this.newStudentName.trim();
if (name && !this.modelValue.list.some(s => s.name === name)) { if (name && !this.modelValue.list.some(s => s.name === name)) {
const maxId = Math.max(0, ...this.modelValue.list.map(s => s.id)); const maxId = Math.max(0, ...this.modelValue.list.map(s => s.id));
this.modelValue.list.push({ id: maxId + 1, name }); this.modelValue.list.push({id: maxId + 1, name});
this.newStudentName = ""; this.newStudentName = "";
} }
}, },
@ -442,8 +442,8 @@ export default {
sortStudentsByPinyin() { sortStudentsByPinyin() {
const sorted = [...this.modelValue.list].sort((a, b) => { const sorted = [...this.modelValue.list].sort((a, b) => {
const pinyinA = pinyin(a.name, { toneType: "none" }); const pinyinA = pinyin(a.name, {toneType: "none"});
const pinyinB = pinyin(b.name, { toneType: "none" }); const pinyinB = pinyin(b.name, {toneType: "none"});
return pinyinA.localeCompare(pinyinB); return pinyinA.localeCompare(pinyinB);
}); });
sorted.forEach((s, i) => s.id = i + 1); sorted.forEach((s, i) => s.id = i + 1);

View File

@ -1,7 +1,7 @@
<template> <template>
<v-card class="my-4" :loading="loading" :disabled="!hasNamespaceInfo"> <v-card :disabled="!hasNamespaceInfo" :loading="loading" class="my-4">
<template #loader> <template #loader>
<v-progress-linear v-if="loading" indeterminate color="primary" /> <v-progress-linear v-if="loading" color="primary" indeterminate/>
</template> </template>
@ -21,18 +21,18 @@
class="mb-4" class="mb-4"
> >
<v-alert <v-alert
border
type="warning" type="warning"
variant="tonal" variant="tonal"
border
> >
<v-alert-title>设备未绑定账号</v-alert-title> <v-alert-title>设备未绑定账号</v-alert-title>
<div>当前设备尚未绑定账号,部分功能可能受限请前往绑定账号以获得完整体验</div> <div>当前设备尚未绑定账号,部分功能可能受限请前往绑定账号以获得完整体验</div>
<v-btn <v-btn
class="mt-3"
variant="outlined"
:href="getBindAccountUrl()" :href="getBindAccountUrl()"
append-icon="mdi-open-in-new" append-icon="mdi-open-in-new"
class="mt-3"
target="_blank" target="_blank"
variant="outlined"
> >
前往绑定账号 前往绑定账号
</v-btn> </v-btn>
@ -45,13 +45,13 @@
class="d-flex align-center mb-4" class="d-flex align-center mb-4"
> >
<v-card <v-card
border
hover
class="w-100"
variant="tonal"
:prepend-avatar="namespaceInfo.account.avatarUrl" :prepend-avatar="namespaceInfo.account.avatarUrl"
:title="namespaceInfo.account.name || '未命名用户'"
:subtitle="'此设备由贵校管理 管理员账号 ID: ' + namespaceInfo.account.id" :subtitle="'此设备由贵校管理 管理员账号 ID: ' + namespaceInfo.account.id"
:title="namespaceInfo.account.name || '未命名用户'"
border
class="w-100"
hover
variant="tonal"
> >
<v-card-text> <v-card-text>
此设备由贵校或贵单位管理该管理员系此空间所有者如有疑问请咨询他对于恶意绑定滥用行为请反馈 此设备由贵校或贵单位管理该管理员系此空间所有者如有疑问请咨询他对于恶意绑定滥用行为请反馈
@ -62,10 +62,10 @@
<!-- 设备信息卡片 --> <!-- 设备信息卡片 -->
<v-card <v-card
v-if="namespaceInfo.device" v-if="namespaceInfo.device"
variant="tonal"
class="mb-4"
border border
class="mb-4"
hover hover
variant="tonal"
> >
<v-card-title class="pb-1"> <v-card-title class="pb-1">
设备信息 设备信息
@ -74,8 +74,8 @@
<div class="d-flex flex-column gap-1"> <div class="d-flex flex-column gap-1">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon <v-icon
size="small"
class="me-2" class="me-2"
size="small"
> >
mdi-tag mdi-tag
</v-icon> </v-icon>
@ -84,8 +84,8 @@
</div> </div>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon <v-icon
size="small"
class="me-2" class="me-2"
size="small"
> >
mdi-identifier mdi-identifier
</v-icon> </v-icon>
@ -98,8 +98,8 @@
class="d-flex align-center" class="d-flex align-center"
> >
<v-icon <v-icon
size="small"
class="me-2" class="me-2"
size="small"
> >
mdi-uuid mdi-uuid
</v-icon> </v-icon>
@ -108,8 +108,8 @@
</div> </div>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon <v-icon
size="small"
class="me-2" class="me-2"
size="small"
> >
mdi-calendar mdi-calendar
</v-icon> </v-icon>
@ -121,8 +121,8 @@
class="d-flex align-center" class="d-flex align-center"
> >
<v-icon <v-icon
size="small"
class="me-2" class="me-2"
size="small"
> >
mdi-calendar-clock mdi-calendar-clock
</v-icon> </v-icon>
@ -134,13 +134,14 @@
</v-card> </v-card>
<v-card <v-card
title="Classworks KV"
subtitle="文档形键值数据库"
border border
hover hover
subtitle="文档形键值数据库"
title="Classworks KV"
> >
<v-card-text> <v-card-text>
Classworks KV 是厚浪云推出的文档形键值数据库其是一个开放的云应用平台为各种应用提供存储服务此设备正在使用其服务如果您希望管理设备信息请前往 Classworks KV 的网站如果您在服务推出前就在使用 Classworks您的数据已被自动迁移 Classworks KV 是厚浪云推出的文档形键值数据库其是一个开放的云应用平台为各种应用提供存储服务此设备正在使用其服务如果您希望管理设备信息请前往
Classworks KV 的网站如果您在服务推出前就在使用 Classworks您的数据已被自动迁移
<br><br> <br><br>
Classworks KV 的全域管理员是 Classworks KV 的全域管理员是
<a <a
@ -153,8 +154,8 @@
<v-card-actions> <v-card-actions>
<v-btn <v-btn
:href="defaultAuthServer" :href="defaultAuthServer"
class="text-none"
append-icon="mdi-open-in-new" append-icon="mdi-open-in-new"
class="text-none"
target="_blank" target="_blank"
> >
前往 Classworks KV 前往 Classworks KV
@ -174,11 +175,11 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
:loading="loading"
color="primary" color="primary"
variant="outlined" variant="outlined"
:loading="loading"
@click="reloadInfo" @click="reloadInfo"
> >
刷新设备信息 刷新设备信息
@ -202,9 +203,9 @@
<v-card-title>确认重新初始化</v-card-title> <v-card-title>确认重新初始化</v-card-title>
<v-card-text> <v-card-text>
<v-alert <v-alert
class="mb-3"
type="warning" type="warning"
variant="tonal" variant="tonal"
class="mb-3"
> >
<v-alert-title>警告</v-alert-title> <v-alert-title>警告</v-alert-title>
此操作将清除当前的云端存储配置包括 Token您需要重新进行授权 此操作将清除当前的云端存储配置包括 Token您需要重新进行授权
@ -212,7 +213,7 @@
<p>您确定要重新初始化云端存储吗</p> <p>您确定要重新初始化云端存储吗</p>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
variant="text" variant="text"
@click="showReinitDialog = false" @click="showReinitDialog = false"
@ -232,8 +233,8 @@
</template> </template>
<script> <script>
import { kvServerProvider } from "@/utils/providers/kvServerProvider"; import {kvServerProvider} from "@/utils/providers/kvServerProvider";
import { setSetting, getSetting } from "@/utils/settings"; import {setSetting, getSetting} from "@/utils/settings";
export default { export default {
name: "CloudNamespaceInfoCard", name: "CloudNamespaceInfoCard",

View File

@ -1,5 +1,5 @@
<template> <template>
<settings-card title="数据源设置" icon="mdi-database-cog"> <settings-card icon="mdi-database-cog" title="数据源设置">
<v-list> <v-list>
<!-- 服务器模式设置 --> <!-- 服务器模式设置 -->
<template <template
@ -10,7 +10,7 @@
> >
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-lan-connect" class="mr-3" /> <v-icon class="mr-3" icon="mdi-lan-connect"/>
</template> </template>
<v-list-item-title>检查服务器连接</v-list-item-title> <v-list-item-title>检查服务器连接</v-list-item-title>
<template #append> <template #append>
@ -21,7 +21,8 @@
> >
测试连接 测试连接
</v-btn> </v-btn>
</template> </v-list-item </template>
</v-list-item
><!-- 数据迁移仅对KV本地存储有效 --> ><!-- 数据迁移仅对KV本地存储有效 -->
</template> </template>
@ -29,11 +30,12 @@
<template v-if="currentProvider === 'kv-local'"> <template v-if="currentProvider === 'kv-local'">
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-database" class="mr-3" /> <v-icon class="mr-3" icon="mdi-database"/>
</template> </template>
<v-list-item-title>清除数据库缓存</v-list-item-title> <v-list-item-title>清除数据库缓存</v-list-item-title>
<v-list-item-subtitle <v-list-item-subtitle
>这将清除所有本地数据库中的数据</v-list-item-subtitle >这将清除所有本地数据库中的数据
</v-list-item-subtitle
> >
<template #append> <template #append>
<v-btn color="error" variant="tonal" @click="confirmClearIndexedDB"> <v-btn color="error" variant="tonal" @click="confirmClearIndexedDB">
@ -43,45 +45,48 @@
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-database-export" class="mr-3" /> <v-icon class="mr-3" icon="mdi-database-export"/>
</template> </template>
<v-list-item-title>导出数据库</v-list-item-title> <v-list-item-title>导出数据库</v-list-item-title>
<template #append> <template #append>
<v-btn variant="tonal" @click="exportData"> 导出 </v-btn> <v-btn variant="tonal" @click="exportData"> 导出</v-btn>
</template> </template>
</v-list-item> </v-list-item>
</template> </template>
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-database-import" class="mr-3" /> <v-icon class="mr-3" icon="mdi-database-import"/>
</template> </template>
<v-list-item-title>迁移旧数据</v-list-item-title> <v-list-item-title>迁移旧数据</v-list-item-title>
<v-list-item-subtitle <v-list-item-subtitle
>将旧的存储格式数据转移到新的KV存储</v-list-item-subtitle >将旧的存储格式数据转移到新的KV存储
</v-list-item-subtitle
> >
<template #append> <template #append>
<v-btn :loading="migrateLoading" variant="tonal" @click="migrateData"> <v-btn :loading="migrateLoading" variant="tonal" @click="migrateData">
迁移 迁移
</v-btn> </v-btn>
</template> </v-list-item </template>
</v-list-item
><!-- 显示机器ID --> ><!-- 显示机器ID -->
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-identifier" class="mr-3" /> <v-icon class="mr-3" icon="mdi-identifier"/>
</template> </template>
<v-list-item-title>本机唯一标识符</v-list-item-title> <v-list-item-title>本机唯一标识符</v-list-item-title>
<v-list-item-subtitle v-if="machineId">{{ <v-list-item-subtitle v-if="machineId">{{
machineId machineId
}}</v-list-item-subtitle> }}
</v-list-item-subtitle>
<v-list-item-subtitle v-else>正在加载...</v-list-item-subtitle> <v-list-item-subtitle v-else>正在加载...</v-list-item-subtitle>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-lan-connect" class="mr-3" /> <v-icon class="mr-3" icon="mdi-lan-connect"/>
</template> </template>
<v-list-item-title>查看本地缓存</v-list-item-title> <v-list-item-title>查看本地缓存</v-list-item-title>
<template #append> <template #append>
<v-btn variant="tonal" to="/cachemanagement"> 查看 </v-btn> <v-btn to="/cachemanagement" variant="tonal"> 查看</v-btn>
</template> </template>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -94,10 +99,12 @@
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="confirmDialog = false" <v-btn color="grey" variant="text" @click="confirmDialog = false"
>取消</v-btn >取消
</v-btn
> >
<v-btn color="error" variant="tonal" @click="handleConfirm" <v-btn color="error" variant="tonal" @click="handleConfirm"
>确认</v-btn >确认
</v-btn
> >
</v-card-actions> </v-card-actions>
</v-card> </v-card>
@ -107,12 +114,12 @@
<script> <script>
import SettingsCard from "@/components/SettingsCard.vue"; import SettingsCard from "@/components/SettingsCard.vue";
import { getSetting } from "@/utils/settings"; import {getSetting} from "@/utils/settings";
import axios from "axios"; import axios from "axios";
export default { export default {
name: "DataProviderSettingsCard", name: "DataProviderSettingsCard",
components: { SettingsCard }, components: {SettingsCard},
data() { data() {
return { return {
@ -156,7 +163,7 @@ export default {
const siteKey = getSetting("server.siteKey"); const siteKey = getSetting("server.siteKey");
// Prepare headers including site key if available // Prepare headers including site key if available
const headers = { Accept: "application/json" }; const headers = {Accept: "application/json"};
if (siteKey) { if (siteKey) {
headers["x-site-key"] = siteKey; headers["x-site-key"] = siteKey;
} }
@ -227,7 +234,7 @@ export default {
async exportData() { async exportData() {
try { try {
const DBName = "ClassworksDB"; const DBName = "ClassworksDB";
const data = { indexedDB: {} }; const data = {indexedDB: {}};
// //
const db = await new Promise((resolve, reject) => { const db = await new Promise((resolve, reject) => {

View File

@ -1,50 +1,49 @@
<template> <template>
<settings-card title="显示设置" icon="mdi-monitor" border> <settings-card border icon="mdi-monitor" title="显示设置">
<v-list> <v-list>
<setting-item :setting-key="'display.emptySubjectDisplay'" /> <setting-item :setting-key="'display.emptySubjectDisplay'"/>
<v-divider class="my-2" />
<setting-item :setting-key="'display.dynamicSort'" />
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'display.showRandomButton'" /> <setting-item :setting-key="'display.dynamicSort'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'display.showFullscreenButton'" /> <setting-item :setting-key="'display.showRandomButton'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'display.cardHoverEffect'" /> <setting-item :setting-key="'display.showFullscreenButton'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'display.enhancedTouchMode'" /> <setting-item :setting-key="'display.cardHoverEffect'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'display.showQuickTools'" /> <setting-item :setting-key="'display.enhancedTouchMode'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'display.showAntiScreenBurnCard'" /> <setting-item :setting-key="'display.showQuickTools'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'display.showExamScheduleButton'" /> <setting-item :setting-key="'display.showAntiScreenBurnCard'"/>
</v-list> <v-divider class="my-2"/>
<setting-item :setting-key="'display.showExamScheduleButton'"/>
</v-list>
</settings-card> </settings-card>
</template> </template>
<script> <script>
import SettingsCard from '@/components/SettingsCard.vue'; import SettingsCard from '@/components/SettingsCard.vue';
import SettingItem from '@/components/settings/SettingItem.vue'; import SettingItem from '@/components/settings/SettingItem.vue';
export default { export default {
name: 'DisplaySettingsCard', name: 'DisplaySettingsCard',
components: { SettingsCard, SettingItem }, components: {SettingsCard, SettingItem},
data() { data() {
return { return {};
};
}, },
}; };
</script> </script>

View File

@ -1,8 +1,8 @@
<template> <template>
<settings-card <settings-card
border border
title="回声洞"
icon="mdi-thought-bubble" icon="mdi-thought-bubble"
title="回声洞"
@click="handleClick" @click="handleClick"
> >
<v-card-text> <v-card-text>
@ -12,7 +12,7 @@
<transition name="fade"> <transition name="fade">
<v-chip v-if="currentQuote?.contributor" class="contributor"> <v-chip v-if="currentQuote?.contributor" class="contributor">
<v-avatar start> <v-avatar start>
<v-img :src="`https://github.com/${currentQuote.contributor}.png`" /> <v-img :src="`https://github.com/${currentQuote.contributor}.png`"/>
</v-avatar> </v-avatar>
{{ currentQuote.contributor }} {{ currentQuote.contributor }}
</v-chip> </v-chip>
@ -31,13 +31,13 @@ const INITIAL_STATE = {
}; };
const TYPEWRITER_CONFIG = { const TYPEWRITER_CONFIG = {
main: { delay: 50, deleteSpeed: 100 }, main: {delay: 50, deleteSpeed: 100},
source: { delay: 10, deleteSpeed: 10, cursor: "" } source: {delay: 10, deleteSpeed: 10, cursor: ""}
}; };
export default { export default {
name: "EchoChamberCard", name: "EchoChamberCard",
components: { SettingsCard }, components: {SettingsCard},
data: () => ({ data: () => ({
typewriter: null, typewriter: null,
sourceWriter: null, sourceWriter: null,
@ -79,7 +79,7 @@ export default {
async copyToClipboard() { async copyToClipboard() {
if (!this.currentQuote) return; if (!this.currentQuote) return;
const { text, author, contributor, link } = this.currentQuote; const {text, author, contributor, link} = this.currentQuote;
const parts = [ const parts = [
text, text,
author && `作者:${author}`, author && `作者:${author}`,
@ -107,6 +107,11 @@ export default {
font-size: 0.9em; font-size: 0.9em;
} }
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-active, .fade-leave-active {
.fade-enter-from, .fade-leave-to { opacity: 0; } transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style> </style>

View File

@ -1,20 +1,19 @@
<template> <template>
<settings-card title="编辑设置" icon="mdi-cog"> <settings-card icon="mdi-cog" title="编辑设置">
<v-list> <v-list>
<setting-item :setting-key="'edit.autoSave'" /> <setting-item :setting-key="'edit.autoSave'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'edit.blockNonTodayAutoSave'" /> <setting-item :setting-key="'edit.blockNonTodayAutoSave'"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item :setting-key="'edit.confirmNonTodaySave'" /> <setting-item :setting-key="'edit.confirmNonTodaySave'"/>
<v-divider class="my-2"/>
<v-divider class="my-2" /> <setting-item :setting-key="'edit.refreshBeforeEdit'"/>
<setting-item :setting-key="'edit.refreshBeforeEdit'" />
</v-list> </v-list>
</settings-card> </settings-card>

View File

@ -1,17 +1,17 @@
<template> <template>
<settings-card <settings-card
title="作业模板配置"
icon="mdi-book-edit"
:loading="loading" :loading="loading"
border border
icon="mdi-book-edit"
title="作业模板配置"
> >
<!-- 顶部操作按钮 --> <!-- 顶部操作按钮 -->
<v-alert <v-alert
v-if="error" v-if="error"
class="mb-4"
closable
type="error" type="error"
variant="tonal" variant="tonal"
closable
class="mb-4"
> >
{{ error }} {{ error }}
</v-alert> </v-alert>
@ -19,20 +19,20 @@
<div class="d-flex justify-space-between align-center mb-6"> <div class="d-flex justify-space-between align-center mb-6">
<div> <div>
<v-btn <v-btn
color="primary"
size="large"
prepend-icon="mdi-refresh"
:loading="loading" :loading="loading"
@click="loadConfig"
class="mr-2" class="mr-2"
color="primary"
prepend-icon="mdi-refresh"
size="large"
@click="loadConfig"
> >
重新加载配置 重新加载配置
</v-btn> </v-btn>
<v-btn <v-btn
color="success"
size="large"
prepend-icon="mdi-content-save"
:loading="loading" :loading="loading"
color="success"
prepend-icon="mdi-content-save"
size="large"
@click="saveConfig" @click="saveConfig"
> >
保存所有更改 保存所有更改
@ -49,15 +49,15 @@
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<setting-group title="科目配置" icon="mdi-book" border> <setting-group border icon="mdi-book" title="科目配置">
<v-list> <v-list>
<v-list-item> <v-list-item>
<v-text-field <v-text-field
v-model="newSubject" v-model="newSubject"
append-inner-icon="mdi-plus"
density="comfortable"
label="添加新科目" label="添加新科目"
variant="outlined" variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addSubject" @click:append-inner="addSubject"
@keyup.enter="addSubject" @keyup.enter="addSubject"
/> />
@ -70,32 +70,32 @@
v-model="editedSubjects[subject]" v-model="editedSubjects[subject]"
:placeholder="subject" :placeholder="subject"
density="comfortable" density="comfortable"
variant="plain"
hide-details hide-details
variant="plain"
@blur="updateSubject(subject)" @blur="updateSubject(subject)"
/> />
<v-spacer /> <v-spacer/>
<v-btn <v-btn
icon="mdi-delete"
variant="text"
color="error" color="error"
icon="mdi-delete"
size="small" size="small"
variant="text"
@click="deleteSubject(subject)" @click="deleteSubject(subject)"
/> />
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="newBookTypes[subject]" v-model="newBookTypes[subject]"
append-inner-icon="mdi-plus"
class="mb-2"
density="comfortable"
label="添加作业本名称" label="添加作业本名称"
variant="outlined" variant="outlined"
density="comfortable"
class="mb-2"
append-inner-icon="mdi-plus"
@click:append-inner="() => addBookType(subject)" @click:append-inner="() => addBookType(subject)"
@keyup.enter="() => addBookType(subject)" @keyup.enter="() => addBookType(subject)"
/> />
<v-list density="compact" border rounded> <v-list border density="compact" rounded>
<v-list-item <v-list-item
v-for="(books, bookType) in config.subjects[subject].books" v-for="(books, bookType) in config.subjects[subject].books"
:key="bookType" :key="bookType"
@ -103,21 +103,21 @@
@click="openSubjectBookDialog(subject, bookType, books)" @click="openSubjectBookDialog(subject, bookType, books)"
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-book-open-variant" class="mr-2" /> <v-icon class="mr-2" icon="mdi-book-open-variant"/>
</template> </template>
<template v-slot:append> <template v-slot:append>
<v-chip <v-chip
size="small"
class="mr-2" class="mr-2"
color="info" color="info"
size="small"
> >
{{ books.length }}个部分 {{ books.length }}个部分
</v-chip> </v-chip>
<v-btn <v-btn
icon="mdi-delete"
variant="text"
color="error" color="error"
icon="mdi-delete"
size="small" size="small"
variant="text"
@click.stop="() => deleteBookType(subject, bookType)" @click.stop="() => deleteBookType(subject, bookType)"
/> />
</template> </template>
@ -131,22 +131,22 @@
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<setting-group title="通用配置" icon="mdi-cog" border> <setting-group border icon="mdi-cog" title="通用配置">
<v-list> <v-list>
<v-list-item> <v-list-item>
<v-text-field <v-text-field
v-model="newCommonBook" v-model="newCommonBook"
append-inner-icon="mdi-plus"
density="comfortable"
label="添加作业本名称" label="添加作业本名称"
variant="outlined" variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addCommonBook" @click:append-inner="addCommonBook"
@keyup.enter="addCommonBook" @keyup.enter="addCommonBook"
/> />
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-list density="compact" border rounded> <v-list border density="compact" rounded>
<v-list-item <v-list-item
v-for="(books, bookType) in config.commonSubject.books" v-for="(books, bookType) in config.commonSubject.books"
:key="bookType" :key="bookType"
@ -154,21 +154,21 @@
@click="openSubjectBookDialog('common', bookType, books)" @click="openSubjectBookDialog('common', bookType, books)"
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-book-multiple" class="mr-2" /> <v-icon class="mr-2" icon="mdi-book-multiple"/>
</template> </template>
<template v-slot:append> <template v-slot:append>
<v-chip <v-chip
size="small"
class="mr-2" class="mr-2"
color="info" color="info"
size="small"
> >
{{ books.length }}个部分 {{ books.length }}个部分
</v-chip> </v-chip>
<v-btn <v-btn
icon="mdi-delete"
variant="text"
color="error" color="error"
icon="mdi-delete"
size="small" size="small"
variant="text"
@click.stop="() => deleteBookType('common', bookType)" @click.stop="() => deleteBookType('common', bookType)"
/> />
</template> </template>
@ -176,22 +176,22 @@
</v-list> </v-list>
</v-list-item> </v-list-item>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<v-list-item> <v-list-item>
<v-text-field <v-text-field
v-model="newAction" v-model="newAction"
append-inner-icon="mdi-plus"
density="comfortable"
label="添加操作" label="添加操作"
variant="outlined" variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addAction" @click:append-inner="addAction"
@keyup.enter="addAction" @keyup.enter="addAction"
/> />
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-list density="compact" border rounded> <v-list border density="compact" rounded>
<v-list-item <v-list-item
v-for="action in config.actions" v-for="action in config.actions"
:key="action" :key="action"
@ -200,10 +200,10 @@
> >
<template v-slot:append> <template v-slot:append>
<v-btn <v-btn
icon="mdi-delete"
variant="text"
color="error" color="error"
icon="mdi-delete"
size="small" size="small"
variant="text"
@click.stop="removeAction(action)" @click.stop="removeAction(action)"
/> />
</template> </template>
@ -229,41 +229,41 @@
<v-text-field <v-text-field
v-model="dialog.editedItem.name" v-model="dialog.editedItem.name"
:label="dialog.nameLabel" :label="dialog.nameLabel"
variant="outlined"
density="comfortable"
:rules="[v => !!v || '名称不能为空']" :rules="[v => !!v || '名称不能为空']"
density="comfortable"
variant="outlined"
/> />
</v-col> </v-col>
<v-col cols="12" v-if="dialog.editedItem.type === 'subjectBook'"> <v-col v-if="dialog.editedItem.type === 'subjectBook'" cols="12">
<div class="text-subtitle-2 mb-2">所属科目</div> <div class="text-subtitle-2 mb-2">所属科目</div>
<v-chip color="primary">{{ dialog.editedItem.subject }}</v-chip> <v-chip color="primary">{{ dialog.editedItem.subject }}</v-chip>
</v-col> </v-col>
<v-col cols="12" v-if="['subjectBook', 'commonBook'].includes(dialog.editedItem.type)"> <v-col v-if="['subjectBook', 'commonBook'].includes(dialog.editedItem.type)" cols="12">
<v-card variant="outlined"> <v-card variant="outlined">
<v-card-title class="text-subtitle-1 py-2">需完成部分</v-card-title> <v-card-title class="text-subtitle-1 py-2">需完成部分</v-card-title>
<v-card-text class="pt-0"> <v-card-text class="pt-0">
<v-list density="compact" border rounded class="mb-2"> <v-list border class="mb-2" density="compact" rounded>
<v-list-item <v-list-item
v-for="(task, index) in dialog.editedItem.tasks" v-for="(task, index) in dialog.editedItem.tasks"
:key="index" :key="index"
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-icon icon="mdi-checkbox-blank-circle-outline" size="small" class="mr-2" /> <v-icon class="mr-2" icon="mdi-checkbox-blank-circle-outline" size="small"/>
</template> </template>
<v-text-field <v-text-field
v-model="dialog.editedItem.tasks[index]" v-model="dialog.editedItem.tasks[index]"
variant="plain"
density="compact" density="compact"
hide-details hide-details
variant="plain"
/> />
<template v-slot:append> <template v-slot:append>
<v-btn <v-btn
icon="mdi-delete"
variant="text"
color="error" color="error"
icon="mdi-delete"
size="small" size="small"
variant="text"
@click="removeTask(index)" @click="removeTask(index)"
/> />
</template> </template>
@ -271,13 +271,13 @@
</v-list> </v-list>
<v-text-field <v-text-field
v-model="newTask" v-model="newTask"
append-inner-icon="mdi-plus"
class="mt-2"
density="comfortable"
label="添加需完成部分" label="添加需完成部分"
variant="outlined" variant="outlined"
density="comfortable"
append-inner-icon="mdi-plus"
@click:append-inner="addTask" @click:append-inner="addTask"
@keyup.enter="addTask" @keyup.enter="addTask"
class="mt-2"
/> />
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -318,7 +318,7 @@
</template> </template>
<script> <script>
import { reactive } from 'vue'; import {reactive} from 'vue';
import SettingsCard from '@/components/SettingsCard.vue'; import SettingsCard from '@/components/SettingsCard.vue';
import SettingGroup from '@/components/settings/SettingGroup.vue'; import SettingGroup from '@/components/settings/SettingGroup.vue';
import dataProvider from "@/utils/dataProvider.js"; import dataProvider from "@/utils/dataProvider.js";
@ -466,7 +466,7 @@ export default {
addSubject() { addSubject() {
if (!this.newSubject) return; if (!this.newSubject) return;
if (!this.config.subjects[this.newSubject]) { if (!this.config.subjects[this.newSubject]) {
this.config.subjects[this.newSubject] = { books: {} }; this.config.subjects[this.newSubject] = {books: {}};
} }
this.newSubject = ''; this.newSubject = '';
}, },
@ -627,7 +627,7 @@ export default {
}, },
saveDialog() { saveDialog() {
const { type, name, subject, originalName, tasks } = this.dialog.editedItem; const {type, name, subject, originalName, tasks} = this.dialog.editedItem;
if (!name) { if (!name) {
this.showMessage('名称不能为空', 'error'); this.showMessage('名称不能为空', 'error');
@ -687,4 +687,4 @@ export default {
.v-card-text { .v-card-text {
padding-top: 0; padding-top: 0;
} }
</style> </style>

View File

@ -1,36 +1,36 @@
<template> <template>
<settings-card title="KV数据库管理" icon="mdi-database-edit" :loading="loading"> <settings-card :loading="loading" icon="mdi-database-edit" title="KV数据库管理">
<v-list> <v-list>
<!-- 数据库连接状态 --> <!-- 数据库连接状态 -->
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon :icon="connectionIcon" :color="connectionColor" class="mr-3" /> <v-icon :color="connectionColor" :icon="connectionIcon" class="mr-3"/>
</template> </template>
<v-list-item-title>数据库状态</v-list-item-title> <v-list-item-title>数据库状态</v-list-item-title>
<v-list-item-subtitle>{{ connectionStatus }}</v-list-item-subtitle> <v-list-item-subtitle>{{ connectionStatus }}</v-list-item-subtitle>
<template #append> <template #append>
<v-btn variant="tonal" @click="refreshConnection" :loading="loading"> <v-btn :loading="loading" variant="tonal" @click="refreshConnection">
刷新 刷新
</v-btn> </v-btn>
</template> </template>
</v-list-item> </v-list-item>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<!-- 数据列表 --> <!-- 数据列表 -->
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-format-list-bulleted" class="mr-3" /> <v-icon class="mr-3" icon="mdi-format-list-bulleted"/>
</template> </template>
<v-list-item-title>数据条目</v-list-item-title> <v-list-item-title>数据条目</v-list-item-title>
<v-list-item-subtitle> {{ kvData.length }} 条记录</v-list-item-subtitle> <v-list-item-subtitle> {{ kvData.length }} 条记录</v-list-item-subtitle>
<template #append> <template #append>
<v-btn-group variant="tonal"> <v-btn-group variant="tonal">
<v-btn @click="loadKvData" :loading="loadingData"> <v-btn :loading="loadingData" @click="loadKvData">
加载数据 加载数据
</v-btn> </v-btn>
<v-btn @click="createNewItem" :disabled="!isKvProvider"> <v-btn :disabled="!isKvProvider" @click="createNewItem">
<v-icon icon="mdi-plus" class="mr-1" /> <v-icon class="mr-1" icon="mdi-plus"/>
新建 新建
</v-btn> </v-btn>
</v-btn-group> </v-btn-group>
@ -41,60 +41,60 @@
<!-- 数据表格 --> <!-- 数据表格 -->
<v-card v-if="kvData.length > 0" class="mt-4" variant="outlined"> <v-card v-if="kvData.length > 0" class="mt-4" variant="outlined">
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon icon="mdi-table" class="mr-2" /> <v-icon class="mr-2" icon="mdi-table"/>
KV数据列表 KV数据列表
<v-spacer /> <v-spacer/>
<v-text-field <v-text-field
v-model="searchQuery" v-model="searchQuery"
label="搜索键名" clearable
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="compact" density="compact"
hide-details hide-details
clearable label="搜索键名"
prepend-inner-icon="mdi-magnify"
style="max-width: 300px;" style="max-width: 300px;"
variant="outlined"
/> />
</v-card-title> </v-card-title>
<v-data-table <v-data-table
:headers="tableHeaders" :headers="tableHeaders"
:items="filteredKvData" :items="filteredKvData"
:loading="loadingData"
item-value="key"
class="elevation-0"
:items-per-page="10" :items-per-page="10"
:loading="loadingData"
class="elevation-0"
item-value="key"
> >
<template #[`item.key`]="{ item }"> <template #[`item.key`]="{ item }">
<code class="text-primary">{{ item.key }}</code> <code class="text-primary">{{ item.key }}</code>
</template> </template>
<template #[`item.actions`]="{ item }"> <template #[`item.actions`]="{ item }">
<v-btn-group variant="text" density="compact"> <v-btn-group density="compact" variant="text">
<v-btn <v-btn
icon="mdi-eye" icon="mdi-eye"
size="small" size="small"
@click="viewItem(item)"
title="查看" title="查看"
@click="viewItem(item)"
/> />
<v-btn <v-btn
icon="mdi-pencil" icon="mdi-pencil"
size="small" size="small"
@click="editItem(item)"
title="编辑" title="编辑"
@click="editItem(item)"
/> />
<v-btn <v-btn
color="primary"
icon="mdi-cloud-download" icon="mdi-cloud-download"
size="small" size="small"
color="primary"
@click="getCloudUrl(item)"
title="获取云端地址" title="获取云端地址"
@click="getCloudUrl(item)"
/> />
<v-btn <v-btn
color="error"
icon="mdi-delete" icon="mdi-delete"
size="small" size="small"
color="error"
@click="confirmDelete(item)"
title="删除" title="删除"
@click="confirmDelete(item)"
/> />
</v-btn-group> </v-btn-group>
</template> </template>
@ -105,10 +105,10 @@
<v-dialog v-model="viewDialog" max-width="800px"> <v-dialog v-model="viewDialog" max-width="800px">
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon icon="mdi-eye" class="mr-2" /> <v-icon class="mr-2" icon="mdi-eye"/>
查看数据 查看数据
<v-spacer /> <v-spacer/>
<v-btn icon="mdi-close" variant="text" @click="viewDialog = false" /> <v-btn icon="mdi-close" variant="text" @click="viewDialog = false"/>
</v-card-title> </v-card-title>
<v-card-subtitle v-if="selectedItem"> <v-card-subtitle v-if="selectedItem">
@ -119,21 +119,21 @@
<v-textarea <v-textarea
v-if="selectedItem" v-if="selectedItem"
:model-value="formatJsonData(selectedItem.value)" :model-value="formatJsonData(selectedItem.value)"
class="font-monospace"
label="数据内容" label="数据内容"
variant="outlined"
readonly readonly
rows="15" rows="15"
class="font-monospace" variant="outlined"
/> />
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn @click="copyToClipboard(selectedItem?.value)" variant="tonal"> <v-btn variant="tonal" @click="copyToClipboard(selectedItem?.value)">
<v-icon icon="mdi-content-copy" class="mr-1" /> <v-icon class="mr-1" icon="mdi-content-copy"/>
复制数据 复制数据
</v-btn> </v-btn>
<v-btn @click="viewDialog = false" variant="text"> <v-btn variant="text" @click="viewDialog = false">
关闭 关闭
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -144,10 +144,10 @@
<v-dialog v-model="editDialog" max-width="800px"> <v-dialog v-model="editDialog" max-width="800px">
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon icon="mdi-pencil" class="mr-2" /> <v-icon class="mr-2" icon="mdi-pencil"/>
编辑数据 编辑数据
<v-spacer /> <v-spacer/>
<v-btn icon="mdi-close" variant="text" @click="closeEditDialog" /> <v-btn icon="mdi-close" variant="text" @click="closeEditDialog"/>
</v-card-title> </v-card-title>
<v-card-subtitle v-if="editingItem"> <v-card-subtitle v-if="editingItem">
@ -157,26 +157,26 @@
<v-card-text> <v-card-text>
<v-textarea <v-textarea
v-model="editingData" v-model="editingData"
label="数据内容 (JSON格式)"
variant="outlined"
rows="15"
class="font-monospace"
:error="!isValidJson" :error="!isValidJson"
:error-messages="isValidJson ? [] : ['请输入有效的JSON格式']" :error-messages="isValidJson ? [] : ['请输入有效的JSON格式']"
class="font-monospace"
label="数据内容 (JSON格式)"
rows="15"
variant="outlined"
/> />
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn @click="closeEditDialog" variant="text"> <v-btn variant="text" @click="closeEditDialog">
取消 取消
</v-btn> </v-btn>
<v-btn <v-btn
@click="saveEditedData"
variant="tonal"
color="primary"
:disabled="!isValidJson" :disabled="!isValidJson"
:loading="savingData" :loading="savingData"
color="primary"
variant="tonal"
@click="saveEditedData"
> >
保存 保存
</v-btn> </v-btn>
@ -188,46 +188,46 @@
<v-dialog v-model="createDialog" max-width="800px"> <v-dialog v-model="createDialog" max-width="800px">
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon icon="mdi-plus" class="mr-2" /> <v-icon class="mr-2" icon="mdi-plus"/>
新建数据 新建数据
<v-spacer /> <v-spacer/>
<v-btn icon="mdi-close" variant="text" @click="closeCreateDialog" /> <v-btn icon="mdi-close" variant="text" @click="closeCreateDialog"/>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="newKey" v-model="newKey"
label="键名"
variant="outlined"
class="mb-4"
:error="!isValidKey" :error="!isValidKey"
:error-messages="isValidKey ? [] : ['键名不能为空且不能与现有键重复']" :error-messages="isValidKey ? [] : ['键名不能为空且不能与现有键重复']"
class="mb-4"
label="键名"
placeholder="请输入键名my-config" placeholder="请输入键名my-config"
variant="outlined"
/> />
<v-textarea <v-textarea
v-model="newData" v-model="newData"
label="数据内容 (JSON格式)"
variant="outlined"
rows="15"
class="font-monospace"
:error="!isValidNewJson" :error="!isValidNewJson"
:error-messages="isValidNewJson ? [] : ['请输入有效的JSON格式']" :error-messages="isValidNewJson ? [] : ['请输入有效的JSON格式']"
class="font-monospace"
label="数据内容 (JSON格式)"
placeholder='请输入JSON数据{"name": "value"}' placeholder='请输入JSON数据{"name": "value"}'
rows="15"
variant="outlined"
/> />
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn @click="closeCreateDialog" variant="text"> <v-btn variant="text" @click="closeCreateDialog">
取消 取消
</v-btn> </v-btn>
<v-btn <v-btn
@click="saveNewData"
variant="tonal"
color="primary"
:disabled="!isValidKey || !isValidNewJson" :disabled="!isValidKey || !isValidNewJson"
:loading="savingData" :loading="savingData"
color="primary"
variant="tonal"
@click="saveNewData"
> >
创建 创建
</v-btn> </v-btn>
@ -239,10 +239,10 @@
<v-dialog v-model="cloudUrlDialog" max-width="800px"> <v-dialog v-model="cloudUrlDialog" max-width="800px">
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon icon="mdi-cloud-download" class="mr-2" /> <v-icon class="mr-2" icon="mdi-cloud-download"/>
获取云端访问地址 获取云端访问地址
<v-spacer /> <v-spacer/>
<v-btn icon="mdi-close" variant="text" @click="cloudUrlDialog = false" /> <v-btn icon="mdi-close" variant="text" @click="cloudUrlDialog = false"/>
</v-card-title> </v-card-title>
<v-card-subtitle v-if="selectedCloudItem"> <v-card-subtitle v-if="selectedCloudItem">
@ -250,19 +250,19 @@
</v-card-subtitle> </v-card-subtitle>
<v-card-text> <v-card-text>
<v-alert v-if="cloudUrlError" type="error" variant="tonal" class="mb-4"> <v-alert v-if="cloudUrlError" class="mb-4" type="error" variant="tonal">
{{ cloudUrlError }} {{ cloudUrlError }}
</v-alert> </v-alert>
<v-alert v-if="cloudUrlResult && cloudUrlResult.success" type="success" variant="tonal" class="mb-4"> <v-alert v-if="cloudUrlResult && cloudUrlResult.success" class="mb-4" type="success" variant="tonal">
<v-alert-title>云端地址获取成功</v-alert-title> <v-alert-title>云端地址获取成功</v-alert-title>
<div class="mt-2"> <div class="mt-2">
<div v-if="cloudUrlResult.migrated" class="mb-2"> <div v-if="cloudUrlResult.migrated" class="mb-2">
<v-icon icon="mdi-database-arrow-up" class="mr-1" color="success" /> <v-icon class="mr-1" color="success" icon="mdi-database-arrow-up"/>
数据已从本地迁移到云端 数据已从本地迁移到云端
</div> </div>
<div v-if="cloudUrlResult.configured" class="mb-2"> <div v-if="cloudUrlResult.configured" class="mb-2">
<v-icon icon="mdi-cog" class="mr-1" color="info" /> <v-icon class="mr-1" color="info" icon="mdi-cog"/>
云端配置已自动设置 云端配置已自动设置
</div> </div>
</div> </div>
@ -271,39 +271,39 @@
<v-text-field <v-text-field
v-if="cloudUrlResult && cloudUrlResult.url" v-if="cloudUrlResult && cloudUrlResult.url"
:model-value="cloudUrlResult.url" :model-value="cloudUrlResult.url"
label="云端访问地址"
variant="outlined"
readonly
class="font-monospace"
append-inner-icon="mdi-content-copy" append-inner-icon="mdi-content-copy"
class="font-monospace"
label="云端访问地址"
readonly
variant="outlined"
@click:append-inner="copyCloudUrl" @click:append-inner="copyCloudUrl"
/> />
<v-expansion-panels v-if="cloudUrlResult && cloudUrlResult.url" class="mt-4"> <v-expansion-panels v-if="cloudUrlResult && cloudUrlResult.url" class="mt-4">
<v-expansion-panel> <v-expansion-panel>
<v-expansion-panel-title> <v-expansion-panel-title>
<v-icon icon="mdi-cog" class="mr-2" /> <v-icon class="mr-2" icon="mdi-cog"/>
高级选项 高级选项
</v-expansion-panel-title> </v-expansion-panel-title>
<v-expansion-panel-text> <v-expansion-panel-text>
<v-checkbox <v-checkbox
v-model="cloudUrlOptions.migrateFromLocal" v-model="cloudUrlOptions.migrateFromLocal"
label="从本地迁移数据到云端"
density="compact" density="compact"
label="从本地迁移数据到云端"
/> />
<v-checkbox <v-checkbox
v-model="cloudUrlOptions.autoConfigureCloud" v-model="cloudUrlOptions.autoConfigureCloud"
label="自动配置云端默认设置"
density="compact" density="compact"
label="自动配置云端默认设置"
/> />
<v-btn <v-btn
@click="refreshCloudUrl"
variant="tonal"
color="primary"
:loading="gettingCloudUrl" :loading="gettingCloudUrl"
class="mt-2" class="mt-2"
color="primary"
variant="tonal"
@click="refreshCloudUrl"
> >
<v-icon icon="mdi-refresh" class="mr-1" /> <v-icon class="mr-1" icon="mdi-refresh"/>
重新获取 重新获取
</v-btn> </v-btn>
</v-expansion-panel-text> </v-expansion-panel-text>
@ -312,17 +312,17 @@
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn @click="cloudUrlDialog = false" variant="text"> <v-btn variant="text" @click="cloudUrlDialog = false">
关闭 关闭
</v-btn> </v-btn>
<v-btn <v-btn
v-if="cloudUrlResult && cloudUrlResult.url" v-if="cloudUrlResult && cloudUrlResult.url"
@click="openCloudUrl"
variant="tonal"
color="primary" color="primary"
variant="tonal"
@click="openCloudUrl"
> >
<v-icon icon="mdi-open-in-new" class="mr-1" /> <v-icon class="mr-1" icon="mdi-open-in-new"/>
在新窗口打开 在新窗口打开
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -333,28 +333,28 @@
<v-dialog v-model="deleteDialog" max-width="400px"> <v-dialog v-model="deleteDialog" max-width="400px">
<v-card> <v-card>
<v-card-title class="d-flex align-center text-error"> <v-card-title class="d-flex align-center text-error">
<v-icon icon="mdi-alert" class="mr-2" /> <v-icon class="mr-2" icon="mdi-alert"/>
确认删除 确认删除
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
确定要删除键名为 <code>{{ itemToDelete?.key }}</code> 的数据吗 确定要删除键名为 <code>{{ itemToDelete?.key }}</code> 的数据吗
<br><br> <br><br>
<v-alert type="warning" variant="tonal" class="mt-2"> <v-alert class="mt-2" type="warning" variant="tonal">
此操作不可撤销请谨慎操作 此操作不可撤销请谨慎操作
</v-alert> </v-alert>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn @click="deleteDialog = false" variant="text"> <v-btn variant="text" @click="deleteDialog = false">
取消 取消
</v-btn> </v-btn>
<v-btn <v-btn
@click="deleteItem"
variant="tonal"
color="error"
:loading="deletingData" :loading="deletingData"
color="error"
variant="tonal"
@click="deleteItem"
> >
删除 删除
</v-btn> </v-btn>
@ -367,8 +367,8 @@
<script> <script>
import SettingsCard from '@/components/SettingsCard.vue'; import SettingsCard from '@/components/SettingsCard.vue';
import dataProvider from '@/utils/dataProvider'; import dataProvider from '@/utils/dataProvider';
import { getSetting } from '@/utils/settings'; import {getSetting} from '@/utils/settings';
import { openDB } from 'idb'; import {openDB} from 'idb';
export default { export default {
name: 'KvDatabaseCard', name: 'KvDatabaseCard',
@ -414,8 +414,8 @@ export default {
// //
tableHeaders: [ tableHeaders: [
{ title: '键名', key: 'key', sortable: true }, {title: '键名', key: 'key', sortable: true},
{ title: '操作', key: 'actions', sortable: false, width: '120px' } {title: '操作', key: 'actions', sortable: false, width: '120px'}
] ]
}; };
}, },
@ -426,7 +426,7 @@ export default {
}, },
isKvProvider() { isKvProvider() {
return this.currentProvider === 'kv-local' || this.currentProvider === 'kv-server'||this.currentProvider === 'classworkscloud' return this.currentProvider === 'kv-local' || this.currentProvider === 'kv-server' || this.currentProvider === 'classworkscloud'
}, },
connectionStatus() { connectionStatus() {
@ -537,15 +537,10 @@ export default {
}, },
async viewItem(item) { async viewItem(item) {
this.selectedItem = item; this.selectedItem = item;
this.viewDialog = true; this.viewDialog = true;
// //
if (!item.loaded || item.value === null) { if (!item.loaded || item.value === null) {
await this.loadItemData(item); await this.loadItemData(item);
@ -554,12 +549,12 @@ export default {
async editItem(item) { async editItem(item) {
this.editingItem = item; this.editingItem = item;
// //
if (!item.loaded || item.value === null) { if (!item.loaded || item.value === null) {
await this.loadItemData(item); await this.loadItemData(item);
} }
this.editingData = this.formatJsonData(item.value); this.editingData = this.formatJsonData(item.value);
this.editDialog = true; this.editDialog = true;
}, },
@ -716,22 +711,22 @@ export default {
this.cloudUrlResult = null; this.cloudUrlResult = null;
this.cloudUrlError = null; this.cloudUrlError = null;
this.cloudUrlDialog = true; this.cloudUrlDialog = true;
await this.fetchCloudUrl(); await this.fetchCloudUrl();
}, },
async fetchCloudUrl() { async fetchCloudUrl() {
if (!this.selectedCloudItem) return; if (!this.selectedCloudItem) return;
this.gettingCloudUrl = true; this.gettingCloudUrl = true;
this.cloudUrlError = null; this.cloudUrlError = null;
try { try {
const result = await dataProvider.getKeyCloudUrl( const result = await dataProvider.getKeyCloudUrl(
this.selectedCloudItem.key, this.selectedCloudItem.key,
this.cloudUrlOptions this.cloudUrlOptions
); );
if (result.success) { if (result.success) {
this.cloudUrlResult = result; this.cloudUrlResult = result;
this.$message.success('云端地址获取成功'); this.$message.success('云端地址获取成功');
@ -753,7 +748,7 @@ export default {
async copyCloudUrl() { async copyCloudUrl() {
if (!this.cloudUrlResult?.url) return; if (!this.cloudUrlResult?.url) return;
try { try {
await navigator.clipboard.writeText(this.cloudUrlResult.url); await navigator.clipboard.writeText(this.cloudUrlResult.url);
this.$message.success('云端地址已复制到剪贴板'); this.$message.success('云端地址已复制到剪贴板');
@ -764,7 +759,7 @@ export default {
openCloudUrl() { openCloudUrl() {
if (!this.cloudUrlResult?.url) return; if (!this.cloudUrlResult?.url) return;
try { try {
window.open(this.cloudUrlResult.url, '_blank'); window.open(this.cloudUrlResult.url, '_blank');
} catch (error) { } catch (error) {

View File

@ -1,30 +1,28 @@
<template> <template>
<settings-card title="编辑设置" icon="mdi-cog"> <settings-card icon="mdi-cog" title="编辑设置">
<v-list> <v-list>
<setting-item setting-key="randomPicker.enabled" /> <setting-item setting-key="randomPicker.enabled"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item setting-key="randomPicker.mode" /> <setting-item setting-key="randomPicker.mode"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item setting-key="randomPicker.minNumber" /> <setting-item setting-key="randomPicker.minNumber"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item setting-key="randomPicker.maxNumber" /> <setting-item setting-key="randomPicker.maxNumber"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item setting-key="randomPicker.defaultCount" /> <setting-item setting-key="randomPicker.defaultCount"/>
<v-divider class="my-2" /> <v-divider class="my-2"/>
<setting-item setting-key="randomPicker.animation" /> <setting-item setting-key="randomPicker.animation"/>
</v-list>
</settings-card>
</template>
<script>
import SettingsCard from '@/components/SettingsCard.vue';
import SettingItem from '../SettingItem.vue';
</v-list> </script>
</settings-card>
</template>
<script>
import SettingsCard from '@/components/SettingsCard.vue';
import SettingItem from '../SettingItem.vue';
</script>

View File

@ -1,10 +1,11 @@
<template> <template>
<settings-card title="刷新设置" icon="mdi-refresh-circle"> <settings-card icon="mdi-refresh-circle" title="刷新设置">
<v-form> <v-form>
<v-list> <v-list>
<setting-item setting-key="refresh.auto" title="自动刷新" /> <v-divider class="my-2" /> <setting-item setting-key="refresh.auto" title="自动刷新"/>
<v-divider class="my-2"/>
<setting-item setting-key="refresh.interval" title="刷新间隔" /> <setting-item setting-key="refresh.interval" title="刷新间隔"/>
</v-list> </v-list>
</v-form> </v-form>
@ -14,15 +15,14 @@
<script> <script>
import SettingsCard from '@/components/SettingsCard.vue'; import SettingsCard from '@/components/SettingsCard.vue';
import SettingItem from '@/components/settings/SettingItem.vue'; import SettingItem from '@/components/settings/SettingItem.vue';
export default { export default {
name: 'RefreshSettingsCard', name: 'RefreshSettingsCard',
components: { SettingsCard }, components: {SettingsCard},
data() { data() {
return { return {};
};
}, },
}; };
</script> </script>

View File

@ -1,8 +1,8 @@
<template> <template>
<settings-card <settings-card
title="数据源设置"
icon="mdi-database"
:loading="loading" :loading="loading"
icon="mdi-database"
title="数据源设置"
> >
<v-form> <v-form>
<!-- 使用双向绑定来替代 setting-key --> <!-- 使用双向绑定来替代 setting-key -->
@ -13,20 +13,20 @@
{ title: 'KV本地存储', value: 'kv-local' }, { title: 'KV本地存储', value: 'kv-local' },
{ title: 'KV远程服务器', value: 'kv-server' } { title: 'KV远程服务器', value: 'kv-server' }
]" ]"
label="数据提供者" class="mb-3"
variant="outlined"
density="comfortable" density="comfortable"
item-title="title" item-title="title"
item-value="value" item-value="value"
label="数据提供者"
prepend-icon="mdi-database" prepend-icon="mdi-database"
class="mb-3" variant="outlined"
/> />
<v-alert <v-alert
v-if="isKvProvider" v-if="isKvProvider"
class="my-2"
type="info" type="info"
variant="tonal" variant="tonal"
class="my-2"
> >
<v-alert-title>KV 存储系统</v-alert-title> <v-alert-title>KV 存储系统</v-alert-title>
<p>KV存储系统使用本机唯一标识符(UUID)来区分不同设备的数据</p> <p>KV存储系统使用本机唯一标识符(UUID)来区分不同设备的数据</p>
@ -38,10 +38,10 @@
<v-alert <v-alert
v-if="isClassworksCloud" v-if="isClassworksCloud"
type="info"
color="success"
variant="tonal"
class="my-2" class="my-2"
color="success"
type="info"
variant="tonal"
> >
<v-alert-title>Classworks云端存储</v-alert-title> <v-alert-title>Classworks云端存储</v-alert-title>
<p>Classworks云端存储是官方提供的存储解决方案自动配置了最优的访问设置</p> <p>Classworks云端存储是官方提供的存储解决方案自动配置了最优的访问设置</p>
@ -56,17 +56,16 @@
<div v-if="isClassworksCloud"> <div v-if="isClassworksCloud">
<v-text-field <v-text-field
v-model="serverSettings.kvToken" v-model="serverSettings.kvToken"
label="KV 授权令牌"
variant="outlined"
density="comfortable"
prepend-icon="mdi-shield-key"
class="mb-2" class="mb-2"
density="comfortable"
hint="令牌用于云端存储授权" hint="令牌用于云端存储授权"
label="KV 授权令牌"
persistent-hint persistent-hint
prepend-icon="mdi-shield-key"
variant="outlined"
/> />
<cloud-namespace-info-card <cloud-namespace-info-card
:visible="isClassworksCloud" :visible="isClassworksCloud"
class="mt-4" class="mt-4"
@ -77,24 +76,24 @@
<div v-else-if="currentProvider === 'kv-server'"> <div v-else-if="currentProvider === 'kv-server'">
<v-text-field <v-text-field
v-model="serverSettings.domain" v-model="serverSettings.domain"
label="服务器域名"
variant="outlined"
density="comfortable"
prepend-icon="mdi-web"
class="mb-2" class="mb-2"
density="comfortable"
hint="例如: https://example.com (不需要路径)" hint="例如: https://example.com (不需要路径)"
label="服务器域名"
persistent-hint persistent-hint
prepend-icon="mdi-web"
variant="outlined"
/> />
<v-text-field <v-text-field
v-model="serverSettings.kvToken" v-model="serverSettings.kvToken"
label="KV 授权令牌"
variant="outlined"
density="comfortable"
prepend-icon="mdi-shield-key"
class="mb-2" class="mb-2"
density="comfortable"
hint="令牌用于服务器验证" hint="令牌用于服务器验证"
label="KV 授权令牌"
persistent-hint persistent-hint
prepend-icon="mdi-shield-key"
variant="outlined"
/> />
</div> </div>
@ -102,13 +101,13 @@
<div v-else-if="currentProvider === 'kv-local'"> <div v-else-if="currentProvider === 'kv-local'">
<v-text-field <v-text-field
v-model="serverSettings.classNumber" v-model="serverSettings.classNumber"
label="班级编号"
variant="outlined"
density="comfortable"
prepend-icon="mdi-account-group"
class="mb-2" class="mb-2"
density="comfortable"
hint="例如: 高三八班" hint="例如: 高三八班"
label="班级编号"
persistent-hint persistent-hint
prepend-icon="mdi-account-group"
variant="outlined"
/> />
</div> </div>
</v-form> </v-form>
@ -118,11 +117,11 @@
<script> <script>
import SettingsCard from "@/components/SettingsCard.vue"; import SettingsCard from "@/components/SettingsCard.vue";
import CloudNamespaceInfoCard from "./CloudNamespaceInfoCard.vue"; import CloudNamespaceInfoCard from "./CloudNamespaceInfoCard.vue";
import { getSetting, setSetting, watchSettings } from "@/utils/settings"; import {getSetting, setSetting, watchSettings} from "@/utils/settings";
export default { export default {
name: "ServerSettingsCard", name: "ServerSettingsCard",
components: { SettingsCard, CloudNamespaceInfoCard }, components: {SettingsCard, CloudNamespaceInfoCard},
props: { props: {
loading: Boolean, loading: Boolean,
}, },

View File

@ -1,16 +1,16 @@
<template> <template>
<settings-card <settings-card
title="科目管理"
icon="mdi-book-multiple"
:loading="loading" :loading="loading"
border border
icon="mdi-book-multiple"
title="科目管理"
> >
<v-alert <v-alert
v-if="error" v-if="error"
class="mb-4"
closable
type="error" type="error"
variant="tonal" variant="tonal"
closable
class="mb-4"
> >
{{ error }} {{ error }}
</v-alert> </v-alert>
@ -18,32 +18,32 @@
<div class="d-flex justify-space-between align-center mb-6"> <div class="d-flex justify-space-between align-center mb-6">
<div> <div>
<v-btn <v-btn
variant="text"
color="primary"
size="large"
prepend-icon="mdi-refresh"
:loading="loading" :loading="loading"
@click="loadConfig"
class="mr-2" class="mr-2"
color="primary"
prepend-icon="mdi-refresh"
size="large"
variant="text"
@click="loadConfig"
> >
重新加载 重新加载
</v-btn> </v-btn>
<v-btn <v-btn
color="success"
size="large"
prepend-icon="mdi-content-save"
:loading="loading" :loading="loading"
color="success"
prepend-icon="mdi-content-save"
size="large"
@click="saveConfig" @click="saveConfig"
> >
保存 保存
</v-btn> </v-btn>
<v-btn <v-btn
variant="text"
prepend-icon="mdi-restore"
:loading="loading" :loading="loading"
@click="resetToDefault"
class="mr-2" class="mr-2"
prepend-icon="mdi-restore"
variant="text"
@click="resetToDefault"
> >
重置为默认 重置为默认
</v-btn> </v-btn>
@ -64,12 +64,12 @@
<v-col cols="12" sm="6"> <v-col cols="12" sm="6">
<v-text-field <v-text-field
v-model="newSubjectName" v-model="newSubjectName"
:rules="[v => !!v || '科目名称不能为空']"
append-inner-icon="mdi-plus"
density="comfortable"
label="科目名称" label="科目名称"
variant="outlined" variant="outlined"
density="comfortable"
:rules="[v => !!v || '科目名称不能为空']"
@keyup.enter="addSubject" @keyup.enter="addSubject"
append-inner-icon="mdi-plus"
@click:append-inner="addSubject" @click:append-inner="addSubject"
/> />
</v-col> </v-col>
@ -88,17 +88,17 @@
<template v-slot:prepend> <template v-slot:prepend>
<div class="d-flex flex-column align-center mr-2"> <div class="d-flex flex-column align-center mr-2">
<v-btn <v-btn
icon="mdi-chevron-up"
variant="text"
size="small"
:disabled="index === 0" :disabled="index === 0"
icon="mdi-chevron-up"
size="small"
variant="text"
@click="moveSubject(index, -1)" @click="moveSubject(index, -1)"
/> />
<v-btn <v-btn
icon="mdi-chevron-down"
variant="text"
size="small"
:disabled="index === subjects.length - 1" :disabled="index === subjects.length - 1"
icon="mdi-chevron-down"
size="small"
variant="text"
@click="moveSubject(index, 1)" @click="moveSubject(index, 1)"
/> />
</div> </div>
@ -107,19 +107,19 @@
<v-list-item-title> <v-list-item-title>
<v-text-field <v-text-field
v-model="subject.name" v-model="subject.name"
variant="plain"
density="compact" density="compact"
hide-details hide-details
variant="plain"
@blur="updateSubject(subject)" @blur="updateSubject(subject)"
/> />
</v-list-item-title> </v-list-item-title>
<template v-slot:append> <template v-slot:append>
<v-btn <v-btn
icon="mdi-delete"
variant="text"
color="error" color="error"
icon="mdi-delete"
size="small" size="small"
variant="text"
@click="deleteSubject(subject)" @click="deleteSubject(subject)"
/> />
</template> </template>
@ -161,16 +161,16 @@ export default {
snackbarText: '', snackbarText: '',
snackbarColor: 'success', snackbarColor: 'success',
defaultSubjects: [ defaultSubjects: [
{ name: '语文', order: 0 }, {name: '语文', order: 0},
{ name: '数学', order: 1 }, {name: '数学', order: 1},
{ name: '英语', order: 2 }, {name: '英语', order: 2},
{ name: '物理', order: 3 }, {name: '物理', order: 3},
{ name: '化学', order: 4 }, {name: '化学', order: 4},
{ name: '生物', order: 5 }, {name: '生物', order: 5},
{ name: '政治', order: 6 }, {name: '政治', order: 6},
{ name: '历史', order: 7 }, {name: '历史', order: 7},
{ name: '地理', order: 8 }, {name: '地理', order: 8},
{ name: '其他', order: 9 } {name: '其他', order: 9}
] ]
}; };
}, },
@ -250,7 +250,7 @@ export default {
updateSubject(subject) { updateSubject(subject) {
const index = this.subjects.findIndex(s => s.order === subject.order); const index = this.subjects.findIndex(s => s.order === subject.order);
if (index > -1) { if (index > -1) {
this.subjects[index] = { ...subject }; this.subjects[index] = {...subject};
} }
}, },
@ -291,7 +291,8 @@ export default {
.v-list-item { .v-list-item {
border-bottom: 1px solid rgba(0, 0, 0, 0.12); border-bottom: 1px solid rgba(0, 0, 0, 0.12);
} }
.v-list-item:last-child { .v-list-item:last-child {
border-bottom: none; border-bottom: none;
} }
</style> </style>

View File

@ -1,24 +1,24 @@
<template> <template>
<settings-card title="主题设置" icon="mdi-palette"> <settings-card icon="mdi-palette" title="主题设置">
<v-list> <v-list>
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-theme-light-dark" class="mr-3" /> <v-icon class="mr-3" icon="mdi-theme-light-dark"/>
</template> </template>
<v-list-item-title>主题模式</v-list-item-title> <v-list-item-title>主题模式</v-list-item-title>
<v-list-item-subtitle>选择明亮或暗黑主题</v-list-item-subtitle> <v-list-item-subtitle>选择明亮或暗黑主题</v-list-item-subtitle>
<template #append> <template #append>
<v-btn-toggle <v-btn-toggle
v-model="localTheme" v-model="localTheme"
density="comfortable"
color="primary" color="primary"
density="comfortable"
> >
<v-btn value="light"> <v-btn value="light">
<v-icon icon="mdi-white-balance-sunny" class="mr-2" /> <v-icon class="mr-2" icon="mdi-white-balance-sunny"/>
明亮 明亮
</v-btn> </v-btn>
<v-btn value="dark"> <v-btn value="dark">
<v-icon icon="mdi-moon-waning-crescent" class="mr-2" /> <v-icon class="mr-2" icon="mdi-moon-waning-crescent"/>
暗黑 暗黑
</v-btn> </v-btn>
</v-btn-toggle> </v-btn-toggle>
@ -30,12 +30,12 @@
<script> <script>
import SettingsCard from '@/components/SettingsCard.vue'; import SettingsCard from '@/components/SettingsCard.vue';
import { getSetting, setSetting } from '@/utils/settings'; import {getSetting, setSetting} from '@/utils/settings';
import { useTheme } from 'vuetify'; import {useTheme} from 'vuetify';
export default { export default {
name: 'ThemeSettingsCard', name: 'ThemeSettingsCard',
components: { SettingsCard }, components: {SettingsCard},
data() { data() {
return { return {
@ -52,7 +52,7 @@ export default {
setup() { setup() {
const theme = useTheme(); const theme = useTheme();
return { theme }; return {theme};
}, },
methods: { methods: {

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
# Layouts # Layouts
Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across multiple pages. Layouts are reusable components that wrap around pages. They are used to provide a consistent look and feel across
multiple pages.
Full documentation for this feature can be found in the Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository. Full documentation for this feature can be found in the
Official [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) repository.

View File

@ -1,11 +1,11 @@
<template> <template>
<v-app> <v-app>
<v-main> <v-main>
<router-view /> <router-view/>
</v-main> </v-main>
</v-app> </v-app>
</template> </template>
<script setup> <script setup>
// //
</script> </script>

View File

@ -5,8 +5,9 @@
*/ */
// Plugins // Plugins
import { registerPlugins } from '@/plugins' import {registerPlugins} from '@/plugins'
import { createPinia } from 'pinia' import {createPinia} from 'pinia'
const pinia = createPinia() const pinia = createPinia()
// Components // Components
@ -14,8 +15,9 @@ import App from './App.vue'
import GlobalMessage from '@/components/GlobalMessage.vue' import GlobalMessage from '@/components/GlobalMessage.vue'
// Composables // Composables
import { createApp } from 'vue' import {createApp} from 'vue'
import Clarity from '@microsoft/clarity'; import Clarity from '@microsoft/clarity';
const projectId = "rhp8uqoc3l" const projectId = "rhp8uqoc3l"
//import TDesign from 'tdesign-vue-next' //import TDesign from 'tdesign-vue-next'
//import 'tdesign-vue-next/es/style/index.css' //import 'tdesign-vue-next/es/style/index.css'
@ -37,18 +39,18 @@ app.mount('#app')
// 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后) // 移除首屏 CSS 加载覆盖层(在 Vue 挂载完成后)
try { try {
const removeLoader = () => { const removeLoader = () => {
document.body.classList.add('app-loaded'); document.body.classList.add('app-loaded');
const el = document.getElementById('app-loader'); const el = document.getElementById('app-loader');
if (!el) return; if (!el) return;
// 与 CSS 过渡对齐,稍等再移除节点,避免闪烁 // 与 CSS 过渡对齐,稍等再移除节点,避免闪烁
setTimeout(() => el.remove(), 220); setTimeout(() => el.remove(), 220);
}; };
if (document.readyState === 'complete' || document.readyState === 'interactive') { if (document.readyState === 'complete' || document.readyState === 'interactive') {
removeLoader(); removeLoader();
} else { } else {
window.addEventListener('DOMContentLoaded', removeLoader, { once: true }); window.addEventListener('DOMContentLoaded', removeLoader, {once: true});
} }
} catch { } catch {
// 安全失败:即便移除失败也不影响应用 // 安全失败:即便移除失败也不影响应用
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<error404 /> <error404/>
</template> </template>
<script setup> <script setup>
import error404 from "@/components/error/404.vue"; import error404 from "@/components/error/404.vue";
</script> </script>

View File

@ -3,38 +3,39 @@
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<div class="d-flex align-center mb-6"> <div class="d-flex align-center mb-6">
<v-icon size="x-large" color="primary" class="mr-3">mdi-database-cog-outline</v-icon> <v-icon class="mr-3" color="primary" size="x-large">mdi-database-cog-outline</v-icon>
<div> <div>
<h1 class="text-h4 ">缓存管理</h1> <h1 class="text-h4 ">缓存管理</h1>
<div class="text-subtitle-1 text-grey">管理应用的本地缓存资源</div> <div class="text-subtitle-1 text-grey">管理应用的本地缓存资源</div>
</div> </div>
</div> </div>
<v-card class="mb-6" variant="tonal" color="info" density="compact"> <v-card class="mb-6" color="info" density="compact" variant="tonal">
<v-card-text class="d-flex align-center"> <v-card-text class="d-flex align-center">
<v-icon color="info" class="mr-2">mdi-information-outline</v-icon> <v-icon class="mr-2" color="info">mdi-information-outline</v-icon>
<span>在这里您可以查看和管理应用的缓存文件清除缓存可能会导致应用需要重新下载资源但有助于解决某些显示问题</span> <span>在这里您可以查看和管理应用的缓存文件清除缓存可能会导致应用需要重新下载资源但有助于解决某些显示问题</span>
</v-card-text> </v-card-text>
</v-card> </v-card>
<v-row> <v-row>
<v-col cols="12" md="8"> <v-col cols="12" md="8">
<v-card class="mb-4" variant="tonal"> <v-card class="mb-4" variant="tonal">
<v-card-text> <v-card-text>
<div class="d-flex align-center mb-2"> <div class="d-flex align-center mb-2">
<v-icon color="primary" class="mr-2">mdi-information</v-icon> <v-icon class="mr-2" color="primary">mdi-information</v-icon>
<span class="text-h6">什么是缓存</span> <span class="text-h6">什么是缓存</span>
</div> </div>
<p>缓存是浏览器在本地存储的网站资源副本如图片脚本和样式表等这些缓存可以加快页面加载速度减少数据使用并在离线时提供基本功能</p> <p>
缓存是浏览器在本地存储的网站资源副本如图片脚本和样式表等这些缓存可以加快页面加载速度减少数据使用并在离线时提供基本功能</p>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<v-card class="mb-4" variant="tonal"> <v-card class="mb-4" variant="tonal">
<v-card-text> <v-card-text>
<div class="d-flex align-center mb-2"> <div class="d-flex align-center mb-2">
<v-icon color="warning" class="mr-2">mdi-lightbulb-outline</v-icon> <v-icon class="mr-2" color="warning">mdi-lightbulb-outline</v-icon>
<span class="text-h6">何时清除缓存</span> <span class="text-h6">何时清除缓存</span>
</div> </div>
<ul class="pl-4"> <ul class="pl-4">
@ -46,8 +47,8 @@
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<CacheManager /> <CacheManager/>
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
@ -65,4 +66,4 @@ export default {
title: '缓存管理' title: '缓存管理'
} }
} }
</script> </script>

View File

@ -4,9 +4,9 @@
<v-col cols="12"> <v-col cols="12">
<div class="d-flex align-center mb-6"> <div class="d-flex align-center mb-6">
<v-icon <v-icon
size="x-large"
color="primary"
class="mr-3" class="mr-3"
color="primary"
size="x-large"
> >
mdi-database-sync mdi-database-sync
</v-icon> </v-icon>
@ -22,13 +22,13 @@
<v-card <v-card
class="mb-6" class="mb-6"
variant="tonal"
color="info" color="info"
variant="tonal"
> >
<v-card-text class="d-flex align-center"> <v-card-text class="d-flex align-center">
<v-icon <v-icon
color="info"
class="mr-2" class="mr-2"
color="info"
> >
mdi-information-outline mdi-information-outline
</v-icon> </v-icon>
@ -38,7 +38,7 @@
</v-card-text> </v-card-text>
</v-card> </v-card>
<MigrationTool ref="migrationTool" /> <MigrationTool ref="migrationTool"/>
</v-col> </v-col>
</v-row> </v-row>
@ -51,9 +51,9 @@
<v-card> <v-card>
<v-card-title class="text-h5 d-flex align-center"> <v-card-title class="text-h5 d-flex align-center">
<v-icon <v-icon
class="mr-3"
color="primary" color="primary"
size="large" size="large"
class="mr-3"
> >
mdi-database-sync mdi-database-sync
</v-icon> </v-icon>
@ -66,10 +66,10 @@
</p> </p>
<v-alert <v-alert
color="info"
variant="tonal"
class="mt-4" class="mt-4"
color="info"
icon="mdi-information-outline" icon="mdi-information-outline"
variant="tonal"
> >
<ul class="ml-3 mt-1"> <ul class="ml-3 mt-1">
<li>数据源: {{ dataSourceText }}</li> <li>数据源: {{ dataSourceText }}</li>
@ -83,7 +83,7 @@
</v-alert> </v-alert>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
color="grey-darken-1" color="grey-darken-1"
variant="text" variant="text"
@ -92,16 +92,16 @@
稍后再说 稍后再说
</v-btn> </v-btn>
<v-btn <v-btn
:disabled="isAutoMigrating"
:loading="isAutoMigrating"
color="primary" color="primary"
size="large" size="large"
variant="elevated" variant="elevated"
:loading="isAutoMigrating"
:disabled="isAutoMigrating"
@click="startAutoMigration" @click="startAutoMigration"
> >
<v-icon <v-icon
left
class="mr-2" class="mr-2"
left
> >
mdi-database-export mdi-database-export
</v-icon> </v-icon>
@ -115,7 +115,7 @@
<script> <script>
import MigrationTool from "@/components/MigrationTool.vue"; import MigrationTool from "@/components/MigrationTool.vue";
import { getSetting, setSetting } from "@/utils/settings"; import {getSetting, setSetting} from "@/utils/settings";
export default { export default {
name: "DataMigrationPage", name: "DataMigrationPage",

View File

@ -2,4 +2,5 @@
Vue components created in this folder will automatically be converted to navigatable routes. Vue components created in this folder will automatically be converted to navigatable routes.
Full documentation for this feature can be found in the Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository. Full documentation for this feature can be found in the
Official [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) repository.

View File

@ -1,7 +1,7 @@
<template> <template>
<v-container class="fill-height" fluid> <v-container class="fill-height" fluid>
<v-row align="center" justify="center"> <v-row align="center" justify="center">
<v-col cols="12" sm="8" md="6"> <v-col cols="12" md="6" sm="8">
<v-card> <v-card>
<v-card-title class="text-h5"> <v-card-title class="text-h5">
{{ status === 'processing' ? '正在处理授权...' : status === 'success' ? '授权成功' : '授权失败' }} {{ status === 'processing' ? '正在处理授权...' : status === 'success' ? '授权成功' : '授权失败' }}
@ -9,9 +9,9 @@
<v-card-text> <v-card-text>
<v-progress-linear <v-progress-linear
v-if="status === 'processing'" v-if="status === 'processing'"
indeterminate
color="primary"
class="mb-4" class="mb-4"
color="primary"
indeterminate
></v-progress-linear> ></v-progress-linear>
<p>{{ message }}</p> <p>{{ message }}</p>
</v-card-text> </v-card-text>
@ -26,9 +26,9 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import {ref, onMounted} from 'vue';
import { useRoute, useRouter } from 'vue-router'; import {useRoute, useRouter} from 'vue-router';
import { getSetting, setSetting } from '@/utils/settings'; import {getSetting, setSetting} from '@/utils/settings';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -59,7 +59,7 @@ onMounted(async () => {
} }
status.value = 'success'; status.value = 'success';
router.push('/'); router.push('/');
} catch (error) { } catch (error) {
console.error('授权处理失败:', error); console.error('授权处理失败:', error);

View File

@ -45,12 +45,19 @@
<!-- 输入方式选择 --> <!-- 输入方式选择 -->
<v-tabs v-model="activeTab" class="mb-4 mx-2" color="primary" rounded> <v-tabs v-model="activeTab" class="mb-4 mx-2" color="primary" rounded>
<v-tab value="text" class="px-5"><v-icon start>mdi-text-box</v-icon> 文本粘贴</v-tab> <v-tab value="text" class="px-5">
<v-tab value="file" class="px-5"><v-icon start>mdi-file-upload</v-icon> 文件上传</v-tab> <v-icon start>mdi-text-box</v-icon>
文本粘贴
</v-tab>
<v-tab value="file" class="px-5">
<v-icon start>mdi-file-upload</v-icon>
文件上传
</v-tab>
</v-tabs> </v-tabs>
<!-- 格式选择 --> <!-- 格式选择 -->
<v-btn-toggle v-model="formatMode" color="primary" class="mb-4 mx-2" mandatory density="comfortable" border rounded> <v-btn-toggle v-model="formatMode" color="primary" class="mb-4 mx-2" mandatory density="comfortable" border
rounded>
<v-btn value="auto">自动检测</v-btn> <v-btn value="auto">自动检测</v-btn>
<v-btn value="json">JSON</v-btn> <v-btn value="json">JSON</v-btn>
<v-btn value="yaml" :disabled="!yamlLibLoaded"> <v-btn value="yaml" :disabled="!yamlLibLoaded">
@ -268,12 +275,12 @@
<span <span
v-if="!settings.hideTeacherName && course.teacher" v-if="!settings.hideTeacherName && course.teacher"
> >
<br />{{ course.teacher }} <br/>{{ course.teacher }}
</span> </span>
<span <span
v-if="!settings.hideRoom && course.room" v-if="!settings.hideRoom && course.room"
> >
<br />{{ course.room }} <br/>{{ course.room }}
</span> </span>
<span <span
v-if="course.weekType" v-if="course.weekType"
@ -288,17 +295,17 @@
<span <span
v-if="!settings.hideTeacherName && item[day].teacher" v-if="!settings.hideTeacherName && item[day].teacher"
> >
<br />{{ item[day].teacher }} <br/>{{ item[day].teacher }}
</span> </span>
<span <span
v-if="!settings.hideRoom && item[day].room" v-if="!settings.hideRoom && item[day].room"
> >
<br />{{ item[day].room }} <br/>{{ item[day].room }}
</span> </span>
<span <span
v-if="item[day].weekType" v-if="item[day].weekType"
class="week-type" class="week-type"
> >
{{ item[day].weekType }} {{ item[day].weekType }}
</span> </span>
</template> </template>
@ -329,7 +336,8 @@
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<!-- 美化日期导航标签 --> <!-- 美化日期导航标签 -->
<v-tabs v-if="daysWithSchedule.length > 0" v-model="activeDay" class="mb-4" color="primary" grow align-tabs="center"> <v-tabs v-if="daysWithSchedule.length > 0" v-model="activeDay" class="mb-4" color="primary" grow
align-tabs="center">
<v-tab v-for="day in daysWithSchedule" :key="day" :value="day" class="px-2 font-weight-medium"> <v-tab v-for="day in daysWithSchedule" :key="day" :value="day" class="px-2 font-weight-medium">
{{ dayNames[day] }} {{ dayNames[day] }}
<v-badge <v-badge
@ -345,67 +353,69 @@
<v-window-item v-for="day in daysWithSchedule" :key="day" :value="day"> <v-window-item v-for="day in daysWithSchedule" :key="day" :value="day">
<v-table density="compact" class="rounded" :headers-length="6" disable-sort> <v-table density="compact" class="rounded" :headers-length="6" disable-sort>
<thead> <thead>
<tr> <tr>
<th class="text-center">节次</th> <th class="text-center">节次</th>
<th>课程</th> <th>课程</th>
<th>时间</th> <th>时间</th>
<th>教师</th> <th>教师</th>
<th>教室</th> <th>教室</th>
<th>周次</th> <th>周次</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="(group, index) in groupByPeriod(getDaySchedule(day))" :key="index"> <template v-for="(group, index) in groupByPeriod(getDaySchedule(day))" :key="index">
<tr> <tr>
<td class="text-center font-weight-bold"> <td class="text-center font-weight-bold">
{{ group.period }} {{ group.period }}
<v-tooltip v-if="group.originalPeriod !== group.period"> <v-tooltip v-if="group.originalPeriod !== group.period">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-icon size="x-small" v-bind="props" color="info" class="ml-1">mdi-sync</v-icon> <v-icon size="x-small" v-bind="props" color="info" class="ml-1">mdi-sync</v-icon>
</template>
原节次: {{ group.originalPeriod }}
</v-tooltip>
</td>
<td>
<div v-for="(item, i) in group.items" :key="i" class="mb-1">
<v-chip size="small" :color="getSubjectColor(item.subject)" label text-color="white" class="mr-1">
{{ item.subject }}
</v-chip>
<v-chip v-if="group.items.length > 1" size="x-small" class="ml-1" :color="item.weekType === '单' ? 'warning' : 'success'">
{{ item.weekType }}
</v-chip>
</div>
</td>
<td>
<div v-for="(timeSlot, i) in group.uniqueTimeSlots" :key="i" class="mb-1">
<v-chip size="x-small" class="time-chip">
{{ formatTime(timeSlot.startTime) }} - {{ formatTime(timeSlot.endTime) }}
</v-chip>
</div>
</td>
<td>
<template v-if="!settings.hideTeacherName">
<div v-for="(item, i) in group.items" :key="i" class="mb-1">
{{ item.teacher || '-' }}
</div>
</template> </template>
<template v-else>-</template> 原节次: {{ group.originalPeriod }}
</td> </v-tooltip>
<td> </td>
<template v-if="!settings.hideRoom"> <td>
<div v-for="(item, i) in group.items" :key="i" class="mb-1"> <div v-for="(item, i) in group.items" :key="i" class="mb-1">
{{ item.room || '-' }} <v-chip size="small" :color="getSubjectColor(item.subject)" label text-color="white"
</div> class="mr-1">
</template> {{ item.subject }}
<template v-else>-</template> </v-chip>
</td> <v-chip v-if="group.items.length > 1" size="x-small" class="ml-1"
<td> :color="item.weekType === '单' ? 'warning' : 'success'">
{{ item.weekType }}
</v-chip>
</div>
</td>
<td>
<div v-for="(timeSlot, i) in group.uniqueTimeSlots" :key="i" class="mb-1">
<v-chip size="x-small" class="time-chip">
{{ formatTime(timeSlot.startTime) }} - {{ formatTime(timeSlot.endTime) }}
</v-chip>
</div>
</td>
<td>
<template v-if="!settings.hideTeacherName">
<div v-for="(item, i) in group.items" :key="i" class="mb-1"> <div v-for="(item, i) in group.items" :key="i" class="mb-1">
{{ item.weeks }} {{ item.teacher || '-' }}
</div> </div>
</td> </template>
</tr> <template v-else>-</template>
</template> </td>
<td>
<template v-if="!settings.hideRoom">
<div v-for="(item, i) in group.items" :key="i" class="mb-1">
{{ item.room || '-' }}
</div>
</template>
<template v-else>-</template>
</td>
<td>
<div v-for="(item, i) in group.items" :key="i" class="mb-1">
{{ item.weeks }}
</div>
</td>
</tr>
</template>
</tbody> </tbody>
</v-table> </v-table>
</v-window-item> </v-window-item>
@ -600,25 +610,25 @@ export default {
totalWeeks: 30 totalWeeks: 30
}, },
tableHeaders: [ tableHeaders: [
{ title: "", key: "data-table-select" }, {title: "", key: "data-table-select"},
{ title: "节次", key: "period" }, {title: "节次", key: "period"},
{ title: "周一", key: "1" }, {title: "周一", key: "1"},
{ title: "周二", key: "2" }, {title: "周二", key: "2"},
{ title: "周三", key: "3" }, {title: "周三", key: "3"},
{ title: "周四", key: "4" }, {title: "周四", key: "4"},
{ title: "周五", key: "5" }, {title: "周五", key: "5"},
{ title: "周六", key: "6" }, {title: "周六", key: "6"},
{ title: "周日", key: "7" }, {title: "周日", key: "7"},
], ],
timeTableHeaders: [ timeTableHeaders: [
{ title: "节次", key: "period" }, {title: "节次", key: "period"},
{ title: "课程", key: "subject" }, {title: "课程", key: "subject"},
{ title: "星期", key: "day" }, {title: "星期", key: "day"},
{ title: "开始时间", key: "startTime" }, {title: "开始时间", key: "startTime"},
{ title: "结束时间", key: "endTime" }, {title: "结束时间", key: "endTime"},
{ title: "教师", key: "teacher" }, {title: "教师", key: "teacher"},
{ title: "教室", key: "room" }, {title: "教室", key: "room"},
{ title: "周次", key: "weeks" }, {title: "周次", key: "weeks"},
], ],
dayNames: { dayNames: {
1: "周一", 1: "周一",
@ -700,7 +710,7 @@ export default {
// //
if (a.period !== b.period) return a.period - b.period; if (a.period !== b.period) return a.period - b.period;
// //
const dayOrder = { "周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7 }; const dayOrder = {"周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7};
return dayOrder[a.day] - dayOrder[b.day]; return dayOrder[a.day] - dayOrder[b.day];
}); });
}, },
@ -845,8 +855,8 @@ export default {
// CSESParser // CSESParser
if (data instanceof CSESParser) { if (data instanceof CSESParser) {
return data.version === 1 && return data.version === 1 &&
Array.isArray(data.subjects) && Array.isArray(data.subjects) &&
Array.isArray(data.schedules); Array.isArray(data.schedules);
} }
// //
@ -865,7 +875,7 @@ export default {
}, },
processCsesData(data) { processCsesData(data) {
const { schedules, subjects } = data; const {schedules, subjects} = data;
// 使使 // 使使
const subjectMap = Object.fromEntries( const subjectMap = Object.fromEntries(
@ -1004,7 +1014,7 @@ export default {
for (const group of periodGroups) { for (const group of periodGroups) {
// //
for (const item of group.items) { for (const item of group.items) {
const dayNumber = { "周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7 }[item.day]; const dayNumber = {"周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7}[item.day];
const teacher = this.settings.hideTeacherName ? "" : (item.teacher || ""); const teacher = this.settings.hideTeacherName ? "" : (item.teacher || "");
const room = this.settings.hideRoom ? "" : (item.room || ""); const room = this.settings.hideRoom ? "" : (item.room || "");
@ -1072,7 +1082,7 @@ export default {
} }
// YAML key: value // YAML key: value
return /^\s*[a-zA-Z0-9_-]+\s*:/.test(trimmed) || return /^\s*[a-zA-Z0-9_-]+\s*:/.test(trimmed) ||
/\n\s*[a-zA-Z0-9_-]+\s*:/.test(trimmed); /\n\s*[a-zA-Z0-9_-]+\s*:/.test(trimmed);
}, },
parseYaml(text) { parseYaml(text) {
@ -1094,7 +1104,7 @@ export default {
// //
const allData = this.getUnfilteredTimeTableData(); const allData = this.getUnfilteredTimeTableData();
return allData.filter(item => { return allData.filter(item => {
const dayNum = { "周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7 }[item.day]; const dayNum = {"周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7}[item.day];
return dayNum === day; return dayNum === day;
}); });
}, },
@ -1165,7 +1175,7 @@ export default {
// //
if (a.period !== b.period) return a.period - b.period; if (a.period !== b.period) return a.period - b.period;
// //
const dayOrder = { "周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7 }; const dayOrder = {"周一": 1, "周二": 2, "周三": 3, "周四": 4, "周五": 5, "周六": 6, "周日": 7};
return dayOrder[a.day] - dayOrder[b.day]; return dayOrder[a.day] - dayOrder[b.day];
}); });
}, },

View File

@ -26,18 +26,18 @@
label="server.authDomain" label="server.authDomain"
/> />
</v-form> </v-form>
<v-divider class="my-4" /> <v-divider class="my-4"/>
<v-btn <v-btn
color="primary"
class="me-2" class="me-2"
color="primary"
@click="applySettings" @click="applySettings"
> >
应用设置 应用设置
</v-btn> </v-btn>
<v-btn <v-btn
color="secondary"
class="me-2" class="me-2"
color="secondary"
@click="clearGuard" @click="clearGuard"
> >
清除重定向守卫 清除重定向守卫
@ -83,9 +83,9 @@
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import {ref, computed} from 'vue'
import { getSetting, setSetting } from '@/utils/settings' import {getSetting, setSetting} from '@/utils/settings'
import { kvServerProvider } from '@/utils/providers/kvServerProvider' import {kvServerProvider} from '@/utils/providers/kvServerProvider'
const REDIRECT_GUARD_KEY = 'kvinit.redirecting' const REDIRECT_GUARD_KEY = 'kvinit.redirecting'
@ -104,7 +104,11 @@ const applySettings = () => {
} }
const clearGuard = () => { const clearGuard = () => {
try { sessionStorage.removeItem(REDIRECT_GUARD_KEY) } catch (e) { console.debug(e) } try {
sessionStorage.removeItem(REDIRECT_GUARD_KEY)
} catch (e) {
console.debug(e)
}
} }
const simulateLoadError = () => { const simulateLoadError = () => {
@ -117,7 +121,11 @@ const simulateLoadError = () => {
} }
const guardRaw = computed(() => { const guardRaw = computed(() => {
try { return sessionStorage.getItem(REDIRECT_GUARD_KEY) } catch (e) { return String(e) } try {
return sessionStorage.getItem(REDIRECT_GUARD_KEY)
} catch (e) {
return String(e)
}
}) })
const settingsDump = computed(() => { const settingsDump = computed(() => {

View File

@ -25,8 +25,8 @@
<v-list-item-subtitle> <v-list-item-subtitle>
<v-chip <v-chip
:color="connected ? 'success' : 'error'" :color="connected ? 'success' : 'error'"
size="small"
class="mr-2" class="mr-2"
size="small"
> >
{{ connected ? 'connected' : 'disconnected' }} {{ connected ? 'connected' : 'disconnected' }}
</v-chip> </v-chip>
@ -42,7 +42,7 @@
<v-list-item-subtitle>{{ currentDataKey }}</v-list-item-subtitle> <v-list-item-subtitle>{{ currentDataKey }}</v-list-item-subtitle>
</v-list-item> </v-list-item>
</v-list> </v-list>
<v-divider class="my-4" /> <v-divider class="my-4"/>
<v-row> <v-row>
<v-col <v-col
cols="12" cols="12"
@ -50,26 +50,26 @@
> >
<v-text-field <v-text-field
v-model="manualToken" v-model="manualToken"
label="手动加入 Token (留空使用配置的 Token)"
clearable clearable
label="手动加入 Token (留空使用配置的 Token)"
/> />
</v-col> </v-col>
<v-col <v-col
class="d-flex align-center"
cols="12" cols="12"
md="4" md="4"
class="d-flex align-center"
> >
<v-btn <v-btn
color="primary"
class="mr-2" class="mr-2"
color="primary"
@click="handleJoinToken(manualToken || currentToken)" @click="handleJoinToken(manualToken || currentToken)"
> >
加入 加入
</v-btn> </v-btn>
<v-btn <v-btn
color="warning"
class="mr-2"
:disabled="!joinedToken" :disabled="!joinedToken"
class="mr-2"
color="warning"
@click="handleLeaveToken(joinedToken)" @click="handleLeaveToken(joinedToken)"
> >
离开当前 离开当前
@ -83,24 +83,24 @@
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
<v-divider class="my-4" /> <v-divider class="my-4"/>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-card variant="tonal" color="primary" border> <v-card border color="primary" variant="tonal">
<v-card-title class="text-subtitle-1">聊天室消息</v-card-title> <v-card-title class="text-subtitle-1">聊天室消息</v-card-title>
<v-card-text> <v-card-text>
<v-textarea <v-textarea
v-model="chatInput" v-model="chatInput"
label="发送到当前已加入的设备频道"
rows="2"
auto-grow auto-grow
clearable clearable
label="发送到当前已加入的设备频道"
rows="2"
/> />
<div class="d-flex"> <div class="d-flex">
<v-spacer /> <v-spacer/>
<v-btn <v-btn
color="primary"
:disabled="!canSendChat" :disabled="!canSendChat"
color="primary"
@click="sendChat" @click="sendChat"
> >
发送聊天 发送聊天
@ -128,8 +128,8 @@
<v-card-title>在线设备</v-card-title> <v-card-title>在线设备</v-card-title>
<v-card-text> <v-card-text>
<v-btn <v-btn
color="primary"
class="mb-3" class="mb-3"
color="primary"
@click="fetchOnline" @click="fetchOnline"
> >
刷新在线列表 刷新在线列表
@ -178,11 +178,11 @@
<v-card border> <v-card border>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
事件日志 事件日志
<v-spacer /> <v-spacer/>
<v-btn <v-btn
color="error"
size="small" size="small"
variant="text" variant="text"
color="error"
@click="clearLogs" @click="clearLogs"
> >
清空 清空
@ -214,8 +214,8 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue' import {ref, onMounted, onBeforeUnmount, computed} from 'vue'
import { getSetting } from '@/utils/settings' import {getSetting} from '@/utils/settings'
import { import {
getSocket, getSocket,
on as socketOn, on as socketOn,
@ -262,17 +262,17 @@ function wireSocketBaseEvents() {
s.on('connect', () => { s.on('connect', () => {
connected.value = true connected.value = true
socketId.value = s.id || '' socketId.value = s.id || ''
pushLog('connect', { id: s.id }) pushLog('connect', {id: s.id})
// re-join with token if set // re-join with token if set
if (joinedToken.value) joinToken(joinedToken.value) if (joinedToken.value) joinToken(joinedToken.value)
}) })
s.on('disconnect', (reason) => { s.on('disconnect', (reason) => {
connected.value = false connected.value = false
pushLog('disconnect', { reason }) pushLog('disconnect', {reason})
}) })
s.on('connect_error', (err) => pushLog('connect_error', { message: err?.message })) s.on('connect_error', (err) => pushLog('connect_error', {message: err?.message}))
s.on('reconnect_attempt', (n) => pushLog('reconnect_attempt', { attempt: n })) s.on('reconnect_attempt', (n) => pushLog('reconnect_attempt', {attempt: n}))
s.on('reconnect', (n) => pushLog('reconnect', { attempt: n })) s.on('reconnect', (n) => pushLog('reconnect', {attempt: n}))
} }
function wireBusinessEvents() { function wireBusinessEvents() {
@ -306,7 +306,7 @@ function handleJoinToken(token) {
} }
joinToken(token) joinToken(token)
joinedToken.value = token joinedToken.value = token
pushLog('join-token', { token }) pushLog('join-token', {token})
} catch (e) { } catch (e) {
pushLog('join-token-error', String(e)) pushLog('join-token-error', String(e))
} }
@ -316,7 +316,7 @@ function handleLeaveToken(token) {
try { try {
leaveToken(token) leaveToken(token)
if (joinedToken.value === token) joinedToken.value = '' if (joinedToken.value === token) joinedToken.value = ''
pushLog('leave-token', { token }) pushLog('leave-token', {token})
} catch (e) { } catch (e) {
pushLog('leave-token-error', String(e)) pushLog('leave-token-error', String(e))
} }
@ -353,7 +353,7 @@ function sendChat() {
const s = getSocket() const s = getSocket()
// send as plain string per server contract // send as plain string per server contract
s.emit('chat:send', text) s.emit('chat:send', text)
pushLog('chat:send', { text }) pushLog('chat:send', {text})
chatInput.value = '' chatInput.value = ''
} catch (e) { } catch (e) {
pushLog('chat:error', String(e)) pushLog('chat:error', String(e))
@ -373,7 +373,7 @@ async function fetchOnline() {
const resp = await fetch(`${serverUrl.value}/devices/online`) const resp = await fetch(`${serverUrl.value}/devices/online`)
const data = await resp.json() const data = await resp.json()
onlineDevices.value = Array.isArray(data?.devices) ? data.devices : [] onlineDevices.value = Array.isArray(data?.devices) ? data.devices : []
pushLog('fetch-online', { count: onlineDevices.value.length }) pushLog('fetch-online', {count: onlineDevices.value.length})
} catch (e) { } catch (e) {
pushLog('fetch-online-error', String(e)) pushLog('fetch-online-error', String(e))
} }

View File

@ -11,12 +11,12 @@
<v-app-bar-title class="text-h6"> <v-app-bar-title class="text-h6">
编辑考试配置 编辑考试配置
</v-app-bar-title> </v-app-bar-title>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
color="success"
variant="outlined"
prepend-icon="mdi-content-save"
:loading="saving" :loading="saving"
color="success"
prepend-icon="mdi-content-save"
variant="outlined"
@click="save" @click="save"
> >
保存 保存
@ -31,8 +31,8 @@
v-if="id" v-if="id"
ref="editor" ref="editor"
:config-id="id" :config-id="id"
@saved="onSaved"
@error="onError" @error="onError"
@saved="onSaved"
/> />
</v-container> </v-container>
</v-container> </v-container>
@ -43,7 +43,7 @@ import ExamConfigEditor from '@/components/ExamConfigEditor.vue'
export default { export default {
name: 'ExamEditorPage', name: 'ExamEditorPage',
components: { ExamConfigEditor }, components: {ExamConfigEditor},
data() { data() {
return { return {
id: this.$route.params.id, id: this.$route.params.id,

View File

@ -1,30 +1,30 @@
<template> <template>
<v-alert <v-alert
v-if="error" v-if="error"
type="error"
variant="tonal"
border="start" border="start"
class="mb-4" class="mb-4"
closable closable
type="error"
variant="tonal"
@click:close="error = ''" @click:close="error = ''"
> >
{{ error }} {{ error }}
</v-alert> </v-alert>
<v-skeleton-loader v-if="loading" type="article" /> <v-skeleton-loader v-if="loading" type="article"/>
<div v-else-if="!config"> <div v-else-if="!config">
<v-alert type="warning" variant="tonal" border="start"> <v-alert border="start" type="warning" variant="tonal">
缺少配置请通过 URL 参数 id url 传入配置 缺少配置请通过 URL 参数 id url 传入配置
</v-alert> </v-alert>
</div> </div>
<div v-else> <div v-else>
<div class="player" ref="playerRef"> <div ref="playerRef" class="player">
<ExamPlayer <ExamPlayer
v-model:room-number="roomNumberLocal" v-model:room-number="roomNumberLocal"
:exam-config="config"
:config="playerConfigObj" :config="playerConfigObj"
:exam-config="config"
:show-action-bar="true" :show-action-bar="true"
:time-sync-status="'电脑时间'" :time-sync-status="'电脑时间'"
@exit="exit()" @exit="exit()"
@ -34,14 +34,14 @@
</template> </template>
<script> <script>
import { ref, computed, onMounted, watch } from "vue"; import {ref, computed, onMounted, watch} from "vue";
import { useRoute, useRouter } from "vue-router"; import {useRoute, useRouter} from "vue-router";
import dataProvider from "@/utils/dataProvider"; import dataProvider from "@/utils/dataProvider";
import { ExamPlayer } from "@examaware-cs/player"; import {ExamPlayer} from "@examaware-cs/player";
export default { export default {
name: "ExamPlayerPage", name: "ExamPlayerPage",
components: { ExamPlayer }, components: {ExamPlayer},
setup() { setup() {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -103,11 +103,11 @@ export default {
// ExamAware examInfos: { name, start, end, alertTime? } // ExamAware examInfos: { name, start, end, alertTime? }
examInfos: Array.isArray(raw?.examInfos) examInfos: Array.isArray(raw?.examInfos)
? raw.examInfos.map((i) => ({ ? raw.examInfos.map((i) => ({
name: i?.name || "未命名科目", name: i?.name || "未命名科目",
start: i?.start || "", start: i?.start || "",
end: i?.end || "", end: i?.end || "",
alertTime: typeof i?.alertTime === "number" ? i.alertTime : 15, alertTime: typeof i?.alertTime === "number" ? i.alertTime : 15,
})) }))
: [], : [],
}; };
} }
@ -143,7 +143,7 @@ export default {
@font-face { @font-face {
font-family: "TCloudNumber"; font-family: "TCloudNumber";
src: url("../assets/fonts/TCloudNumberVF.ttf") format("truetype-variations"), src: url("../assets/fonts/TCloudNumberVF.ttf") format("truetype-variations"),
url("../assets/fonts/TCloudNumberVF.ttf") format("truetype"); url("../assets/fonts/TCloudNumberVF.ttf") format("truetype");
font-weight: 100 900; font-weight: 100 900;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@ -156,8 +156,8 @@ body,
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: "MiSans", MiSans, -apple-system, BlinkMacSystemFont, "Segoe UI", font-family: "MiSans", MiSans, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans",
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
} }
/* 设置主题为深色 */ /* 设置主题为深色 */

View File

@ -2,9 +2,9 @@
<v-container class="fill-height"> <v-container class="fill-height">
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-card class="elevation-12" border> <v-card border class="elevation-12">
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4"> <v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
<v-icon color="white" class="mr-2">mdi-calendar-check</v-icon> <v-icon class="mr-2" color="white">mdi-calendar-check</v-icon>
考试看板 考试看板
</v-card-title> </v-card-title>
<v-card-subtitle> <v-card-subtitle>
@ -14,11 +14,11 @@
<!-- 错误提示 --> <!-- 错误提示 -->
<v-alert <v-alert
v-if="error" v-if="error"
type="error"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start" border="start"
class="mb-4 mt-3 mx-2"
closable closable
type="error"
variant="tonal"
@click:close="error = ''" @click:close="error = ''"
> >
<div class="d-flex align-center"> <div class="d-flex align-center">
@ -30,11 +30,11 @@
<!-- 成功提示 --> <!-- 成功提示 -->
<v-alert <v-alert
v-if="success" v-if="success"
type="success"
class="mb-4 mt-3 mx-2"
variant="tonal"
border="start" border="start"
class="mb-4 mt-3 mx-2"
closable closable
type="success"
variant="tonal"
@click:close="success = ''" @click:close="success = ''"
> >
<div class="d-flex align-center"> <div class="d-flex align-center">
@ -47,19 +47,19 @@
<div class="d-flex justify-space-between align-center mb-4"> <div class="d-flex justify-space-between align-center mb-4">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-btn <v-btn
class="mr-2"
color="primary" color="primary"
prepend-icon="mdi-plus" prepend-icon="mdi-plus"
class="mr-2"
@click="createNewConfig" @click="createNewConfig"
> >
新建配置 新建配置
</v-btn> </v-btn>
<v-btn <v-btn
:loading="loading"
color="info" color="info"
prepend-icon="mdi-refresh" prepend-icon="mdi-refresh"
:loading="loading"
@click="loadConfigs"
variant="outlined" variant="outlined"
@click="loadConfigs"
> >
刷新 刷新
</v-btn> </v-btn>
@ -77,8 +77,8 @@
<v-card v-if="loading" class="my-4" outlined> <v-card v-if="loading" class="my-4" outlined>
<v-card-text> <v-card-text>
<v-skeleton-loader <v-skeleton-loader
type="list-item-avatar-two-line@3"
class="mx-auto" class="mx-auto"
type="list-item-avatar-two-line@3"
></v-skeleton-loader> ></v-skeleton-loader>
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -98,7 +98,7 @@
@click="showEditDialog(config)" @click="showEditDialog(config)"
> >
<template #prepend> <template #prepend>
<v-avatar color="primary" class="mr-2"> <v-avatar class="mr-2" color="primary">
<v-icon color="white">mdi-calendar-text</v-icon> <v-icon color="white">mdi-calendar-text</v-icon>
</v-avatar> </v-avatar>
</template> </template>
@ -108,11 +108,11 @@
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle class="text-caption mt-1"> <v-list-item-subtitle class="text-caption mt-1">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-icon size="small" class="mr-1">mdi-information-outline</v-icon> <v-icon class="mr-1" size="small">mdi-information-outline</v-icon>
{{ config.message || '无描述' }} {{ config.message || '无描述' }}
</div> </div>
<div class="d-flex align-center mt-1"> <div class="d-flex align-center mt-1">
<v-icon size="small" class="mr-1">mdi-book-multiple</v-icon> <v-icon class="mr-1" size="small">mdi-book-multiple</v-icon>
{{ config.examInfos ? config.examInfos.length : 0 }} 堂考试 {{ config.examInfos ? config.examInfos.length : 0 }} 堂考试
</div> </div>
</v-list-item-subtitle> </v-list-item-subtitle>
@ -120,22 +120,22 @@
<template #append> <template #append>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-btn <v-btn
class="mr-1"
color="primary"
icon="mdi-pencil" icon="mdi-pencil"
size="small" size="small"
color="primary"
variant="text" variant="text"
class="mr-1"
@click="showEditDialog(config)" @click="showEditDialog(config)"
> >
<v-icon>mdi-pencil</v-icon> <v-icon>mdi-pencil</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
class="mr-1"
color="info"
icon="mdi-eye" icon="mdi-eye"
size="small" size="small"
color="info"
variant="text" variant="text"
class="mr-1"
@click="showEditDialog(config)" @click="showEditDialog(config)"
> >
<v-icon>mdi-eye</v-icon> <v-icon>mdi-eye</v-icon>
@ -151,7 +151,7 @@
<!-- 空状态 --> <!-- 空状态 -->
<v-card v-if="!loading && configs.length === 0" class="my-4" elevation="1"> <v-card v-if="!loading && configs.length === 0" class="my-4" elevation="1">
<v-card-text class="text-center py-8"> <v-card-text class="text-center py-8">
<v-icon size="64" color="grey-lighten-1" class="mb-4"> <v-icon class="mb-4" color="grey-lighten-1" size="64">
mdi-calendar-blank mdi-calendar-blank
</v-icon> </v-icon>
<h3 class="text-h6 mb-2 text-grey-darken-1">暂无配置</h3> <h3 class="text-h6 mb-2 text-grey-darken-1">暂无配置</h3>
@ -176,16 +176,16 @@
<v-dialog v-model="renameDialog" max-width="500"> <v-dialog v-model="renameDialog" max-width="500">
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-rename-box</v-icon> <v-icon class="mr-2" color="primary">mdi-rename-box</v-icon>
重命名配置 重命名配置
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="newConfigName" v-model="newConfigName"
:rules="[v => !!v || '配置名称不能为空']"
label="配置名称" label="配置名称"
prepend-inner-icon="mdi-calendar-text" prepend-inner-icon="mdi-calendar-text"
variant="outlined" variant="outlined"
:rules="[v => !!v || '配置名称不能为空']"
@keyup.enter="renameConfig" @keyup.enter="renameConfig"
></v-text-field> ></v-text-field>
</v-card-text> </v-card-text>
@ -199,11 +199,11 @@
取消 取消
</v-btn> </v-btn>
<v-btn <v-btn
:disabled="!newConfigName"
:loading="renaming"
color="primary" color="primary"
variant="outlined" variant="outlined"
:loading="renaming"
@click="renameConfig" @click="renameConfig"
:disabled="!newConfigName"
> >
确认 确认
</v-btn> </v-btn>
@ -215,22 +215,22 @@
<v-dialog v-model="editDialog" max-width="1200" persistent> <v-dialog v-model="editDialog" max-width="1200" persistent>
<v-card> <v-card>
<v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4"> <v-card-title class="d-flex align-center primary lighten-1 white--text py-3 px-4">
<v-icon color="white" class="mr-2">mdi-pencil</v-icon> <v-icon class="mr-2" color="white">mdi-pencil</v-icon>
编辑考试配置 编辑考试配置
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-chip <v-chip
v-if="editingConfig" v-if="editingConfig"
color="white"
text-color="primary"
size="small"
class="mr-2" class="mr-2"
color="white"
size="small"
text-color="primary"
> >
ID: {{ editingConfig.id }} ID: {{ editingConfig.id }}
</v-chip> </v-chip>
<v-btn <v-btn
icon="mdi-close"
color="white" color="white"
icon="mdi-close"
variant="text" variant="text"
@click="closeEditDialog" @click="closeEditDialog"
> >
@ -238,33 +238,33 @@
</v-btn> </v-btn>
</v-card-title> </v-card-title>
<v-card-text class="pa-4" <v-card-text class="pa-4"
style="max-height: 70vh; overflow-y: auto;"> style="max-height: 70vh; overflow-y: auto;">
<ExamConfigEditor <ExamConfigEditor
v-if="editingConfig" v-if="editingConfig"
:config-id="editingConfig.id"
ref="configEditor" ref="configEditor"
:config-id="editingConfig.id"
:dialog-mode="true" :dialog-mode="true"
@saved="onConfigSaved" @deleted="onConfigDeleted"
@error="onConfigError" @error="onConfigError"
@opened="onConfigOpened" @opened="onConfigOpened"
@deleted="onConfigDeleted" @saved="onConfigSaved"
/> />
</v-card-text> </v-card-text>
<v-card-actions class="pa-4"> <v-card-actions class="pa-4">
<v-btn <v-btn
color="grey" color="grey"
variant="outlined"
prepend-icon="mdi-close" prepend-icon="mdi-close"
variant="outlined"
@click="closeEditDialog" @click="closeEditDialog"
> >
关闭 关闭
</v-btn> </v-btn>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
color="success"
variant="outlined"
prepend-icon="mdi-content-save"
:loading="saving" :loading="saving"
color="success"
prepend-icon="mdi-content-save"
variant="outlined"
@click="saveConfigInDialog" @click="saveConfigInDialog"
> >
保存配置 保存配置
@ -277,7 +277,7 @@ style="max-height: 70vh; overflow-y: auto;">
<script> <script>
import dataProvider from '@/utils/dataProvider' import dataProvider from '@/utils/dataProvider'
import { getSetting } from '@/utils/settings' import {getSetting} from '@/utils/settings'
import ExamConfigEditor from '@/components/ExamConfigEditor.vue' import ExamConfigEditor from '@/components/ExamConfigEditor.vue'
export default { export default {
@ -358,12 +358,12 @@ export default {
] ]
// //
const configList = exampleConfigs.map(c => ({ id: c.id })) const configList = exampleConfigs.map(c => ({id: c.id}))
await dataProvider.saveData('es_list', configList) await dataProvider.saveData('es_list', configList)
// //
for (let config of exampleConfigs) { for (let config of exampleConfigs) {
const configData = { ...config } const configData = {...config}
delete configData.id delete configData.id
await dataProvider.saveData(`es_${config.id}`, configData) await dataProvider.saveData(`es_${config.id}`, configData)
} }
@ -444,7 +444,7 @@ export default {
}) })
// //
const currentList = this.configs.map(c => ({ id: c.id })) const currentList = this.configs.map(c => ({id: c.id}))
const listResponse = await dataProvider.saveData('es_list', currentList) const listResponse = await dataProvider.saveData('es_list', currentList)
if (!listResponse) { if (!listResponse) {
throw new Error(listResponse.error?.message || '更新列表失败') throw new Error(listResponse.error?.message || '更新列表失败')
@ -464,11 +464,6 @@ export default {
}, },
/** /**
* 显示重命名对话框 * 显示重命名对话框
*/ */
@ -534,7 +529,6 @@ export default {
}, },
/** /**
* 在弹框中保存配置 * 在弹框中保存配置
*/ */

View File

@ -4,16 +4,16 @@
{{ titleText }} {{ titleText }}
</v-app-bar-title> </v-app-bar-title>
<v-spacer /> <v-spacer/>
<template #append> <template #append>
<!-- 只读 Token 警告 --> <!-- 只读 Token 警告 -->
<v-chip <v-chip
v-if="tokenDisplayInfo.readonly" v-if="tokenDisplayInfo.readonly"
color="warning"
variant="tonal"
class="mx-2" class="mx-2"
color="warning"
prepend-icon="mdi-lock-alert" prepend-icon="mdi-lock-alert"
variant="tonal"
> >
只读 只读
</v-chip> </v-chip>
@ -21,25 +21,25 @@
<!-- 学生名称显示 chip始终蓝色 --> <!-- 学生名称显示 chip始终蓝色 -->
<v-chip <v-chip
v-if="tokenDisplayInfo.show" v-if="tokenDisplayInfo.show"
color="primary"
variant="tonal"
class="mx-2"
prepend-icon="mdi-account"
:style="{ cursor: tokenDisplayInfo.disabled ? 'default' : 'pointer' }" :style="{ cursor: tokenDisplayInfo.disabled ? 'default' : 'pointer' }"
class="mx-2"
color="primary"
prepend-icon="mdi-account"
variant="tonal"
@click="handleTokenChipClick" @click="handleTokenChipClick"
> >
{{ tokenDisplayInfo.text }} {{ tokenDisplayInfo.text }}
</v-chip> </v-chip>
<v-btn icon="mdi-chat" variant="text" @click="isChatOpen = true" /> <v-btn icon="mdi-chat" variant="text" @click="isChatOpen = true"/>
<v-btn <v-btn
icon="mdi-bell"
variant="text"
:badge="unreadCount || undefined" :badge="unreadCount || undefined"
:badge-color="unreadCount ? 'error' : undefined" :badge-color="unreadCount ? 'error' : undefined"
icon="mdi-bell"
variant="text"
@click="$refs.messageLog.drawer = true" @click="$refs.messageLog.drawer = true"
/> />
<v-btn icon="mdi-cog" variant="text" @click="$router.push('/settings')" /> <v-btn icon="mdi-cog" variant="text" @click="$router.push('/settings')"/>
</template> </template>
</v-app-bar> </v-app-bar>
<!-- 初始化选择卡片仅在首页且需要授权时显示不影响顶栏 --> <!-- 初始化选择卡片仅在首页且需要授权时显示不影响顶栏 -->
@ -65,17 +65,17 @@
<div <div
v-for="item in sortedItems" v-for="item in sortedItems"
:key="item.key" :key="item.key"
class="grid-item"
:style="{ :style="{
'grid-row-end': `span ${item.rowSpan}`, 'grid-row-end': `span ${item.rowSpan}`,
order: item.order, order: item.order,
}" }"
class="grid-item"
> >
<v-card <v-card
border
height="100%"
class="glow-track"
:class="{ 'glow-highlight': highlightedCards[item.key] }" :class="{ 'glow-highlight': highlightedCards[item.key] }"
border
class="glow-track"
height="100%"
@click="!isEditingDisabled && openDialog(item.key)" @click="!isEditingDisabled && openDialog(item.key)"
@mousemove="handleMouseMove" @mousemove="handleMouseMove"
@touchmove="handleTouchMove" @touchmove="handleTouchMove"
@ -106,7 +106,7 @@
:disabled="isEditingDisabled" :disabled="isEditingDisabled"
@click="openDialog(subject.name)" @click="openDialog(subject.name)"
> >
<v-icon start> mdi-plus </v-icon> <v-icon start> mdi-plus</v-icon>
{{ subject.name }} {{ subject.name }}
</v-btn> </v-btn>
</v-btn-group> </v-btn-group>
@ -116,16 +116,16 @@
<v-card <v-card
v-for="subject in unusedSubjects" v-for="subject in unusedSubjects"
:key="subject.name" :key="subject.name"
:disabled="isEditingDisabled"
border border
class="empty-subject-card" class="empty-subject-card"
:disabled="isEditingDisabled"
@click="openDialog(subject.name)" @click="openDialog(subject.name)"
> >
<v-card-title class="text-subtitle-1"> <v-card-title class="text-subtitle-1">
{{ subject.name }} {{ subject.name }}
</v-card-title> </v-card-title>
<v-card-text class="text-center"> <v-card-text class="text-center">
<v-icon size="small" color="grey"> mdi-plus </v-icon> <v-icon color="grey" size="small"> mdi-plus</v-icon>
<div class="text-caption text-grey">点击添加作业</div> <div class="text-caption text-grey">点击添加作业</div>
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -134,10 +134,10 @@
</div> </div>
<v-btn <v-btn
v-if="!state.synced" v-if="!state.synced"
color="error"
size="large"
:loading="loading.upload" :loading="loading.upload"
class="ml-2" class="ml-2"
color="error"
size="large"
@click="manualUpload" @click="manualUpload"
> >
上传 上传
@ -147,31 +147,31 @@
</v-btn> </v-btn>
<v-btn <v-btn
v-if="showRandomPickerButton" v-if="showRandomPickerButton"
append-icon="mdi-dice-multiple"
class="ml-2"
color="amber" color="amber"
prepend-icon="mdi-account-question" prepend-icon="mdi-account-question"
append-icon="mdi-dice-multiple"
size="large" size="large"
class="ml-2"
@click="openRandomPicker" @click="openRandomPicker"
> >
随机点名 随机点名
</v-btn> </v-btn>
<v-btn <v-btn
v-if="showExamScheduleButton" v-if="showExamScheduleButton"
class="ml-2"
color="green" color="green"
prepend-icon="mdi-calendar-check" prepend-icon="mdi-calendar-check"
size="large" size="large"
class="ml-2"
@click="$router.push('/examschedule')" @click="$router.push('/examschedule')"
> >
考试看板 考试看板
</v-btn> </v-btn>
<v-btn <v-btn
v-if="showListCardButton" v-if="showListCardButton"
class="ml-2"
color="primary-darken-1" color="primary-darken-1"
prepend-icon="mdi-list-box" prepend-icon="mdi-list-box"
size="large" size="large"
class="ml-2"
@click="$router.push('/list')" @click="$router.push('/list')"
> >
列表 列表
@ -182,8 +182,8 @@
:prepend-icon=" :prepend-icon="
state.isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' state.isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'
" "
size="large"
class="ml-2" class="ml-2"
size="large"
@click="toggleFullscreen" @click="toggleFullscreen"
> >
{{ state.isFullscreen ? "退出全屏" : "全屏显示" {{ state.isFullscreen ? "退出全屏" : "全屏显示"
@ -197,7 +197,7 @@
variant="tonal" variant="tonal"
> >
<v-card-title class="text-subtitle-1"> <v-card-title class="text-subtitle-1">
<v-icon start icon="mdi-shield-check" size="small" /> <v-icon icon="mdi-shield-check" size="small" start/>
屏幕保护技术已启用 屏幕保护技术已启用
</v-card-title> </v-card-title>
<v-card-text class="text-body-2"> <v-card-text class="text-body-2">
@ -206,10 +206,10 @@
</p> </p>
<p class="text-caption text-grey"> <p class="text-caption text-grey">
*研究显示动态像素偏移技术可以修复屏幕坏点起到保护屏幕的作用数据来自实验室<a *研究显示动态像素偏移技术可以修复屏幕坏点起到保护屏幕的作用数据来自实验室<a
href="https://patentscope.wipo.int/search/zh/detail.jsf?docId=CN232281523&_cid=P20-M8L0YX-67061-1" href="https://patentscope.wipo.int/search/zh/detail.jsf?docId=CN232281523&_cid=P20-M8L0YX-67061-1"
target="_blank" target="_blank"
>专利号CN108648692 >专利号CN108648692
</a> </a>
</p> </p>
<p class="text-caption text-grey"> <p class="text-caption text-grey">
*技术已自动适配您的设备无需手动调整 *技术已自动适配您的设备无需手动调整
@ -220,71 +220,76 @@
<!-- 出勤统计区域 --> <!-- 出勤统计区域 -->
<v-col <v-col
v-ripple="{ class: `text-${['primary','secondary','info','success','warning','error'][Math.floor(Math.random()*6)]}` }"
v-if="state.studentList && state.studentList.length" v-if="state.studentList && state.studentList.length"
v-ripple="{ class: `text-${['primary','secondary','info','success','warning','error'][Math.floor(Math.random()*6)]}` }"
class="attendance-area no-select" class="attendance-area no-select"
cols="1" cols="1"
@click="setAttendanceArea()" @click="setAttendanceArea()"
> >
<h1>出勤</h1> <h1>出勤</h1>
<h2> <h2>
<snap style="white-space: nowrap"> 应到 </snap>: <snap style="white-space: nowrap"> 应到</snap>
:
<snap style="white-space: nowrap"> <snap style="white-space: nowrap">
{{ {{
state.studentList.length - state.studentList.length -
state.boardData.attendance.exclude.length state.boardData.attendance.exclude.length
}} }}
</snap> </snap>
</h2> </h2>
<h2> <h2>
<snap style="white-space: nowrap"> 实到 </snap>: <snap style="white-space: nowrap"> 实到</snap>
:
<snap style="white-space: nowrap"> <snap style="white-space: nowrap">
{{ {{
state.studentList.length - state.studentList.length -
state.boardData.attendance.absent.length - state.boardData.attendance.absent.length -
state.boardData.attendance.late.length - state.boardData.attendance.late.length -
state.boardData.attendance.exclude.length state.boardData.attendance.exclude.length
}} }}
</snap> </snap>
</h2> </h2>
<h2> <h2>
<snap style="white-space: nowrap"> 请假 </snap>: <snap style="white-space: nowrap"> 请假</snap>
:
<snap style="white-space: nowrap"> <snap style="white-space: nowrap">
{{ state.boardData.attendance.absent.length }} {{ state.boardData.attendance.absent.length }}
</snap> </snap>
</h2> </h2>
<h3 <h3
class="gray-text"
v-for="(name, index) in state.boardData.attendance.absent" v-for="(name, index) in state.boardData.attendance.absent"
:key="'absent-' + index" :key="'absent-' + index"
class="gray-text"
> >
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span <span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span
><span style="white-space: nowrap">{{ name }}</span> ><span style="white-space: nowrap">{{ name }}</span>
</h3> </h3>
<h2> <h2>
<snap style="white-space: nowrap">迟到</snap>: <snap style="white-space: nowrap">迟到</snap>
:
<snap style="white-space: nowrap"> <snap style="white-space: nowrap">
{{ state.boardData.attendance.late.length }} {{ state.boardData.attendance.late.length }}
</snap> </snap>
</h2> </h2>
<h3 <h3
class="gray-text"
v-for="(name, index) in state.boardData.attendance.late" v-for="(name, index) in state.boardData.attendance.late"
:key="'late-' + index" :key="'late-' + index"
class="gray-text"
> >
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span <span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span
><span style="white-space: nowrap">{{ name }}</span> ><span style="white-space: nowrap">{{ name }}</span>
</h3> </h3>
<h2> <h2>
<snap style="white-space: nowrap">不参与</snap>: <snap style="white-space: nowrap">不参与</snap>
:
<snap style="white-space: nowrap"> <snap style="white-space: nowrap">
{{ state.boardData.attendance.exclude.length }} {{ state.boardData.attendance.exclude.length }}
</snap> </snap>
</h2> </h2>
<h3 <h3
class="gray-text"
v-for="(name, index) in state.boardData.attendance.exclude" v-for="(name, index) in state.boardData.attendance.exclude"
:key="'exclude-' + index" :key="'exclude-' + index"
class="gray-text"
> >
<span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span <span v-if="useDisplay().lgAndUp.value">{{ `${index + 1}. ` }}</span
><span style="white-space: nowrap">{{ name }}</span> ><span style="white-space: nowrap">{{ name }}</span>
@ -294,9 +299,9 @@
<homework-edit-dialog <homework-edit-dialog
v-model="state.dialogVisible" v-model="state.dialogVisible"
:title="state.dialogTitle"
:initial-content="state.textarea"
:auto-save="autoSave" :auto-save="autoSave"
:initial-content="state.textarea"
:title="state.dialogTitle"
@save="handleHomeworkSave" @save="handleHomeworkSave"
/> />
@ -306,16 +311,16 @@
<v-dialog <v-dialog
v-model="state.attendanceDialog" v-model="state.attendanceDialog"
max-width="900"
fullscreen-breakpoint="sm" fullscreen-breakpoint="sm"
max-width="900"
@update:model-value="handleAttendanceDialogClose" @update:model-value="handleAttendanceDialogClose"
> >
<v-card> <v-card>
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon icon="mdi-account-group" class="mr-2" /> <v-icon class="mr-2" icon="mdi-account-group"/>
出勤状态管理 出勤状态管理
<v-spacer /> <v-spacer/>
<v-chip color="primary" size="small" class="ml-2"> <v-chip class="ml-2" color="primary" size="small">
{{ state.dateString }} {{ state.dateString }}
</v-chip> </v-chip>
</v-card-title> </v-card-title>
@ -326,11 +331,11 @@
<v-col cols="12" md="12"> <v-col cols="12" md="12">
<v-text-field <v-text-field
v-model="attendanceSearch" v-model="attendanceSearch"
prepend-inner-icon="mdi-magnify"
label="搜索学生"
hint="支持筛选姓氏,如输入'孙'可筛选所有姓孙的学生"
variant="outlined"
clearable clearable
hint="支持筛选姓氏,如输入'孙'可筛选所有姓孙的学生"
label="搜索学生"
prepend-inner-icon="mdi-magnify"
variant="outlined"
@update:model-value="handleSearchChange" @update:model-value="handleSearchChange"
/> />
@ -339,10 +344,10 @@
<v-btn <v-btn
v-for="surname in extractedSurnames" v-for="surname in extractedSurnames"
:key="surname.name" :key="surname.name"
:color="attendanceSearch === surname.name ? 'primary' : ''"
:variant=" :variant="
attendanceSearch === surname.name ? 'elevated' : 'text' attendanceSearch === surname.name ? 'elevated' : 'text'
" "
:color="attendanceSearch === surname.name ? 'primary' : ''"
@click=" @click="
attendanceSearch = attendanceSearch =
attendanceSearch === surname.name ? '' : surname.name attendanceSearch === surname.name ? '' : surname.name
@ -359,63 +364,63 @@
<div class="d-flex flex-wrap mb-4 gap-2"> <div class="d-flex flex-wrap mb-4 gap-2">
<div> <div>
<v-chip <v-chip
value="present" :append-icon="
attendanceFilter.includes('present') ? 'mdi-check' : ''
"
:color="attendanceFilter.includes('present') ? 'success' : ''" :color="attendanceFilter.includes('present') ? 'success' : ''"
:variant=" :variant="
attendanceFilter.includes('present') ? 'elevated' : 'tonal' attendanceFilter.includes('present') ? 'elevated' : 'tonal'
" "
class="px-2 filter-chip" class="px-2 filter-chip"
@click="toggleFilter('present')"
prepend-icon="mdi-account-check" prepend-icon="mdi-account-check"
:append-icon=" value="present"
attendanceFilter.includes('present') ? 'mdi-check' : '' @click="toggleFilter('present')"
"
> >
到课 到课
</v-chip> </v-chip>
<v-chip <v-chip
value="absent" :append-icon="
attendanceFilter.includes('absent') ? 'mdi-check' : ''
"
:color="attendanceFilter.includes('absent') ? 'error' : ''" :color="attendanceFilter.includes('absent') ? 'error' : ''"
:variant=" :variant="
attendanceFilter.includes('absent') ? 'elevated' : 'tonal' attendanceFilter.includes('absent') ? 'elevated' : 'tonal'
" "
class="px-2 filter-chip" class="px-2 filter-chip"
@click="toggleFilter('absent')"
prepend-icon="mdi-account-off" prepend-icon="mdi-account-off"
:append-icon=" value="absent"
attendanceFilter.includes('absent') ? 'mdi-check' : '' @click="toggleFilter('absent')"
"
> >
请假 请假
</v-chip> </v-chip>
<v-chip <v-chip
value="late" :append-icon="
attendanceFilter.includes('late') ? 'mdi-check' : ''
"
:color="attendanceFilter.includes('late') ? 'warning' : ''" :color="attendanceFilter.includes('late') ? 'warning' : ''"
:variant=" :variant="
attendanceFilter.includes('late') ? 'elevated' : 'tonal' attendanceFilter.includes('late') ? 'elevated' : 'tonal'
" "
class="px-2 filter-chip" class="px-2 filter-chip"
@click="toggleFilter('late')"
prepend-icon="mdi-clock-alert" prepend-icon="mdi-clock-alert"
:append-icon=" value="late"
attendanceFilter.includes('late') ? 'mdi-check' : '' @click="toggleFilter('late')"
"
> >
迟到 迟到
</v-chip> </v-chip>
<v-chip <v-chip
value="exclude" :append-icon="
attendanceFilter.includes('exclude') ? 'mdi-check' : ''
"
:color="attendanceFilter.includes('exclude') ? 'grey' : ''" :color="attendanceFilter.includes('exclude') ? 'grey' : ''"
:variant=" :variant="
attendanceFilter.includes('exclude') ? 'elevated' : 'tonal' attendanceFilter.includes('exclude') ? 'elevated' : 'tonal'
" "
class="px-2 filter-chip" class="px-2 filter-chip"
@click="toggleFilter('exclude')"
prepend-icon="mdi-account-cancel" prepend-icon="mdi-account-cancel"
:append-icon=" value="exclude"
attendanceFilter.includes('exclude') ? 'mdi-check' : '' @click="toggleFilter('exclude')"
"
> >
不参与 不参与
</v-chip> </v-chip>
@ -428,11 +433,11 @@
v-for="student in filteredStudents" v-for="student in filteredStudents"
:key="student" :key="student"
cols="12" cols="12"
sm="6"
md="6"
lg="4" lg="4"
md="6"
sm="6"
> >
<v-card class="student-card" border> <v-card border class="student-card">
<v-card-text class="d-flex align-center pa-2"> <v-card-text class="d-flex align-center pa-2">
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="d-flex align-center"> <div class="d-flex align-center">
@ -442,12 +447,13 @@
state.studentList.indexOf(student) state.studentList.indexOf(student)
) )
" "
size="24"
class="mr-2" class="mr-2"
size="24"
> >
<v-icon size="small">{{ <v-icon size="small">{{
getStudentStatusIcon(state.studentList.indexOf(student)) getStudentStatusIcon(state.studentList.indexOf(student))
}}</v-icon> }}
</v-icon>
</v-avatar> </v-avatar>
<div class="text-subtitle-1">{{ student }}</div> <div class="text-subtitle-1">{{ student }}</div>
</div> </div>
@ -459,11 +465,11 @@
? 'success' ? 'success'
: '' : ''
" "
:title="'设为到课'"
icon="mdi-account-check" icon="mdi-account-check"
size="small" size="small"
variant="text" variant="text"
@click="setPresent(state.studentList.indexOf(student))" @click="setPresent(state.studentList.indexOf(student))"
:title="'设为到课'"
/> />
<v-btn <v-btn
:color=" :color="
@ -471,11 +477,11 @@
? 'error' ? 'error'
: '' : ''
" "
:title="'设为请假'"
icon="mdi-account-off" icon="mdi-account-off"
size="small" size="small"
variant="text" variant="text"
@click="setAbsent(state.studentList.indexOf(student))" @click="setAbsent(state.studentList.indexOf(student))"
:title="'设为请假'"
/> />
<v-btn <v-btn
:color=" :color="
@ -483,11 +489,11 @@
? 'warning' ? 'warning'
: '' : ''
" "
:title="'设为迟到'"
icon="mdi-clock-alert" icon="mdi-clock-alert"
size="small" size="small"
variant="text" variant="text"
@click="setLate(state.studentList.indexOf(student))" @click="setLate(state.studentList.indexOf(student))"
:title="'设为迟到'"
/> />
<v-btn <v-btn
:color=" :color="
@ -495,11 +501,11 @@
? 'grey' ? 'grey'
: '' : ''
" "
:title="'设为不参与'"
icon="mdi-account-cancel" icon="mdi-account-cancel"
size="small" size="small"
variant="text" variant="text"
@click="setExclude(state.studentList.indexOf(student))" @click="setExclude(state.studentList.indexOf(student))"
:title="'设为不参与'"
/> />
</div> </div>
</v-card-text> </v-card-text>
@ -508,7 +514,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="12"> <v-col cols="12" md="12">
<v-card variant="tonal" color="primary" class="mb-4"> <v-card class="mb-4" color="primary" variant="tonal">
<v-card-text> <v-card-text>
<div class="text-subtitle-2 mb-2">批量操作</div> <div class="text-subtitle-2 mb-2">批量操作</div>
<v-btn-group> <v-btn-group>
@ -545,14 +551,15 @@
</v-btn-group> </v-btn-group>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col></v-row </v-col>
</v-row
> >
</v-card-text> </v-card-text>
<v-divider /> <v-divider/>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn color="primary" @click="saveAttendance"> <v-btn color="primary" @click="saveAttendance">
<v-icon start>mdi-content-save</v-icon> <v-icon start>mdi-content-save</v-icon>
@ -562,16 +569,16 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<message-log ref="messageLog" /> <message-log ref="messageLog"/>
<!-- 添加悬浮工具栏 --> <!-- 添加悬浮工具栏 -->
<floating-toolbar <floating-toolbar
:loading="loading.download"
:unread-count="unreadCount"
:selected-date="state.selectedDateObj"
:is-today="isToday" :is-today="isToday"
@zoom="zoom" :loading="loading.download"
:selected-date="state.selectedDateObj"
:unread-count="unreadCount"
@refresh="downloadData" @refresh="downloadData"
@zoom="zoom"
@open-messages="$refs.messageLog.drawer = true" @open-messages="$refs.messageLog.drawer = true"
@open-settings="$router.push('/settings')" @open-settings="$router.push('/settings')"
@date-select="handleDateSelect" @date-select="handleDateSelect"
@ -580,24 +587,24 @@
/> />
<!-- 添加ICP备案悬浮组件 --> <!-- 添加ICP备案悬浮组件 -->
<FloatingICP /> <FloatingICP/>
<!-- 设备聊天室右下角浮窗 --> <!-- 设备聊天室右下角浮窗 -->
<ChatWidget v-model="isChatOpen" :show-button="false" /> <ChatWidget v-model="isChatOpen" :show-button="false"/>
<!-- 添加确认对话框 --> <!-- 添加确认对话框 -->
<v-dialog v-model="confirmDialog.show" max-width="400"> <v-dialog v-model="confirmDialog.show" max-width="400">
<v-card> <v-card>
<v-card-title class="text-h6"> 确认保存 </v-card-title> <v-card-title class="text-h6"> 确认保存</v-card-title>
<v-card-text> <v-card-text>
您正在修改 {{ state.dateString }} 的数据确定要保存吗 您正在修改 {{ state.dateString }} 的数据确定要保存吗
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn color="grey" variant="text" @click="confirmDialog.reject"> <v-btn color="grey" variant="text" @click="confirmDialog.reject">
取消 取消
</v-btn> </v-btn>
<v-btn color="primary" @click="confirmDialog.resolve"> 确认保存 </v-btn> <v-btn color="primary" @click="confirmDialog.resolve"> 确认保存</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -605,14 +612,14 @@
<!-- 添加随机点名组件 --> <!-- 添加随机点名组件 -->
<random-picker <random-picker
ref="randomPicker" ref="randomPicker"
:student-list="state.studentList"
:attendance="state.boardData.attendance" :attendance="state.boardData.attendance"
:student-list="state.studentList"
/> />
<!-- 添加URL配置确认对话框 --> <!-- 添加URL配置确认对话框 -->
<v-dialog v-model="urlConfigDialog.show" max-width="500"> <v-dialog v-model="urlConfigDialog.show" max-width="500">
<v-card> <v-card>
<v-card-title class="text-h6"> 确认应用URL配置 </v-card-title> <v-card-title class="text-h6"> 确认应用URL配置</v-card-title>
<v-card-text> <v-card-text>
<p>以下配置将应用于当前班级</p> <p>以下配置将应用于当前班级</p>
<v-list density="compact"> <v-list density="compact">
@ -621,17 +628,18 @@
:key="change.key" :key="change.key"
> >
<template #prepend> <template #prepend>
<v-icon :icon="change.icon" size="small" class="mr-2" /> <v-icon :icon="change.icon" class="mr-2" size="small"/>
</template> </template>
<v-list-item-title class="d-flex align-center"> <v-list-item-title class="d-flex align-center">
<span class="text-subtitle-1">{{ change.name }}</span> <span class="text-subtitle-1">{{ change.name }}</span>
<v-tooltip activator="parent" location="top">{{ <v-tooltip activator="parent" location="top">{{
change.description || change.key change.description || change.key
}}</v-tooltip> }}
</v-tooltip>
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle> <v-list-item-subtitle>
<span class="text-grey-darken-1">{{ change.oldValue }}</span> <span class="text-grey-darken-1">{{ change.oldValue }}</span>
<v-icon icon="mdi-arrow-right" size="small" class="mx-1" /> <v-icon class="mx-1" icon="mdi-arrow-right" size="small"/>
<span class="text-primary font-weight-medium">{{ <span class="text-primary font-weight-medium">{{
change.newValue change.newValue
}}</span> }}</span>
@ -640,7 +648,7 @@
</v-list> </v-list>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer/>
<v-btn <v-btn
color="grey" color="grey"
variant="text" variant="text"
@ -652,8 +660,10 @@
确认应用 确认应用
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-dialog </v-card>
><br /><br /><br /> </v-dialog
>
<br/><br/><br/>
</template> </template>
<script> <script>
@ -672,14 +682,14 @@ import {
setSetting, setSetting,
settingsDefinitions, settingsDefinitions,
} from "@/utils/settings"; } from "@/utils/settings";
import { kvServerProvider } from "@/utils/providers/kvServerProvider"; import {kvServerProvider} from "@/utils/providers/kvServerProvider";
import { useDisplay } from "vuetify"; import {useDisplay} from "vuetify";
import "../styles/index.scss"; import "../styles/index.scss";
import "../styles/transitions.scss"; import "../styles/transitions.scss";
import "../styles/global.scss"; import "../styles/global.scss";
import { pinyin } from "pinyin-pro"; import {pinyin} from "pinyin-pro";
import { debounce, throttle } from "@/utils/debounce"; import {debounce, throttle} from "@/utils/debounce";
import { Base64 } from "js-base64"; import {Base64} from "js-base64";
import { import {
getSocket, getSocket,
on as socketOn, on as socketOn,
@ -702,16 +712,16 @@ export default {
}, },
data() { data() {
const defaultSubjects = [ const defaultSubjects = [
{ name: "语文", order: 0 }, {name: "语文", order: 0},
{ name: "数学", order: 1 }, {name: "数学", order: 1},
{ name: "英语", order: 2 }, {name: "英语", order: 2},
{ name: "物理", order: 3 }, {name: "物理", order: 3},
{ name: "化学", order: 4 }, {name: "化学", order: 4},
{ name: "生物", order: 5 }, {name: "生物", order: 5},
{ name: "政治", order: 6 }, {name: "政治", order: 6},
{ name: "历史", order: 7 }, {name: "历史", order: 7},
{ name: "地理", order: 8 }, {name: "地理", order: 8},
{ name: "其他", order: 9 }, {name: "其他", order: 9},
]; ];
return { return {
@ -738,7 +748,7 @@ export default {
dateString: "", dateString: "",
synced: false, synced: false,
attendDialogVisible: false, attendDialogVisible: false,
contentStyle: { "font-size": `${getSetting("font.size")}px` }, contentStyle: {"font-size": `${getSetting("font.size")}px`},
uploadLoading: false, uploadLoading: false,
downloadLoading: false, downloadLoading: false,
snackbar: false, snackbar: false,
@ -822,7 +832,7 @@ export default {
// //
const deviceName = const deviceName =
this.state.namespaceInfo?.device?.name || this.state.namespaceInfo?.device?.name ||
this.state.classNumber || this.state.classNumber ||
"高三八班"; "高三八班";
const today = this.getToday(); const today = this.getToday();
@ -834,7 +844,7 @@ export default {
const yesterdayStr = this.formatDate(yesterday); const yesterdayStr = this.formatDate(yesterday);
if (currentDateStr === todayStr) { if (currentDateStr === todayStr) {
return deviceName +" - 今天的作业"; return deviceName + " - 今天的作业";
} else if (currentDateStr === yesterdayStr) { } else if (currentDateStr === yesterdayStr) {
return deviceName + " - 昨天的作业"; return deviceName + " - 昨天的作业";
} else { } else {
@ -861,7 +871,7 @@ export default {
rowSpan: Math.ceil( rowSpan: Math.ceil(
(value.content.split("\n").filter((line) => line.trim()).length + (value.content.split("\n").filter((line) => line.trim()).length +
1) * 1) *
0.8 0.8
), ),
})); }));
@ -1011,10 +1021,10 @@ export default {
}); });
return Array.from(surnameMap.entries()) return Array.from(surnameMap.entries())
.map(([name, count]) => ({ name, count })) .map(([name, count]) => ({name, count}))
.sort((a, b) => { .sort((a, b) => {
const pinyinA = pinyin(a.name, { toneType: "none", mode: "surname" }); const pinyinA = pinyin(a.name, {toneType: "none", mode: "surname"});
const pinyinB = pinyin(b.name, { toneType: "none", mode: "surname" }); const pinyinB = pinyin(b.name, {toneType: "none", mode: "surname"});
return pinyinA.localeCompare(pinyinB); return pinyinA.localeCompare(pinyinB);
}); });
}, },
@ -1300,7 +1310,7 @@ export default {
if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) { if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) {
this.state.boardData = { this.state.boardData = {
homework: {}, homework: {},
attendance: { absent: [], late: [], exclude: [] }, attendance: {absent: [], late: [], exclude: []},
}; };
} }
} else { } else {
@ -1327,7 +1337,7 @@ export default {
if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) { if (forceClear || !this.state.boardData || (!this.state.boardData.homework && !this.state.boardData.attendance)) {
this.state.boardData = { this.state.boardData = {
homework: {}, homework: {},
attendance: { absent: [], late: [], exclude: [] }, attendance: {absent: [], late: [], exclude: []},
}; };
} }
} finally { } finally {
@ -1586,7 +1596,7 @@ export default {
updateSettings() { updateSettings() {
this.state.fontSize = getSetting("font.size"); this.state.fontSize = getSetting("font.size");
this.state.contentStyle = { "font-size": `${this.state.fontSize}px` }; this.state.contentStyle = {"font-size": `${this.state.fontSize}px`};
this.setupAutoRefresh(); this.setupAutoRefresh();
this.updateBackendUrl(); this.updateBackendUrl();
// Token // Token
@ -1611,9 +1621,10 @@ export default {
this.$router this.$router
.replace({ .replace({
query: { date: formattedDate }, query: {date: formattedDate},
}) })
.catch(() => {}); .catch(() => {
});
// Load both data and subjects in parallel, force clear data when switching dates // Load both data and subjects in parallel, force clear data when switching dates
await Promise.all([this.downloadData(true), this.loadSubjects()]); await Promise.all([this.downloadData(true), this.loadSubjects()]);
@ -1628,7 +1639,7 @@ export default {
const maxColumns = Math.min(3, Math.floor(window.innerWidth / 300)); const maxColumns = Math.min(3, Math.floor(window.innerWidth / 300));
if (maxColumns <= 1) return items; if (maxColumns <= 1) return items;
const columns = Array.from({ length: maxColumns }, () => ({ const columns = Array.from({length: maxColumns}, () => ({
height: 0, height: 0,
items: [], items: [],
})); }));
@ -1756,7 +1767,7 @@ export default {
isPresent(index) { isPresent(index) {
const student = this.state.studentList[index]; const student = this.state.studentList[index];
const { absent, late, exclude } = this.state.boardData.attendance; const {absent, late, exclude} = this.state.boardData.attendance;
return ( return (
!absent.includes(student) && !absent.includes(student) &&
!late.includes(student) && !late.includes(student) &&

View File

@ -1,260 +1,263 @@
<template><v-app-bar elevation="1"> <template>
<template #prepend> <v-app-bar elevation="1">
<v-btn <template #prepend>
icon="mdi-arrow-left" <v-btn
variant="text" icon="mdi-arrow-left"
@click="$router.push('/')" variant="text"
/> @click="$router.push('/')"
</template> />
<v-app-bar-title class="text-h6" v-if="list && !isRenaming">{{ list.name }}</v-app-bar-title> </template>
<v-app-bar-title class="text-h6" v-else>列表</v-app-bar-title> <v-app-bar-title v-if="list && !isRenaming" class="text-h6">{{ list.name }}</v-app-bar-title>
</v-app-bar> <v-app-bar-title v-else class="text-h6">列表</v-app-bar-title>
</v-app-bar>
<v-container> <v-container>
<div class="d-flex align-center mb-4"> <div class="d-flex align-center mb-4">
<v-btn <v-btn
icon border
class="mr-2" class="mr-2"
to="/list" icon
border to="/list"
> >
<v-icon>mdi-arrow-left</v-icon> <v-icon>mdi-arrow-left</v-icon>
</v-btn>
<h1 v-if="list && !isRenaming">
{{ list.name }}
<v-btn border icon size="small" @click="startRenaming">
<v-icon>mdi-pencil</v-icon>
</v-btn>
</h1>
<div v-else-if="list && isRenaming" class="d-flex align-center">
<v-text-field
v-model="newListName"
autofocus
class="mr-2"
density="compact"
hide-details
label="列表名称"
style="min-width: 200px;"
@keyup.enter="saveListName"
></v-text-field>
<v-btn class="mr-2" color="primary" size="small" @click="saveListName">
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn color="error" size="small" @click="cancelRenaming">
<v-icon>mdi-close</v-icon>
</v-btn> </v-btn>
<h1 v-if="list && !isRenaming">
{{ list.name }}
<v-btn icon size="small" @click="startRenaming" border>
<v-icon>mdi-pencil</v-icon>
</v-btn>
</h1>
<div v-else-if="list && isRenaming" class="d-flex align-center">
<v-text-field
v-model="newListName"
label="列表名称"
hide-details
density="compact"
class="mr-2"
style="min-width: 200px;"
autofocus
@keyup.enter="saveListName"
></v-text-field>
<v-btn color="primary" size="small" class="mr-2" @click="saveListName">
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn color="error" size="small" @click="cancelRenaming">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<h1 v-else>
加载中...
</h1>
</div> </div>
<h1 v-else>
加载中...
</h1>
</div>
<v-card class="mb-5" border rounded="xl"> <v-card border class="mb-5" rounded="xl">
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
项目列表 项目列表
<v-spacer /> <v-spacer/>
<v-btn-toggle <v-btn-toggle
v-model="sortType" v-model="sortType"
mandatory mandatory
>
<v-btn value="default">
<v-icon>mdi-sort-alphabetical-ascending</v-icon>
</v-btn>
<v-btn value="completed">
<v-icon>mdi-check-circle-outline</v-icon>
</v-btn>
</v-btn-toggle>
</v-card-title>
<v-card-text v-if="sortedItems.length === 0">
暂无项目请添加新项目
</v-card-text>
<v-list
v-else
select-strategy="leaf"
> >
<v-list-item <v-btn value="default">
v-for="(item, index) in sortedItems" <v-icon>mdi-sort-alphabetical-ascending</v-icon>
:key="item.id" </v-btn>
:class="{ 'text-decoration-line-through': item.completed }" <v-btn value="completed">
@click="openItemDetails(item)" <v-icon>mdi-check-circle-outline</v-icon>
> </v-btn>
<template #prepend> </v-btn-toggle>
<v-list-item-action start> </v-card-title>
<v-checkbox-btn <v-card-text v-if="sortedItems.length === 0">
:model-value="item.completed" 暂无项目请添加新项目
@update:model-value="updateItemStatus(item.id, $event)" </v-card-text>
@click.stop <v-list
/> v-else
</v-list-item-action> select-strategy="leaf"
</template> >
{{ item.name }} <v-list-item
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle> v-for="(item, index) in sortedItems"
<template #append> :key="item.id"
{{ index + 1 }} :class="{ 'text-decoration-line-through': item.completed }"
</template> @click="openItemDetails(item)"
</v-list-item> >
</v-list> <template #prepend>
<v-card-actions v-if="sortedItems.length > 0"> <v-list-item-action start>
<v-spacer /> <v-checkbox-btn
<v-btn :model-value="item.completed"
color="error" @update:model-value="updateItemStatus(item.id, $event)"
prepend-icon="mdi-delete-sweep" @click.stop
@click="confirmDeleteCompleted" />
:disabled="!hasCompletedItems" </v-list-item-action>
> </template>
删除已完成项目 {{ item.name }}
<v-list-item-subtitle>{{ item.description }}</v-list-item-subtitle>
<template #append>
{{ index + 1 }}
</template>
</v-list-item>
</v-list>
<v-card-actions v-if="sortedItems.length > 0">
<v-spacer/>
<v-btn
:disabled="!hasCompletedItems"
color="error"
prepend-icon="mdi-delete-sweep"
@click="confirmDeleteCompleted"
>
删除已完成项目
</v-btn>
</v-card-actions>
</v-card>
<v-card border class="mb-5" rounded="xl">
<v-card-title>添加新项目</v-card-title>
<v-card-text>
<v-text-field
v-model="newItemName"
:rules="[(v) => !!v || '名称不能为空']"
label="项目名称"
/>
<v-btn
:disabled="!newItemName"
color="primary"
@click="addItem"
>
添加
</v-btn>
</v-card-text>
</v-card>
<v-card border class="mb-5" rounded="xl">
<v-card-title>列表排序</v-card-title>
<v-card-text>
<v-text-field
v-model="sortSeed"
class="mb-3"
hint="输入相同的种子值可以得到相同的排序结果"
label="排序种子 (任意数字或文本)"
persistent-hint
/>
<v-btn
class="mr-2"
color="primary"
@click="randomSort"
>
随机排序
</v-btn>
<v-btn
variant="text"
@click="resetSort"
>
撤销
</v-btn>
</v-card-text>
</v-card>
<!-- 确认删除对话框 -->
<v-dialog v-model="deleteDialog.show" max-width="500">
<v-card border rounded="xl">
<v-card-title>{{ deleteDialog.title }}</v-card-title>
<v-card-text>{{ deleteDialog.text }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">
取消
</v-btn>
<v-btn color="error" variant="text" @click="confirmDelete">
确认删除
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
</v-card><v-card class="mb-5" border rounded="xl">
<v-card-title>添加新项目</v-card-title>
<v-card-text>
<v-text-field
v-model="newItemName"
label="项目名称"
:rules="[(v) => !!v || '名称不能为空']"
/>
<v-btn
color="primary"
:disabled="!newItemName"
@click="addItem"
>
添加
</v-btn>
</v-card-text>
</v-card> </v-card>
</v-dialog>
<!-- 项目详情对话框 -->
<v-dialog v-model="itemDialog.show" max-width="600">
<v-card border rounded="xl">
<v-card-title>
<span v-if="!itemDialog.isEditing">项目详情</span>
<span v-else>编辑项目</span>
</v-card-title>
<v-card class="mb-5" border rounded="xl">
<v-card-title>列表排序</v-card-title>
<v-card-text> <v-card-text>
<v-text-field <div v-if="!itemDialog.isEditing && itemDialog.item">
v-model="sortSeed" <v-list>
label="排序种子 (任意数字或文本)" <v-list-item>
hint="输入相同的种子值可以得到相同的排序结果" <v-list-item-title class="text-subtitle-1 font-weight-bold">{{ itemDialog.item.name }}
persistent-hint </v-list-item-title>
class="mb-3" <v-list-item-subtitle>{{ itemDialog.item.id }}</v-list-item-subtitle>
/> </v-list-item>
<v-btn
color="primary"
class="mr-2"
@click="randomSort"
>
随机排序
</v-btn>
<v-btn
variant="text"
@click="resetSort"
>
撤销
</v-btn>
</v-card-text>
</v-card>
<!-- 确认删除对话框 --> <v-list-item>
<v-dialog v-model="deleteDialog.show" max-width="500"> <v-list-item-title class="text-subtitle-1 font-weight-bold">状态</v-list-item-title>
<v-card border rounded="xl"> <v-list-item-subtitle>
<v-card-title>{{ deleteDialog.title }}</v-card-title> <v-chip
<v-card-text>{{ deleteDialog.text }}</v-card-text> :color="itemDialog.item.completed ? 'success' : 'warning'"
<v-card-actions> size="small"
<v-spacer></v-spacer> >
<v-btn color="primary" variant="text" @click="deleteDialog.show = false"> {{ itemDialog.item.completed ? '已完成' : '未完成' }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="itemDialog.item.description">
<v-list-item-title class="text-subtitle-1 font-weight-bold">描述</v-list-item-title>
<v-list-item-subtitle>{{ itemDialog.item.description }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
<div v-else-if="itemDialog.isEditing && itemDialog.item" class="pa-2">
<v-text-field
v-model="itemDialog.editedItem.name"
class="mb-3"
label="名称"
variant="outlined"
></v-text-field>
<v-textarea
v-model="itemDialog.editedItem.description"
class="mb-3"
label="描述"
rows="3"
variant="outlined"
></v-textarea>
<v-switch
v-model="itemDialog.editedItem.completed"
color="success"
hide-details
label="已完成"
></v-switch>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<template v-if="!itemDialog.isEditing">
<v-btn color="primary" variant="text" @click="startEditingItem">
编辑
</v-btn>
<v-btn color="error" variant="text" @click="confirmDeleteItem(itemDialog.item?.id)">
删除
</v-btn>
<v-btn color="secondary" variant="text" @click="itemDialog.show = false">
关闭
</v-btn>
</template>
<template v-else>
<v-btn color="success" variant="text" @click="saveItemChanges">
保存
</v-btn>
<v-btn color="secondary" variant="text" @click="cancelEditingItem">
取消 取消
</v-btn> </v-btn>
<v-btn color="error" variant="text" @click="confirmDelete"> </template>
确认删除 </v-card-actions>
</v-btn> </v-card>
</v-card-actions> </v-dialog>
</v-card>
</v-dialog>
<!-- 项目详情对话框 -->
<v-dialog v-model="itemDialog.show" max-width="600">
<v-card border rounded="xl">
<v-card-title>
<span v-if="!itemDialog.isEditing">项目详情</span>
<span v-else>编辑项目</span>
</v-card-title>
<v-card-text>
<div v-if="!itemDialog.isEditing && itemDialog.item">
<v-list>
<v-list-item>
<v-list-item-title class="text-subtitle-1 font-weight-bold">{{ itemDialog.item.name }}</v-list-item-title>
<v-list-item-subtitle>{{ itemDialog.item.id }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<v-list-item-title class="text-subtitle-1 font-weight-bold">状态</v-list-item-title>
<v-list-item-subtitle>
<v-chip
:color="itemDialog.item.completed ? 'success' : 'warning'"
size="small"
>
{{ itemDialog.item.completed ? '已完成' : '未完成' }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="itemDialog.item.description">
<v-list-item-title class="text-subtitle-1 font-weight-bold">描述</v-list-item-title>
<v-list-item-subtitle>{{ itemDialog.item.description }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</div>
<div v-else-if="itemDialog.isEditing && itemDialog.item" class="pa-2">
<v-text-field
v-model="itemDialog.editedItem.name"
label="名称"
variant="outlined"
class="mb-3"
></v-text-field>
<v-textarea
v-model="itemDialog.editedItem.description"
label="描述"
variant="outlined"
rows="3"
class="mb-3"
></v-textarea>
<v-switch
v-model="itemDialog.editedItem.completed"
label="已完成"
color="success"
hide-details
></v-switch>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<template v-if="!itemDialog.isEditing">
<v-btn color="primary" variant="text" @click="startEditingItem">
编辑
</v-btn>
<v-btn color="error" variant="text" @click="confirmDeleteItem(itemDialog.item?.id)">
删除
</v-btn>
<v-btn color="secondary" variant="text" @click="itemDialog.show = false">
关闭
</v-btn>
</template>
<template v-else>
<v-btn color="success" variant="text" @click="saveItemChanges">
保存
</v-btn>
<v-btn color="secondary" variant="text" @click="cancelEditingItem">
取消
</v-btn>
</template>
</v-card-actions>
</v-card>
</v-dialog>
</v-container> </v-container>
</template> </template>
@ -544,7 +547,7 @@ export default {
// randomValueitems // randomValueitems
this.items = itemsWithRandom.map((item) => { this.items = itemsWithRandom.map((item) => {
const newItem = { ...item }; const newItem = {...item};
delete newItem.randomValue; delete newItem.randomValue;
return newItem; return newItem;
}); });

View File

@ -1,4 +1,5 @@
<template><v-app-bar elevation="1"> <template>
<v-app-bar elevation="1">
<template #prepend> <template #prepend>
<v-btn <v-btn
icon="mdi-arrow-left" icon="mdi-arrow-left"
@ -7,9 +8,8 @@
/> />
</template> </template>
<v-app-bar-title class="text-h6">列表</v-app-bar-title> <v-app-bar-title class="text-h6">列表</v-app-bar-title>
</v-app-bar><v-container> </v-app-bar>
<v-container>
<v-card border class="mb-5" rounded="xl"> <v-card border class="mb-5" rounded="xl">
@ -21,8 +21,8 @@
<v-list-item <v-list-item
v-for="list in lists" v-for="list in lists"
:key="list.id" :key="list.id"
:to="list.id !== editingListId ? `/list/${list.id}` : undefined"
:active="list.id === editingListId" :active="list.id === editingListId"
:to="list.id !== editingListId ? `/list/${list.id}` : undefined"
> >
<div v-if="list.id !== editingListId"> <div v-if="list.id !== editingListId">
<v-list-item-title>{{ list.name }}</v-list-item-title> <v-list-item-title>{{ list.name }}</v-list-item-title>
@ -30,27 +30,27 @@
<div v-else class="d-flex align-center w-100"> <div v-else class="d-flex align-center w-100">
<v-text-field <v-text-field
v-model="editListName" v-model="editListName"
label="列表名称"
hide-details
density="compact"
class="mr-2"
autofocus autofocus
class="mr-2"
density="compact"
hide-details
label="列表名称"
@keyup.enter="saveListName" @keyup.enter="saveListName"
></v-text-field> ></v-text-field>
<v-btn icon color="primary" @click.stop.prevent="saveListName" class="mr-2" border> <v-btn border class="mr-2" color="primary" icon @click.stop.prevent="saveListName">
<v-icon>mdi-check</v-icon> <v-icon>mdi-check</v-icon>
</v-btn> </v-btn>
<v-btn icon color="error" @click.stop.prevent="cancelEditing" border> <v-btn border color="error" icon @click.stop.prevent="cancelEditing">
<v-icon>mdi-close</v-icon> <v-icon>mdi-close</v-icon>
</v-btn> </v-btn>
</div> </div>
<template #append> <template #append>
<div v-if="list.id !== editingListId"> <div v-if="list.id !== editingListId">
<v-btn icon @click.stop.prevent="startEditing(list.id)" class="mr-2" border> <v-btn border class="mr-2" icon @click.stop.prevent="startEditing(list.id)">
<v-icon>mdi-pencil</v-icon> <v-icon>mdi-pencil</v-icon>
</v-btn> </v-btn>
<v-btn icon @click.stop.prevent="confirmDeleteList(list.id)" border> <v-btn border icon @click.stop.prevent="confirmDeleteList(list.id)">
<v-icon>mdi-delete</v-icon> <v-icon>mdi-delete</v-icon>
</v-btn> </v-btn>
</div> </div>
@ -58,15 +58,15 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-card> </v-card>
<v-card class="mb-5" border rounded="xl"> <v-card border class="mb-5" rounded="xl">
<v-card-title>创建新列表</v-card-title> <v-card-title>创建新列表</v-card-title>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="newListName" v-model="newListName"
label="列表名称"
:rules="[v => !!v || '名称不能为空']" :rules="[v => !!v || '名称不能为空']"
label="列表名称"
></v-text-field> ></v-text-field>
<v-btn color="primary" @click="createNewList" :disabled="!newListName"> <v-btn :disabled="!newListName" color="primary" @click="createNewList">
创建列表 创建列表
</v-btn> </v-btn>
</v-card-text> </v-card-text>
@ -87,11 +87,12 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
</v-container> </v-container>
</template> </template>
<script> <script>
import dataProvider from "@/utils/dataProvider.js"; import dataProvider from "@/utils/dataProvider.js";
export default { export default {
data() { data() {
return { return {

View File

@ -27,11 +27,11 @@
<v-list-item <v-list-item
v-for="tab in settingsTabs" v-for="tab in settingsTabs"
:key="tab.value" :key="tab.value"
@click="settingsTab = tab.value"
:active="settingsTab === tab.value" :active="settingsTab === tab.value"
:color="settingsTab === tab.value ? 'primary' : 'default'"
:prepend-icon="tab.icon" :prepend-icon="tab.icon"
class="rounded-e-xl" class="rounded-e-xl"
:color="settingsTab === tab.value ? 'primary' : 'default'" @click="settingsTab = tab.value"
> >
<v-list-item-title>{{ tab.title }}</v-list-item-title> <v-list-item-title>{{ tab.title }}</v-list-item-title>
</v-list-item> </v-list-item>
@ -40,11 +40,13 @@
<v-tabs-window <v-tabs-window
v-model="settingsTab" v-model="settingsTab"
style="width: 100%"
direction="vertical" direction="vertical"
style="width: 100%"
> >
<v-tabs-window-item value="index" <v-tabs-window-item value="index"
><v-card class="service-card gradient-right clickable mb-4" elevation="8" rounded="xl" border hover @click="openClassworksKV" color="primary" variant="tonal"> >
<v-card border class="service-card gradient-right clickable mb-4" color="primary" elevation="8" hover
rounded="xl" variant="tonal" @click="openClassworksKV">
<v-card-item> <v-card-item>
<div class="card-title"> <div class="card-title">
<div> <div>
@ -58,10 +60,10 @@
<v-card-text> <v-card-text>
<div class="mt-4"> <div class="mt-4">
<v-btn <v-btn
variant="text"
class="text-none"
append-icon="mdi-arrow-right" append-icon="mdi-arrow-right"
class="text-none"
rounded="xl" rounded="xl"
variant="text"
@click="openClassworksKV" @click="openClassworksKV"
> >
打开 Classworks KV 打开 Classworks KV
@ -69,126 +71,137 @@
</div> </div>
</v-card-text> </v-card-text>
</v-card> </v-card>
<v-card title="Classworks" subtitle="设置" class="rounded-xl mb-4" border> <v-card border class="rounded-xl mb-4" subtitle="设置" title="Classworks">
<v-card-text> <v-card-text>
<v-alert <v-alert
color="error"
variant="tonal"
icon="mdi-alert-circle"
class="rounded-xl" class="rounded-xl"
>Classworks color="error"
icon="mdi-alert-circle"
variant="tonal"
>Classworks
是开源免费的软件官方没有提供任何形式的付费支持服务源代码仓库地址在 是开源免费的软件官方没有提供任何形式的付费支持服务源代码仓库地址在
<a <a
href="https://github.com/ZeroCatDev/Classworks" href="https://github.com/ZeroCatDev/Classworks"
target="_blank" target="_blank"
>https://github.com/ZeroCatDev/Classworks</a >https://github.com/ZeroCatDev/Classworks</a
>如果您通过有偿协助等付费方式取得本应用在遇到问题时请在与卖家约定的服务框架下优先向卖家求助如果卖家没有提供您预期的服务请退款或通过其它形式积极维护您的合法权益</v-alert >如果您通过有偿协助等付费方式取得本应用在遇到问题时请在与卖家约定的服务框架下优先向卖家求助如果卖家没有提供您预期的服务请退款或通过其它形式积极维护您的合法权益
</v-alert
> >
<v-alert <v-alert
class="mt-4 rounded-xl" class="mt-4 rounded-xl"
color="info" color="info"
variant="tonal"
icon="mdi-information" icon="mdi-information"
>请不要使用浏览器清除缓存功能否则会导致配置丢失<del variant="tonal"
>恶意的操作可能导致您受到贵校教师的处理</del >请不要使用浏览器清除缓存功能否则会导致配置丢失
></v-alert <del
>恶意的操作可能导致您受到贵校教师的处理
</del
>
</v-alert
> >
<v-alert <v-alert
class="mt-4 rounded-xl" class="mt-4 rounded-xl"
color="warning" color="warning"
variant="tonal"
icon="mdi-information" icon="mdi-information"
><p> variant="tonal"
请不要使用包括但不限于360极速浏览器360安全浏览器夸克浏览器QQ浏览器等浏览器使用 ><p>
Classworks 请不要使用包括但不限于360极速浏览器360安全浏览器夸克浏览器QQ浏览器等浏览器使用
这些浏览器过时且存在严重的一致性问题在Windows上使用新版 Classworks
Microsoft Edge 浏览器是最推荐的选择 这些浏览器过时且存在严重的一致性问题在Windows上使用新版
</p> Microsoft Edge 浏览器是最推荐的选择
</p>
<p style="color: #666"> <p style="color: #666">
上述浏览器商标为其所属公司所有Classworks 上述浏览器商标为其所属公司所有Classworks
与上述浏览器所属公司无竞争关系 与上述浏览器所属公司无竞争关系
</p> </p>
<br /><v-btn <br/>
<v-btn
append-icon="mdi-open-in-new"
class="text-none rounded-xl"
color="warning"
href="https://www.microsoft.com/zh-cn/windows/microsoft-edge" href="https://www.microsoft.com/zh-cn/windows/microsoft-edge"
target="_blank" target="_blank"
color="warning"
variant="tonal" variant="tonal"
class="text-none rounded-xl" >下载 Microsoft Edge微软边缘浏览器
append-icon="mdi-open-in-new" </v-btn
>下载 Microsoft Edge微软边缘浏览器</v-btn >
></v-alert </v-alert
> >
</v-card-text> </v-card-text>
</v-card><about-card /> </v-card>
<about-card/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="server"> <v-tabs-window-item value="server">
<server-settings-card <server-settings-card
border
:loading="loading.server" :loading="loading.server"
border
@saved="onSettingsSaved" @saved="onSettingsSaved"
/> />
<data-provider-settings-card border class="mt-4" /> <data-provider-settings-card border class="mt-4"/>
<kv-database-card border class="mt-4" /> <kv-database-card border class="mt-4"/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="student"> <v-tabs-window-item value="student">
<student-list-card border :is-mobile="isMobile" /> <student-list-card :is-mobile="isMobile" border/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="share"> <v-tabs-window-item value="share">
<settings-link-generator border class="mt-4" /> <settings-link-generator border class="mt-4"/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="refresh"> <v-tabs-window-item value="refresh">
<refresh-settings-card <refresh-settings-card
border
:loading="loading.refresh" :loading="loading.refresh"
border
@saved="onSettingsSaved" @saved="onSettingsSaved"
/> />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="edit"> <v-tabs-window-item value="edit">
<edit-settings-card <edit-settings-card
border
:loading="loading.edit" :loading="loading.edit"
border
@saved="onSettingsSaved" @saved="onSettingsSaved"
/> />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="display"> <v-tabs-window-item value="display">
<display-settings-card <display-settings-card
border
:loading="loading.display" :loading="loading.display"
border
@saved="onSettingsSaved" @saved="onSettingsSaved"
/> />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="theme"> <v-tabs-window-item value="theme">
<theme-settings-card <theme-settings-card
border
:loading="loading.theme" :loading="loading.theme"
border
@saved="onSettingsSaved" @saved="onSettingsSaved"
/> />
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="randomPicker"> <v-tabs-window-item value="randomPicker">
<random-picker-card border :is-mobile="isMobile" /> <random-picker-card :is-mobile="isMobile" border/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="subject"> <v-tabs-window-item value="subject">
<subject-management-card border /> <br /> <subject-management-card border/>
<homework-template-card border /> <br/>
<homework-template-card border/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="developer" <v-tabs-window-item value="developer"
><settings-card border title="开发者选项" icon="mdi-developer-board"> >
<settings-card border icon="mdi-developer-board" title="开发者选项">
<v-list> <v-list>
<v-list-item> <v-list-item>
<template #prepend> <template #prepend>
<v-icon icon="mdi-code-tags" class="mr-3" /> <v-icon class="mr-3" icon="mdi-code-tags"/>
</template> </template>
<v-list-item-title>启用开发者选项</v-list-item-title> <v-list-item-title>启用开发者选项</v-list-item-title>
<v-list-item-subtitle <v-list-item-subtitle
>启用后可以查看和修改开发者设置</v-list-item-subtitle >启用后可以查看和修改开发者设置
</v-list-item-subtitle
> >
<template #append> <template #append>
<v-switch <v-switch
@ -202,39 +215,39 @@
</v-list> </v-list>
</settings-card> </settings-card>
<developer-settings-card <developer-settings-card
border
:loading="loading.developer" :loading="loading.developer"
border
@saved="onSettingsSaved" @saved="onSettingsSaved"
/> />
<template v-if="settings.developer.enabled"> <template v-if="settings.developer.enabled">
<v-card border class="mt-4 rounded-lg"> <v-card border class="mt-4 rounded-lg">
<v-card-title class="d-flex align-center"> <v-card-title class="d-flex align-center">
<v-icon icon="mdi-cog-outline" class="mr-2" /> <v-icon class="mr-2" icon="mdi-cog-outline"/>
所有设置 所有设置
</v-card-title> </v-card-title>
<v-card-subtitle> 浏览和修改所有可用设置 </v-card-subtitle> <v-card-subtitle> 浏览和修改所有可用设置</v-card-subtitle>
<v-card-text> <v-card-text>
<settings-explorer @update="onSettingUpdate" /> <settings-explorer @update="onSettingUpdate"/>
</v-card-text> </v-card-text>
</v-card> </v-card>
</template> </template>
<v-col v-if="settings.developer.enabled" cols="12"> </v-col> <v-col v-if="settings.developer.enabled" cols="12"></v-col>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="about"> <v-tabs-window-item value="about">
<about-card /> <about-card/>
<echo-chamber-card border class="mt-4" /> <echo-chamber-card border class="mt-4"/>
</v-tabs-window-item> </v-tabs-window-item>
</v-tabs-window> </v-tabs-window>
</v-container> </v-container>
<!-- 消息记录组件 --> <!-- 消息记录组件 -->
<message-log ref="messageLog" /> <message-log ref="messageLog"/>
</div> </div>
</template> </template>
<script> <script>
import { useDisplay } from "vuetify"; import {useDisplay} from "vuetify";
import ServerSettingsCard from "@/components/settings/cards/ServerSettingsCard.vue"; import ServerSettingsCard from "@/components/settings/cards/ServerSettingsCard.vue";
import EditSettingsCard from "@/components/settings/cards/EditSettingsCard.vue"; import EditSettingsCard from "@/components/settings/cards/EditSettingsCard.vue";
import RefreshSettingsCard from "@/components/settings/cards/RefreshSettingsCard.vue"; import RefreshSettingsCard from "@/components/settings/cards/RefreshSettingsCard.vue";
@ -259,6 +272,7 @@ import RandomPickerCard from "@/components/settings/cards/RandomPickerCard.vue";
import HomeworkTemplateCard from "@/components/settings/cards/HomeworkTemplateCard.vue"; import HomeworkTemplateCard from "@/components/settings/cards/HomeworkTemplateCard.vue";
import SubjectManagementCard from "@/components/settings/cards/SubjectManagementCard.vue"; import SubjectManagementCard from "@/components/settings/cards/SubjectManagementCard.vue";
import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue"; import KvDatabaseCard from "@/components/settings/cards/KvDatabaseCard.vue";
export default { export default {
name: "Settings", name: "Settings",
components: { components: {
@ -281,8 +295,8 @@ export default {
KvDatabaseCard, KvDatabaseCard,
}, },
setup() { setup() {
const { mobile } = useDisplay(); const {mobile} = useDisplay();
return { isMobile: mobile }; return {isMobile: mobile};
}, },
data() { data() {
const provider = getSetting("server.provider"); const provider = getSetting("server.provider");
@ -326,8 +340,8 @@ export default {
return { return {
settings, settings,
dataProviders: [ dataProviders: [
{ title: "服务器", value: "server" }, {title: "服务器", value: "server"},
{ title: "本地数据库", value: "indexedDB" }, {title: "本地数据库", value: "indexedDB"},
], ],
studentData: { studentData: {
list: [], list: [],
@ -642,6 +656,7 @@ export default {
.settings-page { .settings-page {
.v-card { .v-card {
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
&:hover { &:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
} }

View File

@ -1,3 +1,4 @@
# Plugins # Plugins
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally. Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you
want to use globally.

View File

@ -9,7 +9,7 @@ import vuetify from './vuetify'
import pinia from '@/stores' import pinia from '@/stores'
import router from '@/router' import router from '@/router'
export function registerPlugins (app) { export function registerPlugins(app) {
app app
.use(vuetify) .use(vuetify)
.use(router) .use(router)

View File

@ -9,7 +9,7 @@ import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles' import 'vuetify/styles'
// Composables // Composables
import { createVuetify } from 'vuetify' import {createVuetify} from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides // https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({ export default createVuetify({

View File

@ -5,9 +5,9 @@
*/ */
// Composables // Composables
import { createRouter, createWebHistory } from 'vue-router/auto' import {createRouter, createWebHistory} from 'vue-router/auto'
import { setupLayouts } from 'virtual:generated-layouts' import {setupLayouts} from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes' import {routes} from 'vue-router/auto-routes'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),

View File

@ -1,5 +1,5 @@
// Utilities // Utilities
import { defineStore } from 'pinia' import {defineStore} from 'pinia'
export const useAppStore = defineStore('app', { export const useAppStore = defineStore('app', {
state: () => ({ state: () => ({

View File

@ -1,4 +1,4 @@
// Utilities // Utilities
import { createPinia } from 'pinia' import {createPinia} from 'pinia'
export default createPinia() export default createPinia()

View File

@ -4,20 +4,20 @@
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
.v-card-title { .v-card-title {
font-size: 1.25rem; font-size: 1.25rem;
padding: 16px 20px; padding: 16px 20px;
} }
.v-card-text { .v-card-text {
padding: 16px 20px; padding: 16px 20px;
} }
.v-card-actions { .v-card-actions {
padding: 12px 20px; padding: 12px 20px;
} }
&:active { &:active {
transform: scale(0.98); transform: scale(0.98);
} }
@ -27,7 +27,7 @@
.glow-card { .glow-card {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
@ -35,14 +35,14 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 60%); rgba(255, 255, 255, 0) 60%);
opacity: 0; opacity: 0;
transition: opacity 0.5s; transition: opacity 0.5s;
pointer-events: none; pointer-events: none;
} }
&:hover::after { &:hover::after {
opacity: 1; opacity: 1;
} }

View File

@ -42,8 +42,8 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0) 60%); rgba(255, 255, 255, 0) 60%);
opacity: 0; opacity: 0;
transition: opacity 0.5s; transition: opacity 0.5s;
pointer-events: none; pointer-events: none;

View File

@ -1,6 +1,3 @@
// 添加卡片发光效果 // 添加卡片发光效果
.glow-track { .glow-track {
position: relative; position: relative;
@ -15,8 +12,8 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), background: radial-gradient(circle at var(--x, 50%) var(--y, 50%),
rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.15) 0%,
rgba(255, 255, 255, 0) 70%); rgba(255, 255, 255, 0) 70%);
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
pointer-events: none; pointer-events: none;
@ -32,20 +29,20 @@
.glow-highlight { .glow-highlight {
animation: glow-pulse 3s ease-in-out; animation: glow-pulse 3s ease-in-out;
box-shadow: 0 0 20px rgba(33, 150, 243, 0.6), box-shadow: 0 0 20px rgba(33, 150, 243, 0.6),
0 0 40px rgba(33, 150, 243, 0.4), 0 0 40px rgba(33, 150, 243, 0.4),
0 0 60px rgba(33, 150, 243, 0.2) !important; 0 0 60px rgba(33, 150, 243, 0.2) !important;
} }
@keyframes glow-pulse { @keyframes glow-pulse {
0%, 100% { 0%, 100% {
box-shadow: 0 0 20px rgba(33, 150, 243, 0.6), box-shadow: 0 0 20px rgba(33, 150, 243, 0.6),
0 0 40px rgba(33, 150, 243, 0.4), 0 0 40px rgba(33, 150, 243, 0.4),
0 0 60px rgba(33, 150, 243, 0.2); 0 0 60px rgba(33, 150, 243, 0.2);
} }
50% { 50% {
box-shadow: 0 0 30px rgba(33, 150, 243, 0.8), box-shadow: 0 0 30px rgba(33, 150, 243, 0.8),
0 0 60px rgba(33, 150, 243, 0.6), 0 0 60px rgba(33, 150, 243, 0.6),
0 0 90px rgba(33, 150, 243, 0.4); 0 0 90px rgba(33, 150, 243, 0.4);
} }
} }
@ -140,175 +137,176 @@
transform: scale(0.95); transform: scale(0.95);
} }
} }
.grid-masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 8px;
grid-auto-flow: dense;
}
.grid-item { .grid-masonry {
width: 100%; display: grid;
transition: all 0.2s ease; grid-template-columns: repeat(3, 1fr);
gap: 16px;
padding: 8px;
grid-auto-flow: dense;
}
.grid-item {
width: 100%;
transition: all 0.2s ease;
}
.empty-card {
transform: scale(0.9);
opacity: 0.8;
grid-row-end: span 1 !important;
}
.empty-card:hover {
transform: scale(0.95);
opacity: 1;
}
.empty-subjects-container {
display: flex;
flex-wrap: wrap;
}
@media (max-width: 1199px) {
.grid-masonry {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 799px) {
.grid-masonry {
grid-template-columns: 1fr;
} }
.empty-card { .empty-card {
transform: scale(0.9);
opacity: 0.8;
grid-row-end: span 1 !important;
}
.empty-card:hover {
transform: scale(0.95); transform: scale(0.95);
opacity: 1;
} }
}
.empty-subjects-container { /* 优化滚动条样式 */
display: flex; .main-window::-webkit-scrollbar {
flex-wrap: wrap; width: 8px;
} }
@media (max-width: 1199px) { .main-window::-webkit-scrollbar-track {
.grid-masonry { background: transparent;
grid-template-columns: repeat(2, 1fr); }
}
}
@media (max-width: 799px) { .main-window::-webkit-scrollbar-thumb {
.grid-masonry { background-color: rgba(0, 0, 0, 0.2);
grid-template-columns: 1fr; border-radius: 4px;
} }
.empty-card { .main-window::-webkit-scrollbar-thumb:hover {
transform: scale(0.95); background-color: rgba(0, 0, 0, 0.3);
} }
}
/* 优化滚动条样式 */ .no-data-message {
.main-window::-webkit-scrollbar { display: flex;
width: 8px; justify-content: center;
} align-items: center;
min-height: 200px;
margin: 20px 0;
}
.main-window::-webkit-scrollbar-track { .attendance-drawer {
background: transparent; border-left: 1px solid rgba(0, 0, 0, 0.12);
} }
.main-window::-webkit-scrollbar-thumb { .attendance-drawer :deep(.v-navigation-drawer__content) {
background-color: rgba(0, 0, 0, 0.2); overflow-y: auto;
border-radius: 4px; }
}
.main-window::-webkit-scrollbar-thumb:hover { /* 优化滚动条样式 */
background-color: rgba(0, 0, 0, 0.3); .attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar) {
} width: 8px;
}
.no-data-message { .attendance-drawer
display: flex; :deep(.v-navigation-drawer__content::-webkit-scrollbar-track) {
justify-content: center; background: transparent;
align-items: center; }
min-height: 200px;
margin: 20px 0;
}
.attendance-drawer
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb) {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.attendance-drawer
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb:hover) {
background-color: rgba(0, 0, 0, 0.3);
}
/* 响应式调整 */
@media (max-width: 960px) {
.attendance-drawer { .attendance-drawer {
border-left: 1px solid rgba(0, 0, 0, 0.12); display: none;
} }
}
.attendance-drawer :deep(.v-navigation-drawer__content) { .text-success {
overflow-y: auto; color: rgb(var(--v-theme-success));
} }
/* 优化滚动条样式 */ .text-error {
.attendance-drawer :deep(.v-navigation-drawer__content::-webkit-scrollbar) { color: rgb(var(--v-theme-error));
width: 8px; }
}
.attendance-drawer .text-warning {
:deep(.v-navigation-drawer__content::-webkit-scrollbar-track) { color: rgb(var(--v-theme-warning));
background: transparent; }
}
.attendance-drawer .attendance-card {
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb) { display: flex;
background-color: rgba(0, 0, 0, 0.2); flex-direction: column;
border-radius: 4px; }
}
.attendance-drawer .attendance-numbers {
:deep(.v-navigation-drawer__content::-webkit-scrollbar-thumb:hover) { padding: 20px 0;
background-color: rgba(0, 0, 0, 0.3); }
}
/* 响应式调整 */ .total-number {
@media (max-width: 960px) { border-bottom: 1px solid rgba(0, 0, 0, 0.12);
.attendance-drawer { padding-bottom: 20px;
display: none; }
}
}
.text-success { .status-number {
color: rgb(var(--v-theme-success)); flex: 1;
} }
.text-error { .text-h2,
color: rgb(var(--v-theme-error)); .text-h3 {
} line-height: 1.2;
}
.text-warning { .empty-subjects-grid {
color: rgb(var(--v-theme-warning)); display: grid;
} grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 8px;
}
.attendance-card { .empty-subject-card {
display: flex; cursor: pointer;
flex-direction: column; transition: all 0.2s ease;
} opacity: 0.8;
}
.attendance-numbers { .empty-subject-card:hover {
padding: 20px 0; transform: scale(1.02);
} opacity: 1;
}
.total-number { .empty-subjects {
border-bottom: 1px solid rgba(0, 0, 0, 0.12); border-top: 1px solid rgba(0, 0, 0, 0.12);
padding-bottom: 20px; padding-top: 1rem;
} }
.status-number { .empty-subject-card:not(:disabled):hover {
flex: 1; opacity: 1;
} transform: scale(1.02);
}
.text-h2,
.text-h3 {
line-height: 1.2;
}
.empty-subjects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 8px;
}
.empty-subject-card {
cursor: pointer;
transition: all 0.2s ease;
opacity: 0.8;
}
.empty-subject-card:hover {
transform: scale(1.02);
opacity: 1;
}
.empty-subjects {
border-top: 1px solid rgba(0, 0, 0, 0.12);
padding-top: 1rem;
}
.empty-subject-card:not(:disabled):hover {
opacity: 1;
transform: scale(1.02);
}
// 出勤管理对话框样式 // 出勤管理对话框样式
.attendance-stat { .attendance-stat {

View File

@ -10,72 +10,72 @@
// ); // );
.student-card { .student-card {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.bg-primary-subtle {
background-color: rgb(var(--v-theme-primary), 0.05);
}
.action-buttons {
transition: opacity 0.2s ease;
opacity: 0;
}
.gap-1 {
gap: 4px;
}
.gap-2 {
gap: 8px;
}
.student-card .v-text-field {
margin: 0;
padding: 0;
}
@media (max-width: 600px) {
.v-container {
padding: 12px;
} }
.bg-primary-subtle { .v-col {
background-color: rgb(var(--v-theme-primary), 0.05); padding: 8px;
} }
}
.action-buttons { .student-card.mobile {
transition: opacity 0.2s ease; margin-bottom: 8px;
opacity: 0; }
}
.gap-1 { .student-card.mobile .v-btn {
gap: 4px; min-width: 40px;
} min-height: 40px;
}
.gap-2 { .student-card.mobile .v-text-field {
gap: 8px; font-size: 16px;
} }
.student-card .v-text-field { @media (max-width: 600px) {
margin: 0; .v-col {
padding: 0; padding: 6px !important;
}
@media (max-width: 600px) {
.v-container {
padding: 12px;
}
.v-col {
padding: 8px;
}
}
.student-card.mobile {
margin-bottom: 8px;
}
.student-card.mobile .v-btn {
min-width: 40px;
min-height: 40px;
}
.student-card.mobile .v-text-field {
font-size: 16px;
}
@media (max-width: 600px) {
.v-col {
padding: 6px !important;
}
.student-card {
margin-bottom: 4px;
}
.action-buttons {
opacity: 1;
}
} }
.student-card { .student-card {
-webkit-tap-highlight-color: transparent; margin-bottom: 4px;
} }
.student-card:active { .action-buttons {
background-color: rgb(var(--v-theme-primary), 0.05); opacity: 1;
} }
}
.student-card {
-webkit-tap-highlight-color: transparent;
}
.student-card:active {
background-color: rgb(var(--v-theme-primary), 0.05);
}

View File

@ -8,13 +8,13 @@ $standard-accelerate: cubic-bezier(0.3, 0.0, 1.0, 1.0);
// 网格项目的进入和离开动画 // 网格项目的进入和离开动画
.grid-item { .grid-item {
transition: transform 400ms $emphasized-decelerate, transition: transform 400ms $emphasized-decelerate,
opacity 200ms $standard-easing; opacity 200ms $standard-easing;
will-change: transform, opacity; will-change: transform, opacity;
backface-visibility: hidden; backface-visibility: hidden;
&.v-enter-active { &.v-enter-active {
transition: transform 400ms $emphasized-decelerate, transition: transform 400ms $emphasized-decelerate,
opacity 250ms $standard-easing; opacity 250ms $standard-easing;
} }
&.v-move { &.v-move {
@ -25,7 +25,7 @@ $standard-accelerate: cubic-bezier(0.3, 0.0, 1.0, 1.0);
&.v-leave-active { &.v-leave-active {
position: absolute !important; position: absolute !important;
transition: transform 300ms $emphasized-accelerate, transition: transform 300ms $emphasized-accelerate,
opacity 200ms $standard-accelerate; opacity 200ms $standard-accelerate;
} }
&.v-enter-from, &.v-enter-from,
@ -122,20 +122,20 @@ $standard-accelerate: cubic-bezier(0.3, 0.0, 1.0, 1.0);
// 保持对话框本身的过渡动画 // 保持对话框本身的过渡动画
.v-dialog-transition-enter-active { .v-dialog-transition-enter-active {
transition: transform 400ms $emphasized-decelerate, transition: transform 400ms $emphasized-decelerate,
opacity 300ms $standard-easing; opacity 300ms $standard-easing;
} }
.v-dialog-transition-leave-active { .v-dialog-transition-leave-active {
transition: transform 250ms $emphasized-accelerate, transition: transform 250ms $emphasized-accelerate,
opacity 200ms $standard-accelerate; opacity 200ms $standard-accelerate;
} }
// 按钮状态变化动画 // 按钮状态变化动画
.v-btn { .v-btn {
transition: background-color 250ms $standard-easing, transition: background-color 250ms $standard-easing,
transform 150ms $emphasized-decelerate; transform 150ms $emphasized-decelerate;
touch-action: manipulation; touch-action: manipulation;
min-height: 40px; // 确保触摸目标足够大 min-height: 40px; // 确保触摸目标足够大
min-width: 40px; min-width: 40px;
&:active { &:active {

View File

@ -16,11 +16,19 @@
} }
@keyframes pulse-warning { @keyframes pulse-warning {
0%, 100% { transform: scale(1); } 0%, 100% {
50% { transform: scale(1.002); } transform: scale(1);
}
50% {
transform: scale(1.002);
}
} }
@keyframes pulse-border { @keyframes pulse-border {
0%, 100% { opacity: 1; } 0%, 100% {
50% { opacity: 0.5; } opacity: 1;
}
50% {
opacity: 0.5;
}
} }

View File

@ -1,8 +1,8 @@
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching' import {precacheAndRoute, cleanupOutdatedCaches} from 'workbox-precaching'
import { registerRoute, setCatchHandler } from 'workbox-routing' import {registerRoute, setCatchHandler} from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies' import {CacheFirst, NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration' import {ExpirationPlugin} from 'workbox-expiration'
import { CacheableResponsePlugin } from 'workbox-cacheable-response' import {CacheableResponsePlugin} from 'workbox-cacheable-response'
// 使用 self.__WB_MANIFEST 是 workbox 的一个特殊变量,会被实际的预缓存清单替换 // 使用 self.__WB_MANIFEST 是 workbox 的一个特殊变量,会被实际的预缓存清单替换
precacheAndRoute(self.__WB_MANIFEST) precacheAndRoute(self.__WB_MANIFEST)
@ -81,7 +81,7 @@ registerRoute(
// 外部资源缓存 // 外部资源缓存
registerRoute( registerRoute(
({ url }) => url.origin !== self.location.origin, ({url}) => url.origin !== self.location.origin,
new NetworkFirst({ new NetworkFirst({
cacheName: 'external-resources', cacheName: 'external-resources',
plugins: [ plugins: [
@ -102,7 +102,7 @@ self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'CACHE_KEYS') { if (event.data && event.data.type === 'CACHE_KEYS') {
// 获取所有缓存键 // 获取所有缓存键
caches.keys().then((cacheNames) => { caches.keys().then((cacheNames) => {
event.ports[0].postMessage({ cacheNames }); event.ports[0].postMessage({cacheNames});
}); });
} else if (event.data && event.data.type === 'CACHE_CONTENT') { } else if (event.data && event.data.type === 'CACHE_CONTENT') {
// 获取特定缓存的内容 // 获取特定缓存的内容
@ -110,14 +110,14 @@ self.addEventListener('message', (event) => {
caches.open(cacheName).then((cache) => { caches.open(cacheName).then((cache) => {
cache.keys().then((requests) => { cache.keys().then((requests) => {
const urls = requests.map(request => request.url); const urls = requests.map(request => request.url);
event.ports[0].postMessage({ cacheName, urls }); event.ports[0].postMessage({cacheName, urls});
}); });
}); });
} else if (event.data && event.data.type === 'CLEAR_CACHE') { } else if (event.data && event.data.type === 'CLEAR_CACHE') {
// 清除特定缓存 // 清除特定缓存
const cacheName = event.data.cacheName; const cacheName = event.data.cacheName;
caches.delete(cacheName).then((success) => { caches.delete(cacheName).then((success) => {
event.ports[0].postMessage({ success, cacheName }); event.ports[0].postMessage({success, cacheName});
}); });
} else if (event.data && event.data.type === 'CLEAR_URL') { } else if (event.data && event.data.type === 'CLEAR_URL') {
// 清除特定URL的缓存 // 清除特定URL的缓存
@ -125,7 +125,7 @@ self.addEventListener('message', (event) => {
const url = event.data.url; const url = event.data.url;
caches.open(cacheName).then((cache) => { caches.open(cacheName).then((cache) => {
cache.delete(url).then((success) => { cache.delete(url).then((success) => {
event.ports[0].postMessage({ success, cacheName, url }); event.ports[0].postMessage({success, cacheName, url});
}); });
}); });
} else if (event.data && event.data.type === 'CLEAR_ALL_CACHES') { } else if (event.data && event.data.type === 'CLEAR_ALL_CACHES') {
@ -134,8 +134,8 @@ self.addEventListener('message', (event) => {
Promise.all( Promise.all(
cacheNames.map(name => caches.delete(name)) cacheNames.map(name => caches.delete(name))
).then(() => { ).then(() => {
event.ports[0].postMessage({ success: true }); event.ports[0].postMessage({success: true});
}); });
}); });
} }
}); });

View File

@ -1,5 +1,5 @@
import axios from "@/axios/axios"; import axios from "@/axios/axios";
import { getSetting } from "@/utils/settings"; import {getSetting} from "@/utils/settings";
// Helper function to check if provider is valid for API calls // Helper function to check if provider is valid for API calls
const isValidProvider = () => { const isValidProvider = () => {
@ -9,7 +9,7 @@ const isValidProvider = () => {
// Helper function to get request headers with kvtoken // Helper function to get request headers with kvtoken
const getHeaders = () => { const getHeaders = () => {
const headers = { Accept: "application/json" }; const headers = {Accept: "application/json"};
const kvToken = getSetting("server.kvToken"); const kvToken = getSetting("server.kvToken");
const siteKey = getSetting("server.siteKey"); const siteKey = getSetting("server.siteKey");

View File

@ -1,12 +1,12 @@
import { kvLocalProvider } from "./providers/kvLocalProvider"; import {kvLocalProvider} from "./providers/kvLocalProvider";
import { kvServerProvider } from "./providers/kvServerProvider"; import {kvServerProvider} from "./providers/kvServerProvider";
import { getSetting, setSetting } from "./settings"; import {getSetting, setSetting} from "./settings";
export const formatResponse = (data) => data; export const formatResponse = (data) => data;
export const formatError = (message, code = "UNKNOWN_ERROR") => ({ export const formatError = (message, code = "UNKNOWN_ERROR") => ({
success: false, success: false,
error: { code, message }, error: {code, message},
}); });
// Main data provider with simplified API // Main data provider with simplified API
@ -256,7 +256,6 @@ export default {
}; };
export const ErrorCodes = { export const ErrorCodes = {
NOT_FOUND: "数据不存在", NOT_FOUND: "数据不存在",
NETWORK_ERROR: "网络连接失败", NETWORK_ERROR: "网络连接失败",

View File

@ -1,4 +1,4 @@
import { getSetting } from './settings'; import {getSetting} from './settings';
class LogDB { class LogDB {
constructor() { constructor() {
@ -39,7 +39,7 @@ const defaultOptions = {
}; };
async function createMessage(type, title, content = '', options = {}) { async function createMessage(type, title, content = '', options = {}) {
const msgOptions = { ...defaultOptions, ...options }; const msgOptions = {...defaultOptions, ...options};
const message = { const message = {
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
type, type,
@ -87,8 +87,12 @@ export default {
warning: (title, content, options) => createMessage(MessageType.WARNING, title, content, options), warning: (title, content, options) => createMessage(MessageType.WARNING, title, content, options),
}; };
}, },
onSnackbar: (callback) => { snackbarCallback = callback; }, onSnackbar: (callback) => {
onLog: (callback) => { logCallback = callback; }, snackbarCallback = callback;
},
onLog: (callback) => {
logCallback = callback;
},
getMessages: async () => { getMessages: async () => {
try { try {
return await logDB.getLogs(); return await logDB.getLogs();
@ -107,7 +111,8 @@ export default {
} }
}, },
MessageType, MessageType,
markAsRead: () => {}, // 移除标记已读功能 markAsRead: () => {
}, // 移除标记已读功能
deleteMessage: async (messageId) => { deleteMessage: async (messageId) => {
try { try {
await logDB.deleteLog(messageId); await logDB.deleteLog(messageId);

View File

@ -1,5 +1,5 @@
import { openDB } from "idb"; import {openDB} from "idb";
import { formatResponse, formatError } from "../dataProvider"; import {formatResponse, formatError} from "../dataProvider";
// Database initialization for local storage // Database initialization for local storage
const DB_NAME = "ClassworksDB"; const DB_NAME = "ClassworksDB";
@ -112,4 +112,4 @@ export const kvLocalProvider = {
return formatError("获取本地键名列表失败:" + error.message); return formatError("获取本地键名列表失败:" + error.message);
} }
}, },
}; };

View File

@ -1,10 +1,10 @@
import axios from "@/axios/axios"; import axios from "@/axios/axios";
import { formatResponse, formatError } from "../dataProvider"; import {formatResponse, formatError} from "../dataProvider";
import { getSetting } from "../settings"; import {getSetting} from "../settings";
// Helper function to get request headers with kvtoken // Helper function to get request headers with kvtoken
const getHeaders = () => { const getHeaders = () => {
const headers = { Accept: "application/json" }; const headers = {Accept: "application/json"};
const kvToken = getSetting("server.kvToken"); const kvToken = getSetting("server.kvToken");
const siteKey = getSetting("server.siteKey"); const siteKey = getSetting("server.siteKey");

View File

@ -556,8 +556,8 @@ class SettingsManagerClass {
definition.type === "boolean" definition.type === "boolean"
? Boolean(value) ? Boolean(value)
: definition.type === "number" : definition.type === "number"
? Number(value) ? Number(value)
: String(value); : String(value);
} }
// 验证 // 验证
@ -636,7 +636,8 @@ class SettingsManagerClass {
* @returns {Function} 取消监听的函数 * @returns {Function} 取消监听的函数
*/ */
watchSettings(callback) { watchSettings(callback) {
if (typeof window === "undefined") return () => {}; if (typeof window === "undefined") return () => {
};
const handler = (event) => { const handler = (event) => {
if (event.key === SETTINGS_STORAGE_KEY) { if (event.key === SETTINGS_STORAGE_KEY) {

View File

@ -2,8 +2,8 @@
// - Uses server domain from settings when available // - Uses server domain from settings when available
// - Exposes join/leave helpers and event on/off wrappers // - Exposes join/leave helpers and event on/off wrappers
import { io } from 'socket.io-client'; import {io} from 'socket.io-client';
import { getSetting } from '@/utils/settings'; import {getSetting} from '@/utils/settings';
let socket = null; let socket = null;
let connectedDomain = null; let connectedDomain = null;
@ -20,16 +20,18 @@ export function getSocket() {
const serverUrl = getServerUrl(); const serverUrl = getServerUrl();
if (!socket || connectedDomain !== serverUrl) { if (!socket || connectedDomain !== serverUrl) {
if (socket) { if (socket) {
try { socket.disconnect(); } catch (e) { try {
socket.disconnect();
} catch (e) {
void e; // ignore void e; // ignore
} }
socket = null; socket = null;
} }
connectedDomain = serverUrl; connectedDomain = serverUrl;
socket = io(serverUrl, { transports: ['websocket'] }); socket = io(serverUrl, {transports: ['websocket']});
// Re-attach previously registered event handlers on new socket instance // Re-attach previously registered event handlers on new socket instance
listeners.forEach(({ event, handler }) => { listeners.forEach(({event, handler}) => {
socket.on(event, handler); socket.on(event, handler);
}); });
} }
@ -39,7 +41,7 @@ export function getSocket() {
export function on(event, handler) { export function on(event, handler) {
const s = getSocket(); const s = getSocket();
s.on(event, handler); s.on(event, handler);
listeners.add({ event, handler }); listeners.add({event, handler});
return () => off(event, handler); return () => off(event, handler);
} }
@ -57,12 +59,12 @@ export function off(event, handler) {
export function joinToken(token) { export function joinToken(token) {
const s = getSocket(); const s = getSocket();
if (!token) return; if (!token) return;
s.emit('join-token', { token }); s.emit('join-token', {token});
} }
export function leaveToken(token) { export function leaveToken(token) {
if (!socket) return; if (!socket) return;
socket.emit('leave-token', { token }); socket.emit('leave-token', {token});
} }
export function leaveAll() { export function leaveAll() {
@ -78,7 +80,9 @@ export function onConnect(handler) {
export function disconnect() { export function disconnect() {
if (!socket) return; if (!socket) return;
try { socket.disconnect(); } catch (e) { try {
socket.disconnect();
} catch (e) {
void e; // ignore void e; // ignore
} }
socket = null; socket = null;